diff --git a/ionic/animations/animation.js b/ionic/animations/animation.js index bcc74c8900..ce137a7e21 100644 --- a/ionic/animations/animation.js +++ b/ionic/animations/animation.js @@ -13,6 +13,7 @@ export class Animation { this._to = null; this._duration = null; this._easing = null; + this._rate = null; this._beforeAddCls = []; this._beforeRmvCls = []; @@ -77,6 +78,21 @@ export class Animation { return this._easing || (this._parent && this._parent.easing()); } + playbackRate(value) { + if (arguments.length) { + this._rate = value; + var i; + for (i = 0; i < this._children.length; i++) { + this._children[i].playbackRate(value); + } + for (i = 0; i < this._players.length; i++) { + this._players[i].playbackRate(value); + } + return this; + } + return this._rate || (this._parent && this._parent.playbackRate()); + } + from(property, value) { if (!this._from) { this._from = {}; @@ -120,9 +136,10 @@ export class Animation { } play() { + var i; let promises = []; - for (let i = 0; i < this._children.length; i++) { + for (i = 0; i < this._children.length; i++) { promises.push( this._children[i].play() ); } @@ -132,60 +149,57 @@ export class Animation { } if (!this._players.length) { - for (let i = 0; i < this._el.length; i++) { - var ele = this._el[i]; - - for (let j = 0; j < this._beforeAddCls.length; j++) { - ele.classList.add(this._beforeAddCls[j]); - } - - for (let j = 0; j < this._beforeRmvCls.length; j++) { - ele.classList.remove(this._beforeRmvCls[j]); - } - - var player = new Animate(ele, this._from, this._to, this.duration(), this.easing()); - this._players.push(player); - - promises.push(player.promise); + // first time played + for (i = 0; i < this._el.length; i++) { + this._players.push( + new Animate( this._el[i], + this._from, + this._to, + this.duration(), + this.easing(), + this.playbackRate() ) + ); } + this._onReady(); + } else { - for (let i = 0; i < this._players.length; i++) { + // has been paused, now play again + for (i = 0; i < this._players.length; i++) { this._players[i].play(); } } - var promise = Promise.all(promises); + for (i = 0; i < this._players.length; i++) { + promises.push(this._players[i].promise); + } + + let promise = Promise.all(promises); promise.then(() => { - for (let i = 0; i < this._el.length; i++) { - var ele = this._el[i]; - - for (let j = 0; j < this._afterAddCls.length; j++) { - ele.classList.add(this._afterAddCls[j]); - } - - for (let j = 0; j < this._afterRmvCls.length; j++) { - ele.classList.remove(this._afterRmvCls[j]); - } - } + this._onFinish(); }); return promise; } pause() { - for (let i = 0; i < this._children.length; i++) { + this._hasFinished = false; + + var i; + for (i = 0; i < this._children.length; i++) { this._children[i].pause(); } - for (let i = 0; i < this._players.length; i++) { + for (i = 0; i < this._players.length; i++) { this._players[i].pause(); } } progress(value) { - for (let i = 0; i < this._children.length; i++) { + var i; + + for (i = 0; i < this._children.length; i++) { this._children[i].progress(value); } @@ -194,16 +208,69 @@ export class Animation { this.pause(); } - for (let i = 0; i < this._players.length; i++) { + for (i = 0; i < this._players.length; i++) { this._players[i].progress(value); } } + _onReady() { + if (!this._hasPlayed) { + this._hasPlayed = true; + + var i, j, ele; + for (i = 0; i < this._el.length; i++) { + ele = this._el[i]; + + for (j = 0; j < this._beforeAddCls.length; j++) { + ele.classList.add(this._beforeAddCls[j]); + } + + for (j = 0; j < this._beforeRmvCls.length; j++) { + ele.classList.remove(this._beforeRmvCls[j]); + } + } + + this.onReady && this.onReady(); + } + } + + _onFinish() { + if (!this._hasFinished) { + this._hasFinished = true; + + var i, j, ele; + for (i = 0; i < this._el.length; i++) { + ele = this._el[i]; + + for (j = 0; j < this._afterAddCls.length; j++) { + ele.classList.add(this._afterAddCls[j]); + } + + for (j = 0; j < this._afterRmvCls.length; j++) { + ele.classList.remove(this._afterRmvCls[j]); + } + } + + this.onFinish && this.onFinish(); + } + } + + dispose() { + var i; + for (i = 0; i < this._children.length; i++) { + this._children[i].dispose(); + } + for (i = 0; i < this._players.length; i++) { + this._players[i].dispose(); + } + this._el = this._parent = this._children = this._players = null; + } + } class Animate { - constructor(ele, fromEffect, toEffect, duration, easing) { + constructor(ele, fromEffect, toEffect, duration, easing, playbackRate) { // https://w3c.github.io/web-animations/ // not using the direct API methods because they're still in flux // however, element.animate() seems locked in and uses the latest @@ -226,6 +293,7 @@ class Animate { this.player = ele.animate([fromEffect, toEffect], { duration: duration, easing: easing, + playbackRate: playbackRate || 1, fill: 'both' }); @@ -264,4 +332,12 @@ class Animate { player.currentTime = (this._duration * value); } + playbackRate(value) { + this.player.playbackRate = value; + } + + dispose() { + this.player = null; + } + } diff --git a/ionic/components/app/test/animations/index.js b/ionic/components/app/test/animations/index.js index a9b43fa2e2..36e6cc0fbe 100644 --- a/ionic/components/app/test/animations/index.js +++ b/ionic/components/app/test/animations/index.js @@ -27,6 +27,7 @@ class IonicApp { row1 .from('opacity', 1) .to('opacity', 0) + .to('transform', 'scale(0)') .beforePlay.addClass('added-before-play') .afterFinish.addClass('added-after-finish') @@ -38,22 +39,26 @@ class IonicApp { this.animation.children(row1, row2); + this.animation.onReady = () => { + console.log('onReady'); + } + + this.animation.onFinish = () => { + console.log('onFinish'); + } + } play() { - console.debug('play'); - this.animation.play();; + this.animation.play(); } pause() { - console.debug('pause'); - this.animation.pause(); } progress(ev) { - let value = ev.srcElement.value; - this.animation.progress(value); + this.animation.progress( ev.srcElement.value ); } } diff --git a/ionic/components/content/swipe-handle.js b/ionic/components/content/swipe-handle.js index 31597abf11..ab05ad0ee3 100644 --- a/ionic/components/content/swipe-handle.js +++ b/ionic/components/content/swipe-handle.js @@ -24,10 +24,34 @@ export class SwipeHandle { let navWidth = 0; function onDragEnd(ev) { - let completeSwipeBack = (ev.gesture.center.x / navWidth) > 0.5; + // TODO: POLISH THESE NUMBERS WITH GOOD MATHIFICATION - nav.swipeBackEnd(completeSwipeBack); + let progress = ev.gesture.center.x / navWidth; + let completeSwipeBack = (progress > 0.5); + let playbackRate = 4; + if (completeSwipeBack) { + // complete swipe back + if (progress > 0.7) { + playbackRate = 3; + } else if (progress > 0.8) { + playbackRate = 2; + } else if (progress > 0.9) { + playbackRate = 1; + } + + } else { + // cancel swipe back + if (progress < 0.3) { + playbackRate = 3; + } else if (progress < 0.2) { + playbackRate = 2; + } else if (progress < 0.1) { + playbackRate = 1; + } + } + + nav.swipeBackEnd(completeSwipeBack, progress, playbackRate); navWidth = 0; } diff --git a/ionic/components/nav/nav-base.js b/ionic/components/nav/nav-base.js index 9e2fc90c07..f17c756b7d 100644 --- a/ionic/components/nav/nav-base.js +++ b/ionic/components/nav/nav-base.js @@ -20,6 +20,7 @@ export class NavBase { this.injector = injector; this.items = []; this.navCtrl = new NavController(this); + this.swipeBackTransition = null; } set initial(Class) { @@ -80,7 +81,7 @@ export class NavBase { // get the active item and set that it is staged to be leaving // was probably the one popped from the stack - let leavingItem = this.getActive() || {}; + let leavingItem = this.getActive(); leavingItem.shouldDestroy = true; // the entering item is now the new last item @@ -133,6 +134,9 @@ export class NavBase { // destroy any items that shouldn't stay around this.cleanup(); + // dispose all references + transAnimation.dispose(); + // allow clicks again ClickBlock(false); @@ -147,6 +151,98 @@ export class NavBase { return promise; } + swipeBackStart() { + if (this.isTransitioning() || this.items.length < 2) { + return; + } + + this.swipeBackResolve = null; + + // default the direction to "back" + let opts = { + direction: 'back' + }; + + // get the active item and set that it is staged to be leaving + // was probably the one popped from the stack + let leavingItem = this.getActive(); + leavingItem.shouldDestroy = true; + + // the entering item is now the new last item + let enteringItem = this.getPrevious(leavingItem); + enteringItem.shouldDestroy = false; + + // start the transition + // block possible clicks during transition + ClickBlock(true); + + // wait for the new item to complete setup + enteringItem.stage().then(() => { + + // set that the leaving item is stage to be leaving + leavingItem.state = STAGED_LEAVING_STATE; + + // set that the new item pushed on the stack is staged to be entering + // setting staged state is important for the transition logic to find the correct item + enteringItem.state = STAGED_ENTERING_STATE; + + // init the transition animation + this.swipeBackTransition = Transition.create(this, opts); + this.swipeBackTransition.easing('linear'); + + // wait for the items to be fully staged + this.swipeBackTransition.stage().then(() => { + + // update the state that the items are actively entering/leaving + enteringItem.state = ACTIVELY_ENTERING_STATE; + leavingItem.state = ACTIVELY_LEAVING_STATE; + + let swipeBackPromise = new Promise(res => { this.swipeBackResolve = res; }); + + swipeBackPromise.then(() => { + + // transition has completed, update each item's state + enteringItem.state = ACTIVE_STATE; + leavingItem.state = CACHED_STATE; + + // destroy any items that shouldn't stay around + this.cleanup(); + + // allow clicks again + ClickBlock(false); + + }); + + }); + + }); + + } + + swipeBackEnd(completeSwipeBack, progress, playbackRate) { + console.log('swipeBackEnd, completeSwipeBack: ', completeSwipeBack, ' progress:', progress, ' playbackRate:', playbackRate); + + this.swipeBackTransition.playbackRate(playbackRate); + + this.swipeBackTransition.play().then(() => { + this.swipeBackResolve && this.swipeBackResolve(); + + if (this.swipeBackTransition) { + this.swipeBackTransition.dispose(); + } + + this.swipeBackResolve = this.swipeBackTransition = null; + }); + + } + + swipeBackProgress(progress) { + if (this.swipeBackTransition) { + ClickBlock(true, 4000); + this.swipeBackTransition.progress( Math.min(1, Math.max(0, progress)) ); + } + } + cleanup() { for (let i = 0, ii = this.items.length; i < ii; i++) { @@ -210,30 +306,8 @@ export class NavBase { return null; } - last() { - return this.items[this.items.length - 1] - } - - length() { - return this.items.length; - } - remove(itemOrIndex) { util.array.remove(this.items, itemOrIndex); } - swipeBackStart() { - console.log('swipeBackStart') - } - - swipeBackEnd(completeSwipeBack) { - console.log('swipeBackEnd, completeSwipeBack:', completeSwipeBack) - } - - swipeBackProgress(value) { - value = Math.min(1, Math.max(0, value)); - - console.log('swipeBackProgress', value) - } - } diff --git a/ionic/transitions/ios-transition.js b/ionic/transitions/ios-transition.js index c1db088f85..da0c5b3161 100644 --- a/ionic/transitions/ios-transition.js +++ b/ionic/transitions/ios-transition.js @@ -76,6 +76,7 @@ class IOSTransition extends Animation { // leaving view moves off screen // when completed, set leavingItem to display: none leavingContent + .beforePlay.addClass(SHOW_NAV_ITEM_CSS) .afterFinish.removeClass(SHOW_NAV_ITEM_CSS) .from(TRANSFORM, CENTER) .from(OPACITY, 1); @@ -87,8 +88,8 @@ class IOSTransition extends Animation { .from(TRANSFORM, CENTER) .from(OPACITY, 1); - if (leavingItem && leavingItem.enableBack) { - let leavingBackButton = new Animation(leavingItem.getBackButton()) + if (leavingItem) { + let leavingBackButton = new Animation(leavingItem.getBackButton()); leavingBackButton.from(OPACITY, 1).to(OPACITY, 0); this.addChild(leavingBackButton); } diff --git a/ionic/transitions/none-transition.js b/ionic/transitions/none-transition.js index 6d7a334ca8..35a0a2df67 100644 --- a/ionic/transitions/none-transition.js +++ b/ionic/transitions/none-transition.js @@ -46,6 +46,8 @@ class NoneTransition { return Promise.resolve(); } + dispose(){} + } Transition.register('none', NoneTransition); diff --git a/ionic/util/click-block.js b/ionic/util/click-block.js index e80fd9b30e..b2e03c9524 100644 --- a/ionic/util/click-block.js +++ b/ionic/util/click-block.js @@ -1,9 +1,9 @@ -import {raf} from './dom' - const CSS_CLICK_BLOCK = 'click-block-active'; const DEFAULT_EXPIRE = 330; -let cbEle, fallbackTimerId, pendingShow; +let cbEle, fallbackTimerId; +let isShowing = false; + function preventClick(ev) { ev.preventDefault(); @@ -11,21 +11,11 @@ function preventClick(ev) { } function show(expire) { - pendingShow = true; clearTimeout(fallbackTimerId); fallbackTimerId = setTimeout(hide, expire || DEFAULT_EXPIRE); - raf(addBlock); -} - -function hide() { - pendingShow = false; - clearTimeout(fallbackTimerId); - raf(removeBlock); -} - -function addBlock() { - if (pendingShow) { + if (!isShowing) { + isShowing = true; if (cbEle) { cbEle.classList.add(CSS_CLICK_BLOCK); @@ -35,19 +25,20 @@ function addBlock() { document.body.appendChild(cbEle); cbEle.addEventListener('touchstart', preventClick); cbEle.addEventListener('mousedown', preventClick); + cbEle.addEventListener('pointerdown', preventClick); + cbEle.addEventListener('MSPointerDown', preventClick); } - pendingShow = false; } } -function removeBlock() { - if (!pendingShow) { - cbEle && cbEle.classList.remove(CSS_CLICK_BLOCK); +function hide() { + clearTimeout(fallbackTimerId); + if (isShowing) { + cbEle.classList.remove(CSS_CLICK_BLOCK); + isShowing = false; } } export let ClickBlock = function(shouldShow, expire) { - (shouldShow ? show : hide)(expire); - };