2015: A Swipe Back Odyssey

This commit is contained in:
Adam Bradley
2015-05-21 11:22:39 -05:00
parent 108467dbec
commit 980fa6d771
7 changed files with 261 additions and 88 deletions

View File

@ -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]);
// 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() )
);
}
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);
}
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;
}
}

View File

@ -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 );
}
}

View File

@ -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;
}

View File

@ -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)
}
}

View File

@ -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);
}

View File

@ -46,6 +46,8 @@ class NoneTransition {
return Promise.resolve();
}
dispose(){}
}
Transition.register('none', NoneTransition);

View File

@ -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);
};