nativescript-slides
Version:
NativeScript Slides plugin.
679 lines (590 loc) • 19.2 kB
text/typescript
require('nativescript-dom');
import * as app from 'application';
import * as Platform from 'platform';
import * as utils from 'tns-core-modules/utils/utils';
import { AbsoluteLayout } from 'ui/layouts/absolute-layout';
import { StackLayout } from 'ui/layouts/stack-layout';
import { View } from 'ui/core/view';
import { Button } from 'ui/button';
import { Label } from 'ui/label';
import * as AnimationModule from 'ui/animation';
import * as gestures from 'ui/gestures';
import { AnimationCurve, Orientation } from 'ui/enums';
import { Color } from 'color';
import { Image } from 'ui/image';
declare const android: any;
declare const com: any;
declare const java: any;
const SLIDE_INDICATOR_INACTIVE = 'slide-indicator-inactive';
const SLIDE_INDICATOR_ACTIVE = 'slide-indicator-active';
const SLIDE_INDICATOR_WRAP = 'slide-indicator-wrap';
let LayoutParams: any;
if (app.android) {
LayoutParams = <any>android.view.WindowManager.LayoutParams;
} else {
LayoutParams = {};
}
export class Slide extends StackLayout {}
enum direction {
none,
left,
right
}
enum cancellationReason {
user,
noPrevSlides,
noMoreSlides
}
export interface ISlideMap {
panel: StackLayout;
index: number;
left?: ISlideMap;
right?: ISlideMap;
}
export class SlideContainer extends AbsoluteLayout {
private currentPanel: ISlideMap;
private transitioning: boolean = false;
private direction: direction = direction.none;
private _loaded: boolean;
private _pageWidth: number;
private _loop: boolean;
private _pagerOffset: string;
private _angular: boolean;
private _disablePan: boolean;
private _footer: StackLayout;
private _pageIndicators: boolean;
private _slideMap: ISlideMap[];
private _slideWidth: string;
public static startEvent = 'start';
public static changedEvent = 'changed';
public static cancelledEvent = 'cancelled';
public static finishedEvent = 'finished';
/* page indicator stuff*/
get pageIndicators(): boolean {
return this._pageIndicators;
}
set pageIndicators(value: boolean) {
if (typeof value === 'string') {
value = <any>value == 'true';
}
this._pageIndicators = value;
}
get pagerOffset(): string {
return this._pagerOffset;
}
set pagerOffset(value: string) {
this._pagerOffset = value;
}
get hasNext(): boolean {
return !!this.currentPanel && !!this.currentPanel.right;
}
get hasPrevious(): boolean {
return !!this.currentPanel && !!this.currentPanel.left;
}
get loop() {
return this._loop;
}
set loop(value: boolean) {
this._loop = value;
}
get disablePan() {
return this._disablePan;
}
set disablePan(value: boolean) {
if (this._disablePan === value) {
return;
} // Value did not change
this._disablePan = value;
if (this._loaded && this.currentPanel.panel !== undefined) {
if (value === true) {
this.currentPanel.panel.off('pan');
} else if (value === false) {
this.applySwipe(this.pageWidth);
}
}
}
get pageWidth() {
if (!this.slideWidth) {
return Platform.screen.mainScreen.widthDIPs;
}
return +this.slideWidth;
}
get angular(): boolean {
return this._angular;
}
set angular(value: boolean) {
this._angular = value;
}
get currentIndex(): number {
return this.currentPanel.index;
}
get slideWidth(): string {
return this._slideWidth;
}
set slideWidth(width: string) {
this._slideWidth = width;
}
constructor() {
super();
this.setupDefaultValues();
// if being used in an ng2 app we want to prevent it from excuting the constructView
// until it is called manually in ngAfterViewInit.
this.constructView(true);
}
private setupDefaultValues(): void {
this.clipToBounds = true;
this._loaded = false;
if (this._loop == null) {
this.loop = false;
}
this.transitioning = false;
if (this._disablePan == null) {
this.disablePan = false;
}
if (this._angular == null) {
this.angular = false;
}
if (this._pageIndicators == null) {
this._pageIndicators = false;
}
if (this._pagerOffset == null) {
this._pagerOffset = '88%'; //defaults to white.
}
}
public constructView(constructor: boolean = false): void {
this.on(AbsoluteLayout.loadedEvent, (data: any) => {
//// console.log('LOADDED EVENT');
if (!this._loaded) {
this._loaded = true;
if (this.angular === true && constructor === true) {
return;
}
let slides: StackLayout[] = [];
if (!this.slideWidth) {
this.slideWidth = <any>this.pageWidth;
}
this.width = +this.slideWidth;
this.eachLayoutChild((view: View) => {
if (view instanceof StackLayout) {
AbsoluteLayout.setLeft(view, this.pageWidth);
view.width = this.pageWidth;
(<any>view).height = '100%'; //get around compiler
slides.push(view);
}
});
if (this.pageIndicators) {
this._footer = this.buildFooter(slides.length, 0);
this.setActivePageIndicator(0);
this.insertChild(this._footer, this.getChildrenCount());
}
this.currentPanel = this.buildSlideMap(slides);
if (this.currentPanel) {
this.positionPanels(this.currentPanel);
if (this.disablePan === false) {
this.applySwipe(this.pageWidth);
}
if (app.ios) {
this.ios.clipsToBound = true;
}
//handles application orientation change
app.on(
app.orientationChangedEvent,
(args: app.OrientationChangedEventData) => {
//event and page orientation didn't seem to alwasy be on the same page so setting it in the time out addresses this.
setTimeout(() => {
// console.log('orientationChangedEvent');
this.width = parseInt(this.slideWidth);
this.eachLayoutChild((view: View) => {
if (view instanceof StackLayout) {
AbsoluteLayout.setLeft(view, this.pageWidth);
view.width = this.pageWidth;
}
});
if (this.disablePan === false) {
this.applySwipe(this.pageWidth);
}
if (this.pageIndicators) {
AbsoluteLayout.setTop(this._footer, 0);
var pageIndicatorsLeftOffset = this.pageWidth / 4;
AbsoluteLayout.setLeft(
this._footer,
pageIndicatorsLeftOffset
);
this._footer.width = this.pageWidth / 2;
this._footer.marginTop = <any>this._pagerOffset;
}
this.positionPanels(this.currentPanel);
}, 0);
}
);
}
}
});
}
public nextSlide(): void {
if (!this.hasNext) {
this.triggerCancelEvent(cancellationReason.noMoreSlides);
return;
}
this.direction = direction.left;
this.transitioning = true;
this.triggerStartEvent();
this.showRightSlide(this.currentPanel).then(() => {
this.setupPanel(this.currentPanel.right);
this.triggerChangeEventRightToLeft();
});
}
public previousSlide(): void {
if (!this.hasPrevious) {
this.triggerCancelEvent(cancellationReason.noPrevSlides);
return;
}
this.direction = direction.right;
this.transitioning = true;
this.triggerStartEvent();
this.showLeftSlide(this.currentPanel).then(() => {
this.setupPanel(this.currentPanel.left);
this.triggerChangeEventLeftToRight();
});
}
private setupPanel(panel: ISlideMap) {
this.direction = direction.none;
this.transitioning = false;
this.currentPanel.panel.off('pan');
this.currentPanel = panel;
// sets up each panel so that they are positioned to transition either way.
this.positionPanels(this.currentPanel);
if (this.disablePan === false) {
this.applySwipe(this.pageWidth);
}
if (this.pageIndicators) {
this.setActivePageIndicator(this.currentPanel.index);
}
}
private positionPanels(panel: ISlideMap) {
// sets up each panel so that they are positioned to transition either way.
if (panel.left != null) {
panel.left.panel.translateX = -this.pageWidth * 2;
}
panel.panel.translateX = -this.pageWidth;
if (panel.right != null) {
panel.right.panel.translateX = 0;
}
}
public goToSlide(index: number): void {
if (
this._slideMap &&
this._slideMap.length > 0 &&
index < this._slideMap.length
) {
let previousSlide = this.currentPanel;
this.setupPanel(this._slideMap[index]);
this.notify({
eventName: SlideContainer.changedEvent,
object: this,
eventData: {
direction: direction.none,
newIndex: this.currentPanel.index,
oldIndex: previousSlide.index
}
});
} else {
// console.log('invalid index');
}
}
public applySwipe(pageWidth: number): void {
let previousDelta = -1; //hack to get around ios firing pan event after release
let endingVelocity = 0;
let startTime, deltaTime;
this.currentPanel.panel.on(
'pan',
(args: gestures.PanGestureEventData): void => {
if (args.state === gestures.GestureStateTypes.began) {
startTime = Date.now();
previousDelta = 0;
endingVelocity = 250;
this.triggerStartEvent();
} else if (args.state === gestures.GestureStateTypes.ended) {
deltaTime = Date.now() - startTime;
// if velocityScrolling is enabled then calculate the velocitty
// swiping left to right.
if (args.deltaX > pageWidth / 3) {
if (this.hasPrevious) {
this.transitioning = true;
this.showLeftSlide(
this.currentPanel,
args.deltaX,
endingVelocity
).then(() => {
this.setupPanel(this.currentPanel.left);
this.triggerChangeEventLeftToRight();
});
} else {
//We're at the start
//Notify no more slides
this.triggerCancelEvent(cancellationReason.noPrevSlides);
}
return;
}
// swiping right to left
else if (args.deltaX < -pageWidth / 3) {
if (this.hasNext) {
this.transitioning = true;
this.showRightSlide(
this.currentPanel,
args.deltaX,
endingVelocity
).then(() => {
this.setupPanel(this.currentPanel.right);
// Notify changed
this.triggerChangeEventRightToLeft();
if (!this.hasNext) {
// Notify finsihed
this.notify({
eventName: SlideContainer.finishedEvent,
object: this
});
}
});
} else {
// We're at the end
// Notify no more slides
this.triggerCancelEvent(cancellationReason.noMoreSlides);
}
return;
}
if (this.transitioning === false) {
//Notify cancelled
this.triggerCancelEvent(cancellationReason.user);
this.transitioning = true;
this.currentPanel.panel.animate({
translate: { x: -this.pageWidth, y: 0 },
duration: 200,
curve: AnimationCurve.easeOut
});
if (this.hasNext) {
this.currentPanel.right.panel.animate({
translate: { x: 0, y: 0 },
duration: 200,
curve: AnimationCurve.easeOut
});
if (app.ios)
//for some reason i have to set these in ios or there is some sort of bounce back.
this.currentPanel.right.panel.translateX = 0;
}
if (this.hasPrevious) {
this.currentPanel.left.panel.animate({
translate: { x: -this.pageWidth * 2, y: 0 },
duration: 200,
curve: AnimationCurve.easeOut
});
if (app.ios)
this.currentPanel.left.panel.translateX = -this.pageWidth;
}
if (app.ios) this.currentPanel.panel.translateX = -this.pageWidth;
this.transitioning = false;
}
} else {
if (
!this.transitioning &&
previousDelta !== args.deltaX &&
args.deltaX != null &&
args.deltaX < 0
) {
if (this.hasNext) {
this.direction = direction.left;
this.currentPanel.panel.translateX = args.deltaX - this.pageWidth;
this.currentPanel.right.panel.translateX = args.deltaX;
}
} else if (
!this.transitioning &&
previousDelta !== args.deltaX &&
args.deltaX != null &&
args.deltaX > 0
) {
if (this.hasPrevious) {
this.direction = direction.right;
this.currentPanel.panel.translateX = args.deltaX - this.pageWidth;
this.currentPanel.left.panel.translateX =
-(this.pageWidth * 2) + args.deltaX;
}
}
if (args.deltaX !== 0) {
previousDelta = args.deltaX;
}
}
}
);
}
private showRightSlide(
panelMap: ISlideMap,
offset: number = this.pageWidth,
endingVelocity: number = 32
): AnimationModule.AnimationPromise {
let animationDuration: number;
animationDuration = 300; // default value
let transition = new Array();
transition.push({
target: panelMap.right.panel,
translate: { x: -this.pageWidth, y: 0 },
duration: animationDuration,
curve: AnimationCurve.easeOut
});
transition.push({
target: panelMap.panel,
translate: { x: -this.pageWidth * 2, y: 0 },
duration: animationDuration,
curve: AnimationCurve.easeOut
});
let animationSet = new AnimationModule.Animation(transition, false);
return animationSet.play();
}
private showLeftSlide(
panelMap: ISlideMap,
offset: number = this.pageWidth,
endingVelocity: number = 32
): AnimationModule.AnimationPromise {
let animationDuration: number;
animationDuration = 300; // default value
let transition = new Array();
transition.push({
target: panelMap.left.panel,
translate: { x: -this.pageWidth, y: 0 },
duration: animationDuration,
curve: AnimationCurve.easeOut
});
transition.push({
target: panelMap.panel,
translate: { x: 0, y: 0 },
duration: animationDuration,
curve: AnimationCurve.easeOut
});
let animationSet = new AnimationModule.Animation(transition, false);
return animationSet.play();
}
private buildFooter(
pageCount: number = 5,
activeIndex: number = 0
): StackLayout {
let footerInnerWrap = new StackLayout();
//footerInnerWrap.height = 50;
if (app.ios) {
footerInnerWrap.clipToBounds = false;
}
footerInnerWrap.className = SLIDE_INDICATOR_WRAP;
AbsoluteLayout.setTop(footerInnerWrap, 0);
footerInnerWrap.orientation = 'horizontal';
footerInnerWrap.horizontalAlignment = 'center';
footerInnerWrap.width = this.pageWidth / 2;
let index = 0;
while (index < pageCount) {
footerInnerWrap.addChild(this.createIndicator(index));
index++;
}
let pageIndicatorsLeftOffset = this.pageWidth / 4;
AbsoluteLayout.setLeft(footerInnerWrap, pageIndicatorsLeftOffset);
footerInnerWrap.marginTop = <any>this._pagerOffset;
return footerInnerWrap;
}
private setwidthPercent(view: View, percentage: number) {
(<any>view).width = percentage + '%';
}
private newFooterButton(name: string): Button {
let button = new Button();
button.id = 'btn-info-' + name.toLowerCase();
button.text = name;
this.setwidthPercent(button, 100);
return button;
}
private buildSlideMap(views: StackLayout[]) {
this._slideMap = [];
views.forEach((view: StackLayout, index: number) => {
this._slideMap.push({
panel: view,
index: index
});
});
this._slideMap.forEach((mapping: ISlideMap, index: number) => {
if (this._slideMap[index - 1] != null)
mapping.left = this._slideMap[index - 1];
if (this._slideMap[index + 1] != null)
mapping.right = this._slideMap[index + 1];
});
if (this.loop === true) {
this._slideMap[0].left = this._slideMap[this._slideMap.length - 1];
this._slideMap[this._slideMap.length - 1].right = this._slideMap[0];
}
return this._slideMap[0];
}
private triggerStartEvent() {
this.notify({
eventName: SlideContainer.startEvent,
object: this,
eventData: {
currentIndex: this.currentPanel.index
}
});
}
private triggerChangeEventLeftToRight() {
this.notify({
eventName: SlideContainer.changedEvent,
object: this,
eventData: {
direction: direction.left,
newIndex: this.currentPanel.index,
oldIndex: this.currentPanel.index + 1
}
});
}
private triggerChangeEventRightToLeft() {
this.notify({
eventName: SlideContainer.changedEvent,
object: this,
eventData: {
direction: direction.right,
newIndex: this.currentPanel.index,
oldIndex: this.currentPanel.index - 1
}
});
}
private triggerCancelEvent(cancelReason: cancellationReason) {
this.notify({
eventName: SlideContainer.cancelledEvent,
object: this,
eventData: {
currentIndex: this.currentPanel.index,
reason: cancelReason
}
});
}
createIndicator(index: number): Label {
let indicator = new Label();
(<any>indicator).classList.add(SLIDE_INDICATOR_INACTIVE);
return indicator;
}
setActivePageIndicator(index: number) {
let indicatorsToDeactivate = (<any>this._footer).getElementsByClassName(
SLIDE_INDICATOR_ACTIVE
);
indicatorsToDeactivate.forEach(activeIndicator => {
activeIndicator.classList.remove(SLIDE_INDICATOR_ACTIVE);
activeIndicator.classList.add(SLIDE_INDICATOR_INACTIVE);
});
let activeIndicator = (<any>this._footer).getElementsByClassName(
SLIDE_INDICATOR_INACTIVE
)[index];
if (activeIndicator) {
activeIndicator.classList.remove(SLIDE_INDICATOR_INACTIVE);
activeIndicator.classList.add(SLIDE_INDICATOR_ACTIVE);
}
}
iosProperty(theClass, theProperty) {
if (typeof theProperty === 'function') {
// xCode 7 and below
return theProperty.call(theClass);
} else {
// xCode 8+
return theProperty;
}
}
}