swipe back refactor

This commit is contained in:
Adam Bradley
2015-09-17 16:13:51 -05:00
parent 252a5ee747
commit 5fbc142dae
15 changed files with 224 additions and 292 deletions

View File

@ -392,6 +392,7 @@ export class Animation {
}
progress(value) {
value = Math.min(1, Math.max(0, value));
this.isProgress = true;
let i;
@ -589,7 +590,6 @@ class Animate {
if (animation) {
// passed a number between 0 and 1
value = Math.max(0, Math.min(1, value));
if (animation.playState !== 'paused') {
animation.pause();

View File

@ -84,7 +84,7 @@ export class Activator {
touchEnd(ev) {
let self = this;
if (self.tapPolyfill && self.start && !self.app.isTransitioning()) {
if (self.tapPolyfill && self.start && self.app.isEnabled()) {
let endCoord = pointerCoord(ev);
if (!hasPointerMoved(self.pointerTolerance, self.start, endCoord)) {
@ -140,7 +140,7 @@ export class Activator {
pointerStart(ev) {
let targetEle = this.getActivatableTarget(ev.target);
if (targetEle && !this.app.isTransitioning()) {
if (targetEle && this.app.isEnabled()) {
this.start = pointerCoord(ev);
this.queueActivate(targetEle);
@ -179,7 +179,7 @@ export class Activator {
* @return {boolean} True if click event should be allowed, otherwise false.
*/
allowClick(ev) {
if (this.app.isTransitioning()) {
if (!this.app.isEnabled()) {
return false;
}
if (!ev.isIonicTap) {
@ -257,11 +257,10 @@ export class Activator {
deactivate() {
const self = this;
if (self.app.isTransitioning() && self.deactivateAttempt < 30) {
// the app is actively transitioning, don't bother deactivating
// anything this makes it easier on the GPU so it doesn't
// have to redraw any buttons during a transition
// retry
if (!self.app.isEnabled() && self.deactivateAttempt < 30) {
// the app is actively disabled, so don't bother deactivating anything.
// this makes it easier on the GPU so it doesn't have to redraw any
// buttons during a transition. This will retry in XX milliseconds.
++self.deactivateAttempt;
self.queueDeactivate();

View File

@ -3,6 +3,7 @@ import {ROUTER_BINDINGS, HashLocationStrategy, LocationStrategy, Router} from 'a
import {IonicConfig} from '../../config/config';
import {IonicPlatform, Platform} from '../../platform/platform';
import {ClickBlock} from '../../util/click-block';
import * as util from '../../util/util';
// injectables
@ -40,7 +41,7 @@ export class IonicApp {
*/
constructor() {
this.overlays = [];
this._transDone = 0;
this._enableTime = 0;
// Our component registry map
this.components = {};
@ -77,21 +78,27 @@ export class IonicApp {
}
/**
* Sets if the app is currently transitioning or not. For example
* this is set to `true` while views transition, a modal slides up, an action-menu
* slides up, etc. After the transition completes it is set back to `false`.
* @param {bool} isTransitioning
* Sets if the app is currently enabled or not, meaning if it's
* available to accept new user commands. For example, this is set to `false`
* while views transition, a modal slides up, an action-menu
* slides up, etc. After the transition completes it is set back to `true`.
* @param {bool} isEnabled
* @param {bool} fallback When `isEnabled` is set to `false`, this argument
* is used to set the maximum number of milliseconds that app will wait until
* it will automatically enable the app again. It's basically a fallback incase
* something goes wrong during a transition and the app wasn't re-enabled correctly.
*/
setTransitioning(isTransitioning, msTilDone=800) {
this._transDone = (isTransitioning ? Date.now() + msTilDone : 0);
setEnabled(isEnabled, fallback=700) {
this._enableTime = (isEnabled ? 0 : Date.now() + fallback);
ClickBlock(!isEnabled, fallback + 100);
}
/**
* Boolean if the app is actively transitioning or not.
* @return {bool}
*/
isTransitioning() {
return (this._transDone > Date.now());
isEnabled() {
return (this._enableTime < Date.now());
}
/**

View File

@ -13,7 +13,6 @@ $z-index-navbar-container: 10 !default;
$z-index-content: 5 !default;
$z-index-toolbar: 10 !default;
$z-index-swipe-handle: 15 !default;
$z-index-toolbar-border: 20 !default;
$z-index-list-border: 50 !default;

View File

@ -129,6 +129,7 @@ export class Menu extends Ion {
setProgess(value) {
// user actively dragging the menu
this._disable();
this.app.setEnabled(false);
this._type.setProgess(value);
}
@ -147,7 +148,7 @@ export class Menu extends Ion {
this.getBackdropElement().classList.add('show-backdrop');
this._disable();
this.app.setTransitioning(true);
this.app.setEnabled(false);
}
_after(isOpen) {
@ -165,7 +166,7 @@ export class Menu extends Ion {
this.getBackdropElement().classList.remove('show-backdrop');
}
this.app.setTransitioning(false);
this.app.setEnabled(true);
}
_disable() {
@ -232,6 +233,7 @@ export class Menu extends Ion {
onDestroy() {
this.app.unregister(this.id);
this._gesture && this._gesture.destroy();
this._type && this._type.onDestroy();
this.contentElement = null;
}

View File

@ -3,7 +3,6 @@ import {Component, Directive, View, ElementRef, Inject, forwardRef, Injector, bi
import {Ion} from '../ion';
import {IonicConfig} from '../../config/config';
import {ViewController} from '../view/view-controller';
import {SwipeHandle} from './swipe-handle';
import {IonicComponent} from '../../config/annotations';
import {PaneAnchor, PaneContentAnchor, NavBarContainer} from './anchors';
@ -116,10 +115,9 @@ export class PaneController {
<template pane-anchor></template>
<section class="content-container">
<template content-anchor></template>
<div class="swipe-handle"></div>
</section>
`,
directives: [PaneAnchor, PaneContentAnchor, SwipeHandle]
directives: [PaneAnchor, PaneContentAnchor]
})
export class Pane extends Ion {
constructor(

View File

@ -1,119 +0,0 @@
import {ElementRef, Directive, Host, Optional, Inject, forwardRef, NgZone} from 'angular2/angular2';
import {ViewController} from '../view/view-controller';
import {Pane} from './pane';
import {Gesture} from 'ionic/gestures/gesture';
/**
* TODO
*/
@Directive({
selector: '.swipe-handle',
host: {
'[class.show-handle]': 'showHandle'
}
})
export class SwipeHandle {
/**
* TODO
* @param {ViewController=} viewCtrl TODO
* @param {Pane} pane TODO
* @param {ElementRef} elementRef TODO
* @param {NgZone} ngZone TODO
*/
constructor(
@Optional() @Inject(forwardRef(() => ViewController)) viewCtrl: ViewController,
@Host() @Inject(forwardRef(() => Pane)) pane: Pane,
elementRef: ElementRef,
ngZone: NgZone
) {
if (!viewCtrl || !viewCtrl.isSwipeBackEnabled() || !pane) return;
const self = this;
self.pane = pane;
self.viewCtrl = viewCtrl;
self.zone = ngZone;
this.zone.runOutsideAngular(() => {
let gesture = self.gesture = new Gesture(elementRef.nativeElement);
gesture.listen();
function dragHorizontal(ev) {
self.onDragHorizontal(ev);
}
gesture.on('panend', gesture => { self.onDragEnd(gesture); });
gesture.on('panleft', dragHorizontal);
gesture.on('panright', dragHorizontal);
});
self.startX = null;
self.width = null;
}
onDragEnd(gesture) {
gesture.srcEvent.preventDefault();
gesture.srcEvent.stopPropagation();
// TODO: POLISH THESE NUMBERS WITH GOOD MATHIFICATION
let progress = (gesture.center.x - this.startX) / this.width;
let completeSwipeBack = (progress > 0.5);
let playbackRate = 4;
if (completeSwipeBack) {
// complete swipe back
if (progress > 0.9) {
playbackRate = 1;
} else if (progress > 0.8) {
playbackRate = 2;
} else if (progress > 0.7) {
playbackRate = 3;
}
} else {
// cancel swipe back
if (progress < 0.1) {
playbackRate = 1;
} else if (progress < 0.2) {
playbackRate = 2;
} else if (progress < 0.3) {
playbackRate = 3;
}
}
this.zone.run(() => {
this.viewCtrl.swipeBackFinish(completeSwipeBack, playbackRate);
});
this.startX = null;
}
onDragHorizontal(gesture) {
this.zone.run(() => {
if (this.startX === null) {
// starting drag
gesture.srcEvent.preventDefault();
gesture.srcEvent.stopPropagation();
this.startX = gesture.center.x;
this.width = this.pane.width() - this.startX;
this.viewCtrl.swipeBackStart();
}
this.viewCtrl.swipeBackProgress( (gesture.center.x - this.startX) / this.width );
});
}
get showHandle() {
return (this.viewCtrl ? this.viewCtrl.canSwipeBack() : false);
}
onDestroy() {
this.gesture && this.gesture.destroy();
}
}

View File

@ -3,7 +3,6 @@ import {DirectiveBinding} from 'angular2/src/core/compiler/element_injector';
import {IonicApp} from '../app/app';
import {Animation} from '../../animations/animation';
import {ClickBlock} from '../../util/click-block';
import * as util from 'ionic/util';
@ -127,16 +126,14 @@ export class OverlayRef {
animation.before.addClass('show-overlay');
ClickBlock(true, animation.duration() + 200);
this.app.setTransitioning(true);
this.app.setEnabled(false, animation.duration());
this.app.zoneRunOutside(() => {
animation.play().then(() => {
this.app.zoneRun(() => {
ClickBlock(false);
this.app.setTransitioning(false);
this.app.setEnabled(true);
animation.dispose();
instance.viewDidEnter && instance.viewDidEnter();
resolve();
@ -161,8 +158,7 @@ export class OverlayRef {
let animation = Animation.create(this._elementRef.nativeElement, animationName);
animation.after.removeClass('show-overlay');
ClickBlock(true, animation.duration() + 200);
this.app.setTransitioning(true, animation.duration() + 200);
this.app.setEnabled(false, animation.duration());
animation.play().then(() => {
instance.viewDidLeave && instance.viewDidLeave();
@ -170,8 +166,7 @@ export class OverlayRef {
this._dispose();
ClickBlock(false);
this.app.setTransitioning(false);
this.app.setEnabled(true);
animation.dispose();
resolve();

View File

@ -1,5 +1,6 @@
import {Component, Directive, View, Injector, NgFor, ElementRef, Optional, Host, forwardRef, NgZone} from 'angular2/angular2';
import {IonicApp} from '../app/app';
import {ViewController} from '../view/view-controller';
import {ViewItem} from '../view/view-item';
import {Icon} from '../icon/icon';
@ -56,11 +57,13 @@ export class Tabs extends ViewController {
constructor(
@Optional() hostViewCtrl: ViewController,
@Optional() viewItem: ViewItem,
app: IonicApp,
injector: Injector,
elementRef: ElementRef,
zone: NgZone
) {
super(hostViewCtrl, injector, elementRef, zone);
this.app = app;
// Tabs may also be an actual ViewItem which was navigated to
// if Tabs is static and not navigated to within a ViewController
@ -117,7 +120,7 @@ export class Tabs extends ViewController {
enteringItem = this.getByInstance(tab)
}
if (!enteringItem || !enteringItem.instance || this.isTransitioning()) {
if (!enteringItem || !enteringItem.instance || !this.app.isEnabled()) {
return Promise.reject();
}

View File

@ -7,7 +7,6 @@ import {Label} from './label';
import {Ion} from '../ion';
import {IonicApp} from '../app/app';
import {Content} from '../content/content';
import {ClickBlock} from '../../util/click-block';
import * as dom from '../../util/dom';
import {IonicPlatform} from '../../platform/platform';
@ -160,7 +159,7 @@ export class TextInput extends Ion {
* @param {Event} ev TODO
*/
pointerStart(ev) {
if (this.scrollAssist && !this.app.isTransitioning()) {
if (this.scrollAssist && this.app.isEnabled()) {
// remember where the touchstart/mousedown started
this.startCoord = dom.pointerCoord(ev);
}
@ -171,8 +170,7 @@ export class TextInput extends Ion {
* @param {Event} ev TODO
*/
pointerEnd(ev) {
if (this.app.isTransitioning()) {
if (!this.app.isEnabled()) {
ev.preventDefault();
ev.stopPropagation();
@ -234,8 +232,7 @@ export class TextInput extends Ion {
// manually scroll the text input to the top
// do not allow any clicks while it's scrolling
ClickBlock(true, SCROLL_INTO_VIEW_DURATION + 100);
this.app.setTransitioning(true, SCROLL_INTO_VIEW_DURATION + 100);
this.app.setEnabled(false, SCROLL_INTO_VIEW_DURATION);
// temporarily move the focus to the focus holder so the browser
// doesn't freak out while it's trying to get the input in place
@ -249,8 +246,7 @@ export class TextInput extends Ion {
this.setFocus();
// all good, allow clicks again
ClickBlock(false);
this.app.setTransitioning(false);
this.app.setEnabled(true);
});
} else {

View File

@ -34,7 +34,7 @@ export class ToolbarBase extends Ion {
* @returns {TODO} TODO
*/
getTitleRef() {
return this.titleCmp && this.titleCmp.elementRef.textContent;
return this.titleCmp && this.titleCmp.elementRef;
}
/**

View File

@ -0,0 +1,29 @@
import {SlideEdgeGesture} from 'ionic/gestures/slide-edge-gesture';
export class SwipeBackGesture extends SlideEdgeGesture {
constructor(element: Element, opts: Object = {}, viewCtrl) {
super(element, opts);
// Can check corners through use of eg 'left top'
this.edges = opts.edge.split(' ');
this.threshold = opts.threshold;
this.viewCtrl = viewCtrl;
}
onSlideStart() {
this.viewCtrl.swipeBackStart();
}
onSlide(slide, ev) {
this.viewCtrl.swipeBackProgress(slide.distance / slide.max);
}
onSlideEnd(slide, ev) {
let shouldComplete = (Math.abs(ev.velocityX) > 0.2 || Math.abs(slide.delta) > Math.abs(slide.max) * 0.5);
// TODO: calculate a better playback rate depending on velocity and distance
this.viewCtrl.swipeBackFinish(shouldComplete, 1);
}
}

View File

@ -9,8 +9,7 @@ import {ViewItem} from './view-item';
import {NavController} from '../nav/nav-controller';
import {PaneController} from '../nav/pane';
import {Transition} from '../../transitions/transition';
import {ClickBlock} from '../../util/click-block';
import {SlideEdgeGesture} from 'ionic/gestures/slide-edge-gesture';
import {SwipeBackGesture} from './swipe-back';
import * as util from 'ionic/util';
/**
@ -39,9 +38,9 @@ export class ViewController extends Ion {
this.items = [];
this.panes = new PaneController(this);
this.sbTransition = null;
this.sbActive = false;
this.sbEnabled = true;
this._sbTrans = null;
this.sbEnabled = config.setting('swipeBackEnabled') || false;
this.sbThreshold = config.setting('swipeBackThreshold') || 40
this.id = ++ctrlIds;
this._ids = -1;
@ -62,7 +61,7 @@ export class ViewController extends Ion {
* @returns {Promise} TODO
*/
push(componentType, params = {}, opts = {}) {
if (!componentType || this.isTransitioning()) {
if (!componentType || !this._isEnabled()) {
return Promise.reject();
}
@ -110,7 +109,7 @@ export class ViewController extends Ion {
* @returns {Promise} TODO
*/
pop(opts = {}) {
if (this.isTransitioning() || this.items.length < 2) {
if (!this._isEnabled() || !this.canGoBack()) {
return Promise.reject();
}
@ -146,7 +145,7 @@ export class ViewController extends Ion {
});
} else {
this.transitionComplete();
this._transComplete();
resolve();
}
@ -279,8 +278,7 @@ export class ViewController extends Ion {
if (duration > 64) {
// block any clicks during the transition and provide a
// fallback to remove the clickblock if something goes wrong
ClickBlock(true, duration + 200);
this.app.setTransitioning(true, duration + 200);
this._setEnabled(false, duration);
}
// start the transition
@ -298,7 +296,7 @@ export class ViewController extends Ion {
// all done!
this.zone.run(() => {
this.transitionComplete();
this._transComplete();
callback();
});
});
@ -313,12 +311,12 @@ export class ViewController extends Ion {
* TODO
*/
swipeBackStart() {
if (this.isTransitioning() || this.items.length < 2) {
if (!this._isEnabled() || !this.canSwipeBack()) {
return;
}
this.sbActive = true;
this.sbResolve = null;
// disables the app during the transition
this._setEnabled(false);
// default the direction to "back"
let opts = {
@ -339,26 +337,62 @@ export class ViewController extends Ion {
enteringItem.shouldCache = false;
enteringItem.willEnter();
this.app.setTransitioning(true);
// wait for the new item to complete setup
enteringItem.stage(() => {
this.zone.runOutsideAngular(() => {
// set that the new item pushed on the stack is staged to be entering/leaving
// staged state is important for the transition to find the correct item
enteringItem.state = STAGED_ENTERING_STATE;
leavingItem.state = STAGED_LEAVING_STATE;
// init the transition animation
this.sbTransition = Transition.create(this, opts);
this.sbTransition.easing('linear').progressStart();
// init the swipe back transition animation
this._sbTrans = Transition.create(this, opts);
this._sbTrans.easing('linear').progressStart();
let swipeBackPromise = new Promise(res => { this.sbResolve = res; });
});
});
swipeBackPromise.then((completeSwipeBack) => {
}
/**
* TODO
* @param {TODO} progress TODO
*/
swipeBackProgress(value) {
if (this._sbTrans) {
// continue to disable the app while actively dragging
this._setEnabled(false, 4000);
// set the transition animation's progress
this._sbTrans.progress(value);
}
}
/**
* @private
* @param {TODO} completeSwipeBack Should the swipe back complete or not.
* @param {number} rate How fast it closes
*/
swipeBackFinish(completeSwipeBack, rate) {
if (!this._sbTrans) return;
// disables the app during the transition
this._setEnabled(false);
this._sbTrans.progressFinish(completeSwipeBack, rate).then(() => {
this.zone.run(() => {
// find the items that were entering and leaving
let enteringItem = this.getStagedEnteringItem();
let leavingItem = this.getStagedLeavingItem();
if (enteringItem && leavingItem) {
// finish up the animation
if (completeSwipeBack) {
// swipe back has completed, update each item's state
// swipe back has completed navigating back
// update each item's state
enteringItem.state = ACTIVE_STATE;
leavingItem.state = CACHED_STATE;
@ -366,12 +400,13 @@ export class ViewController extends Ion {
leavingItem.didLeave();
if (this.router) {
// notify router of the state change
// notify router of the pop state change
this.router.stateChange('pop', enteringItem);
}
} else {
// cancelled the swipe back, return items to original state
// cancelled the swipe back, they didn't end up going back
// return items to their original state
leavingItem.state = ACTIVE_STATE;
enteringItem.state = CACHED_STATE;
@ -382,45 +417,44 @@ export class ViewController extends Ion {
leavingItem.shouldDestroy = false;
enteringItem.shouldDestroy = false;
}
}
// empty out and dispose the swipe back transition animation
this._sbTrans && this._sbTrans.dispose();
this._sbTrans = null;
// all done!
this.transitionComplete();
this._transComplete();
});
});
}
/**
* TODO
* @param {TODO} progress TODO
*/
swipeBackProgress(progress) {
if (this.sbTransition) {
ClickBlock(true, 4000);
this.app.setTransitioning(true, 4000);
this.sbTransition.progress( Math.min(1, Math.max(0, progress)) );
}
_runSwipeBack() {
if (this.canSwipeBack()) {
// it is possible to swipe back
if (this.sbGesture) {
// this is already an active gesture, don't create another one
return;
}
/**
* TODO
* @param {TODO} completeSwipeBack TODO
* @param {TODO} progress TODO
* @param {TODO} playbackRate TODO
*/
swipeBackFinish(completeSwipeBack, playbackRate) {
// to reverse the animation use a negative playbackRate
if (this.sbTransition && this.sbActive) {
this.sbActive = false;
let opts = {
edge: 'left',
threshold: this.sbThreshold
};
this.sbGesture = new SwipeBackGesture(this.getNativeElement(), opts, this);
console.debug('SwipeBackGesture listen');
this.sbGesture.listen();
this.sbTransition.progressFinish(completeSwipeBack, playbackRate).then(() => {
this.sbResolve && this.sbResolve(completeSwipeBack);
this.sbTransition && this.sbTransition.dispose();
this.sbResolve = this.sbTransition = null;
this.app.setTransitioning(false);
});
} else if (this.sbGesture) {
// it is not possible to swipe back and there is an
// active sbGesture, so unlisten it
console.debug('SwipeBackGesture unlisten');
this.sbGesture.unlisten();
this.sbGesture = null;
}
}
@ -437,27 +471,33 @@ export class ViewController extends Ion {
}
/**
* TODO
* @returns {TODO} TODO
* If it's possible to use swipe back or not. If it's not possible
* to go back, or swipe back is not enable then this will return false.
* If it is possible to go back, and swipe back is enabled, then this
* will return true.
* @returns {boolean}
*/
canSwipeBack() {
if (this.sbEnabled) {
return (this.sbEnabled && this.canGoBack());
}
/**
* Returns `true` if there's a valid previous view that we can pop back to.
* Otherwise returns false.
* @returns {boolean}
*/
canGoBack() {
let activeItem = this.getActive();
if (activeItem) {
return activeItem.enableBack();
}
}
return false;
}
runSwipeBack() {
if (!this.canSwipeBack()) return;
}
/**
* TODO
* @private
*/
transitionComplete() {
_transComplete() {
let destroys = [];
this.items.forEach(item => {
@ -476,32 +516,35 @@ export class ViewController extends Ion {
item.destroy();
});
// allow clicks again
ClickBlock(false);
this.app.setTransitioning(false);
// allow clicks again, but still set an enable time
// meaning nothing with this view controller can happen for XXms
this._setEnabled(true);
if (this.items.length === 1) {
this.elementRef.nativeElement.classList.add('has-views');
}
this.runSwipeBack();
this._runSwipeBack();
}
/**
* TODO
* @returns {boolean} TODO
*/
isTransitioning() {
let state;
for (let i = 0, ii = this.items.length; i < ii; i++) {
state = this.items[i].state;
if (state === STAGED_ENTERING_STATE ||
state === STAGED_LEAVING_STATE) {
return true;
}
_setEnabled(isEnabled, fallback) {
// used to prevent unwanted transitions after JUST completing one
// prevents the user from crazy clicking everything and possible flickers
// gives the app some time to cool off, slow down, and think about life
this._enableTime = Date.now() + 100;
// IonicApp global setEnabled to prevent other things from starting up
this.app.setEnabled(isEnabled, fallback);
}
_isEnabled() {
// used to prevent unwanted transitions after JUST completing one
if (this._enableTime > Date.now()) {
return false;
}
// IonicApp global isEnabled, maybe something else has the app disabled
return this.app.isEnabled();
}
/**
* TODO

View File

@ -79,6 +79,11 @@ IonicPlatform.register({
},
keyboardHeight: 290,
hoverCSS: false,
swipeBackEnabled: function(p) {
return true; // TODO: remove me! Force it to always work for iOS mode for now
return /iphone|ipad|ipod/i.test(p.navigatorPlatform());
},
swipeBackThreshold: 40,
},
isMatch(p) {
return p.isPlatform('ios', 'iphone|ipad|ipod');

View File

@ -97,31 +97,6 @@ backdrop {
}
// Swipe Handle
// --------------------------------------------------
$swipe-handle-width: 20px !default;
$swipe-handle-top: 70px !default;
$swipe-handle-bottom: 70px !default;
.swipe-handle {
position: absolute;
top: $swipe-handle-top;
left: 0;
bottom: $swipe-handle-bottom;
width: $swipe-handle-width;
z-index: $z-index-swipe-handle;
//background: red;
//opacity: 0.2;
transform: translate3d(-999px, 0px, 0px);
&.show-handle {
transform: translate3d(0px, 0px, 0px);
}
}
// Loading Icon
// --------------------------------------------------