mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 12:29:55 +08:00
2015: A Swipe Back Odyssey
This commit is contained in:
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -46,6 +46,8 @@ class NoneTransition {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
dispose(){}
|
||||
|
||||
}
|
||||
|
||||
Transition.register('none', NoneTransition);
|
||||
|
@ -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);
|
||||
|
||||
};
|
||||
|
Reference in New Issue
Block a user