diff --git a/src/animations/animation.ts b/src/animations/animation.ts index acc21042196..8ac3b49ae10 100644 --- a/src/animations/animation.ts +++ b/src/animations/animation.ts @@ -861,9 +861,32 @@ export class Animation { * Start the animation with a user controlled progress. */ progressStart() { + // ensure all past transition end events have been cleared + this._clearAsync(); + + // fire off all the "before" function that have DOM READS in them + // elements will be in the DOM, however visibily hidden + // so we can read their dimensions if need be + // ******** DOM READ **************** + this._beforeReadFn(); + + // fire off all the "before" function that have DOM WRITES in them + // ******** DOM WRITE **************** + this._beforeWriteFn(); + + // ******** DOM WRITE **************** + this._progressStart(); + } + + /** + * @private + * DOM WRITE + * RECURSION + */ + _progressStart() { for (var i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** - this._c[i].progressStart(); + this._c[i]._progressStart(); } // ******** DOM WRITE **************** @@ -907,13 +930,14 @@ export class Animation { /** * End the progress animation. */ - progressEnd(shouldComplete: boolean, currentStepValue: number) { + progressEnd(shouldComplete: boolean, currentStepValue: number, maxDelta: number = 0) { console.debug('Animation, progressEnd, shouldComplete', shouldComplete, 'currentStepValue', currentStepValue); this._isAsync = (currentStepValue > 0.05 && currentStepValue < 0.95); - const dur = 64; const stepValue = shouldComplete ? 1 : 0; + const factor = Math.max(Math.abs(currentStepValue - stepValue), 0.5) * 2; + const dur = 64 + factor * maxDelta; this._progressEnd(shouldComplete, stepValue, dur, this._isAsync); @@ -922,7 +946,7 @@ export class Animation { // set the async TRANSITION END event // and run onFinishes when the transition ends // ******** DOM WRITE **************** - this._asyncEnd(dur, true); + this._asyncEnd(dur, shouldComplete); // this animation has a duration so we need another RAF // for the CSS TRANSITION properties to kick in diff --git a/src/components/app/app.ts b/src/components/app/app.ts index 8a621e5eba2..4719271b7f8 100644 --- a/src/components/app/app.ts +++ b/src/components/app/app.ts @@ -71,7 +71,7 @@ export class App { // listen for hardware back button events // register this back button action with a default priority _platform.registerBackButtonAction(this.navPop.bind(this)); - this._canDisableScroll = _config.get('canDisableScroll', true); + this._canDisableScroll = _config.get('canDisableScroll', false); } /** diff --git a/src/components/app/test/gesture-collision/page1.html b/src/components/app/test/gesture-collision/page1.html index 0b2df6e34ec..9d8061d33ad 100644 --- a/src/components/app/test/gesture-collision/page1.html +++ b/src/components/app/test/gesture-collision/page1.html @@ -10,7 +10,7 @@ Menu - diff --git a/src/components/item/item-sliding.ts b/src/components/item/item-sliding.ts index e6495dd27ae..50d0b0130b8 100644 --- a/src/components/item/item-sliding.ts +++ b/src/components/item/item-sliding.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, ContentChildren, ContentChild, Dire import { CSS, nativeRaf, nativeTimeout, clearNativeTimeout } from '../../util/dom'; import { Item } from './item'; -import { isPresent, assert } from '../../util/util'; +import { isPresent, swipeShouldReset, assert } from '../../util/util'; import { List } from '../list/list'; const SWIPE_MARGIN = 30; @@ -320,10 +320,10 @@ export class ItemSliding { // Check if the drag didn't clear the buttons mid-point // and we aren't moving fast enough to swipe open - let isCloseDirection = (this._openAmount > 0) === !(velocity < 0); + let isResetDirection = (this._openAmount > 0) === !(velocity < 0); let isMovingFast = Math.abs(velocity) > 0.3; let isOnCloseZone = Math.abs(this._openAmount) < Math.abs(restingPoint / 2); - if (shouldClose(isCloseDirection, isMovingFast, isOnCloseZone)) { + if (swipeShouldReset(isResetDirection, isMovingFast, isOnCloseZone)) { restingPoint = 0; } @@ -463,22 +463,3 @@ export class ItemSliding { this._renderer.setElementClass(this._elementRef.nativeElement, cssClass, shouldAdd); } } - -function shouldClose(isCloseDirection: boolean, isMovingFast: boolean, isOnCloseZone: boolean): boolean { - // The logic required to know when the sliding item should close (openAmount=0) - // depends on three booleans (isCloseDirection, isMovingFast, isOnCloseZone) - // and it ended up being too complicated to be written manually without errors - // so the truth table is attached below: (0=false, 1=true) - // isCloseDirection | isMovingFast | isOnCloseZone || shouldClose - // 0 | 0 | 0 || 0 - // 0 | 0 | 1 || 1 - // 0 | 1 | 0 || 0 - // 0 | 1 | 1 || 0 - // 1 | 0 | 0 || 0 - // 1 | 0 | 1 || 1 - // 1 | 1 | 0 || 1 - // 1 | 1 | 1 || 1 - // The resulting expression was generated by resolving the K-map (Karnaugh map): - let shouldClose = (!isMovingFast && isOnCloseZone) || (isCloseDirection && isMovingFast); - return shouldClose; -} diff --git a/src/components/menu/menu-controller.ts b/src/components/menu/menu-controller.ts index a3388b6821b..5342d819494 100644 --- a/src/components/menu/menu-controller.ts +++ b/src/components/menu/menu-controller.ts @@ -132,7 +132,6 @@ export class MenuController { } return menu.open(); } - return Promise.resolve(false); } diff --git a/src/components/menu/menu-gestures.ts b/src/components/menu/menu-gestures.ts index ee5b8b1775c..257487770eb 100644 --- a/src/components/menu/menu-gestures.ts +++ b/src/components/menu/menu-gestures.ts @@ -3,6 +3,7 @@ import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture'; import { SlideData } from '../../gestures/slide-gesture'; import { assign } from '../../util/util'; import { GestureController, GesturePriority } from '../../gestures/gesture-controller'; +import { NativeRafDebouncer } from '../../util/debouncer'; /** * Gesture attached to the content which the menu is assigned to @@ -11,8 +12,8 @@ export class MenuContentGesture extends SlideEdgeGesture { constructor( public menu: Menu, - gestureCtrl: GestureController, contentEle: HTMLElement, + gestureCtrl: GestureController, options: any = {}) { super(contentEle, assign({ direction: 'x', @@ -20,6 +21,7 @@ export class MenuContentGesture extends SlideEdgeGesture { threshold: 0, maxEdgeStart: menu.maxEdgeStart || 50, maxAngle: 40, + debouncer: new NativeRafDebouncer(), gesture: gestureCtrl.create('menu-swipe', { priority: GesturePriority.MenuSwipe, }) diff --git a/src/components/menu/menu.ts b/src/components/menu/menu.ts index 2c620f6b2d3..6ba179ff9d1 100644 --- a/src/components/menu/menu.ts +++ b/src/components/menu/menu.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, EventEmit import { Backdrop } from '../backdrop/backdrop'; import { Config } from '../../config/config'; -import { isTrueProperty } from '../../util/util'; +import { isTrueProperty, assert } from '../../util/util'; import { Keyboard } from '../../util/keyboard'; import { MenuContentGesture } from './menu-gestures'; import { MenuController } from './menu-controller'; @@ -199,6 +199,7 @@ export class Menu { private _isPers: boolean = false; private _init: boolean = false; private _events: UIEventManager = new UIEventManager(); + private _gestureID: number = 0; /** * @private @@ -304,7 +305,11 @@ export class Menu { private _keyboard: Keyboard, private _zone: NgZone, private _gestureCtrl: GestureController - ) {} + ) { + if (_gestureCtrl) { + this._gestureID = _gestureCtrl.newID(); + } + } /** * @private @@ -333,7 +338,7 @@ export class Menu { this.setElementAttribute('type', this.type); // add the gestures - this._cntGesture = new MenuContentGesture(this, this._gestureCtrl, document.body); + this._cntGesture = new MenuContentGesture(this, document.body, this._gestureCtrl); // register listeners if this menu is enabled // check if more than one menu is on the same side @@ -472,6 +477,8 @@ export class Menu { } private _before() { + assert(!this._isAnimating, '_before() should not be called while animating'); + // this places the menu into the correct location before it animates in // this css class doesn't actually kick off any animations this.menuContent && this.menuContent.resize(); @@ -482,6 +489,7 @@ export class Menu { } private _after(isOpen: boolean) { + assert(this._isAnimating, '_before() should be called while animating'); // keep opening/closing the menu disabled for a touch more yet // only add listeners/css if it's enabled and isOpen // and only remove listeners/css if it's not open @@ -491,8 +499,10 @@ export class Menu { this._events.unlistenAll(); if (isOpen) { - this._cntEle.classList.add('menu-content-open'); + // Disable swipe to go back gesture + this._gestureCtrl.disableGesture('goback-swipe', this._gestureID); + this._cntEle.classList.add('menu-content-open'); let callback = this.onBackdropClick.bind(this); this._events.pointerEvents({ element: this._cntEle, @@ -505,6 +515,9 @@ export class Menu { this.ionOpen.emit(true); } else { + // Enable swipe to go back gesture + this._gestureCtrl.enableGesture('goback-swipe', this._gestureID); + this._cntEle.classList.remove('menu-content-open'); this.setElementClass('show-menu', false); this.backdrop.setElementClass('show-menu', false); diff --git a/src/components/modal/test/basic/app-module.ts b/src/components/modal/test/basic/app-module.ts index ebc874a2106..0423169363b 100644 --- a/src/components/modal/test/basic/app-module.ts +++ b/src/components/modal/test/basic/app-module.ts @@ -147,13 +147,15 @@ export class E2EPage { -

ionViewCanEnter ({{called.ionViewCanEnter}})

-

ionViewCanLeave ({{called.ionViewCanLeave}})

-

ionViewDidLoad ({{called.ionViewDidLoad}})

-

ionViewWillEnter ({{called.ionViewWillEnter}})

-

ionViewDidEnter ({{called.ionViewDidEnter}})

-

ionViewWillLeave ({{called.ionViewWillLeave}})

-

ionViewDidLeave ({{called.ionViewDidLeave}})

+
+

ionViewCanEnter ({{called.ionViewCanEnter}})

+

ionViewCanLeave ({{called.ionViewCanLeave}})

+

ionViewDidLoad ({{called.ionViewDidLoad}})

+

ionViewWillEnter ({{called.ionViewWillEnter}})

+

ionViewDidEnter ({{called.ionViewDidEnter}})

+

ionViewWillLeave ({{called.ionViewWillLeave}})

+

ionViewDidLeave ({{called.ionViewDidLeave}})

+
`, providers: [SomeComponentProvider] @@ -505,10 +507,12 @@ export class ModalFirstPage { } ionViewWillLeave() { + console.log('ModalFirstPage ionViewWillLeave fired'); this.called.ionViewWillLeave++; } ionViewDidLeave() { + console.log('ModalFirstPage ionViewDidLeave fired'); this.called.ionViewDidLeave++; } @@ -612,7 +616,8 @@ export class E2EApp { ], imports: [ IonicModule.forRoot(E2EApp, { - statusbarPadding: true + statusbarPadding: true, + swipeBackEnabled: true }) ], bootstrap: [IonicApp], diff --git a/src/components/range/range.ts b/src/components/range/range.ts index 65a471c2163..0b013ec3a61 100644 --- a/src/components/range/range.ts +++ b/src/components/range/range.ts @@ -3,7 +3,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { clamp, isNumber, isPresent, isString, isTrueProperty } from '../../util/util'; import { Config } from '../../config/config'; -import { Debouncer } from '../../util/debouncer'; +import { TimeoutDebouncer } from '../../util/debouncer'; import { Form } from '../../util/form'; import { Ion } from '../ion'; import { Item } from '../item/item'; @@ -217,7 +217,7 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O _step: number = 1; _snaps: boolean = false; - _debouncer: Debouncer = new Debouncer(0); + _debouncer: TimeoutDebouncer = new TimeoutDebouncer(0); _events: UIEventManager = new UIEventManager(); /** * @private diff --git a/src/components/searchbar/searchbar.ts b/src/components/searchbar/searchbar.ts index 2edecae041a..bb738144f56 100644 --- a/src/components/searchbar/searchbar.ts +++ b/src/components/searchbar/searchbar.ts @@ -4,7 +4,7 @@ import { NgControl } from '@angular/forms'; import { Config } from '../../config/config'; import { Ion } from '../ion'; import { isPresent, isTrueProperty } from '../../util/util'; -import { Debouncer } from '../../util/debouncer'; +import { TimeoutDebouncer } from '../../util/debouncer'; /** @@ -61,7 +61,7 @@ export class Searchbar extends Ion { _autocomplete: string = 'off'; _autocorrect: string = 'off'; _isActive: boolean = false; - _debouncer: Debouncer = new Debouncer(250); + _debouncer: TimeoutDebouncer = new TimeoutDebouncer(250); /** * @input {string} The predefined color to use. For example: `"primary"`, `"secondary"`, `"danger"`. diff --git a/src/components/tabs/test/advanced/app-module.ts b/src/components/tabs/test/advanced/app-module.ts index dc3cece680d..99d0805b27c 100644 --- a/src/components/tabs/test/advanced/app-module.ts +++ b/src/components/tabs/test/advanced/app-module.ts @@ -134,6 +134,12 @@ export class Tab1Page1 { templateUrl: './tab1page2.html' }) export class Tab1Page2 { + constructor(public tabs: Tabs) { } + + favoritesTab() { + // TODO fix this with tabsHideOnSubPages=true + this.tabs.select(1); + } ionViewWillEnter() { console.log('Tab1Page2, ionViewWillEnter'); @@ -346,7 +352,7 @@ export const deepLinkConfig: DeepLinkConfig = { Tab3Page1 ], imports: [ - IonicModule.forRoot(E2EApp, null, deepLinkConfig) + IonicModule.forRoot(E2EApp, {tabsHideOnSubPages: true}, deepLinkConfig) ], bootstrap: [IonicApp], entryComponents: [ diff --git a/src/components/tabs/test/advanced/tab1page2.html b/src/components/tabs/test/advanced/tab1page2.html index fa5afc8f697..d6e68bc76d4 100644 --- a/src/components/tabs/test/advanced/tab1page2.html +++ b/src/components/tabs/test/advanced/tab1page2.html @@ -10,6 +10,7 @@

+

diff --git a/src/gestures/drag-gesture.ts b/src/gestures/drag-gesture.ts index 3aa6d4369ab..1d666d6d6f7 100644 --- a/src/gestures/drag-gesture.ts +++ b/src/gestures/drag-gesture.ts @@ -1,8 +1,9 @@ import { defaults } from '../util/util'; import { GestureDelegate } from '../gestures/gesture-controller'; import { PanRecognizer } from './recognizers'; -import { PointerEvents, UIEventManager } from '../util/ui-event-manager'; +import { PointerEvents, PointerEventsConfig, UIEventManager } from '../util/ui-event-manager'; import { pointerCoord } from '../util/dom'; +import { Debouncer, FakeDebouncer } from '../util/debouncer'; /** * @private @@ -12,12 +13,16 @@ export interface PanGestureConfig { maxAngle?: number; direction?: 'x' | 'y'; gesture?: GestureDelegate; + debouncer?: Debouncer; + zone?: boolean; + capture?: boolean; } /** * @private */ export class PanGesture { + private debouncer: Debouncer; private events: UIEventManager = new UIEventManager(false); private pointerEvents: PointerEvents; private detector: PanRecognizer; @@ -26,31 +31,45 @@ export class PanGesture { public isListening: boolean = false; protected gestute: GestureDelegate; protected direction: string; + private eventsConfig: PointerEventsConfig; constructor(private element: HTMLElement, opts: PanGestureConfig = {}) { defaults(opts, { threshold: 20, maxAngle: 40, - direction: 'x' + direction: 'x', + zone: true, + capture: false, }); + + this.debouncer = (opts.debouncer) + ? opts.debouncer + : new FakeDebouncer(); this.gestute = opts.gesture; this.direction = opts.direction; + this.eventsConfig = { + element: this.element, + pointerDown: this.pointerDown.bind(this), + pointerMove: this.pointerMove.bind(this), + pointerUp: this.pointerUp.bind(this), + zone: opts.zone, + capture: opts.capture + }; this.detector = new PanRecognizer(opts.direction, opts.threshold, opts.maxAngle); } listen() { - if (!this.isListening) { - this.pointerEvents = this.events.pointerEvents({ - element: this.element, - pointerDown: this.pointerDown.bind(this), - pointerMove: this.pointerMove.bind(this), - pointerUp: this.pointerUp.bind(this), - }); - this.isListening = true; + if (this.isListening) { + return; } + this.pointerEvents = this.events.pointerEvents(this.eventsConfig); + this.isListening = true; } unlisten() { + if (!this.isListening) { + return; + } this.gestute && this.gestute.release(); this.events.unlistenAll(); this.isListening = false; @@ -58,6 +77,7 @@ export class PanGesture { destroy() { this.gestute && this.gestute.destroy(); + this.gestute = null; this.unlisten(); this.element = null; } @@ -86,32 +106,36 @@ export class PanGesture { } pointerMove(ev: any) { - if (!this.started) { - return; - } - if (this.captured) { - this.onDragMove(ev); - return; - } - let coord = pointerCoord(ev); - if (this.detector.detect(coord)) { - - if (this.detector.pan() !== 0 && this.canCapture(ev) && - (!this.gestute || this.gestute.capture())) { - this.onDragStart(ev); - this.captured = true; + this.debouncer.debounce(() => { + if (!this.started) { return; } + if (this.captured) { + this.onDragMove(ev); + return; + } + let coord = pointerCoord(ev); + if (this.detector.detect(coord)) { - // Detection/capturing was not successful, aborting! - this.started = false; - this.captured = false; - this.pointerEvents.stop(); - this.notCaptured(ev); - } + if (this.detector.pan() !== 0 && this.canCapture(ev) && + (!this.gestute || this.gestute.capture())) { + this.onDragStart(ev); + this.captured = true; + return; + } + + // Detection/capturing was not successful, aborting! + this.started = false; + this.captured = false; + this.pointerEvents.stop(); + this.notCaptured(ev); + } + }); } pointerUp(ev: any) { + this.debouncer.cancel(); + if (!this.started) { return; } diff --git a/src/navigation/nav-controller-base.ts b/src/navigation/nav-controller-base.ts index d72ac34597c..e70b6911f15 100644 --- a/src/navigation/nav-controller-base.ts +++ b/src/navigation/nav-controller-base.ts @@ -178,10 +178,10 @@ export class NavControllerBase extends Ion implements NavController { // transition has successfully resolved this._trnsId = null; resolve && resolve(hasCompleted, isAsync, enteringName, leavingName, direction); - this._sbCheck(); // let's see if there's another to kick off this.setTransitioning(false); + this._sbCheck(); this._nextTrns(); }; @@ -204,11 +204,10 @@ export class NavControllerBase extends Ion implements NavController { this._trnsCtrl.destroy(trns.trnsId); } - this._sbCheck(); - reject && reject(false, false, rejectReason); this.setTransitioning(false); + this._sbCheck(); this._nextTrns(); }; @@ -371,11 +370,14 @@ export class NavControllerBase extends Ion implements NavController { // and there is not a view that needs to visually transition out // then just destroy them and don't transition anything // batch all of lifecycles together - for (view of destroyQueue) { - this._willLeave(view); - this._didLeave(view); - this._willUnload(view); - } + // let's make sure, callbacks are zoned + this._zone.run(() => { + for (view of destroyQueue) { + this._willLeave(view); + this._didLeave(view); + this._willUnload(view); + } + }); // once all lifecycle events has been delivered, we can safely detroy the views for (view of destroyQueue) { @@ -443,7 +445,7 @@ export class NavControllerBase extends Ion implements NavController { // successfully finished loading the entering view // fire off the "didLoad" lifecycle events - this._didLoad(view); + this._zone.run(this._didLoad.bind(this, view)); componentRef.changeDetectorRef.detectChanges(); } @@ -652,6 +654,10 @@ export class NavControllerBase extends Ion implements NavController { } this._cleanup(transition.enteringView); + } else { + // If transition does not complete, we have to cleanup anyway, because + // previous pages in the stack are not hidden probably. + this._cleanup(transition.leavingView); } if (transition.isRoot()) { @@ -755,12 +761,14 @@ export class NavControllerBase extends Ion implements NavController { _willLoad(view: ViewController) { assert(this.isTransitioning(), 'nav controller should be transitioning'); + assert(NgZone.isInAngularZone(), 'callback should be zoned'); view._willLoad(); } _didLoad(view: ViewController) { assert(this.isTransitioning(), 'nav controller should be transitioning'); + assert(NgZone.isInAngularZone(), 'callback should be zoned'); view._didLoad(); this.viewDidLoad.emit(view); @@ -769,6 +777,7 @@ export class NavControllerBase extends Ion implements NavController { _willEnter(view: ViewController) { assert(this.isTransitioning(), 'nav controller should be transitioning'); + assert(NgZone.isInAngularZone(), 'callback should be zoned'); view._willEnter(); this.viewWillEnter.emit(view); @@ -777,6 +786,7 @@ export class NavControllerBase extends Ion implements NavController { _didEnter(view: ViewController) { assert(this.isTransitioning(), 'nav controller should be transitioning'); + assert(NgZone.isInAngularZone(), 'callback should be zoned'); view._didEnter(); this.viewDidEnter.emit(view); @@ -785,6 +795,7 @@ export class NavControllerBase extends Ion implements NavController { _willLeave(view: ViewController) { assert(this.isTransitioning(), 'nav controller should be transitioning'); + assert(NgZone.isInAngularZone(), 'callback should be zoned'); view._willLeave(); this.viewWillLeave.emit(view); @@ -793,6 +804,7 @@ export class NavControllerBase extends Ion implements NavController { _didLeave(view: ViewController) { assert(this.isTransitioning(), 'nav controller should be transitioning'); + assert(NgZone.isInAngularZone(), 'callback should be zoned'); view._didLeave(); this.viewDidLeave.emit(view); @@ -801,6 +813,7 @@ export class NavControllerBase extends Ion implements NavController { _willUnload(view: ViewController) { assert(this.isTransitioning(), 'nav controller should be transitioning'); + assert(NgZone.isInAngularZone(), 'callback should be zoned'); view._willUnload(); this.viewWillUnload.emit(view); @@ -824,18 +837,19 @@ export class NavControllerBase extends Ion implements NavController { } destroy() { - let view; - for (view of this._views) { + for (var view of this._views) { view._willUnload(); view._destroy(this._renderer); } // purge stack this._views.length = 0; + // release swipe back gesture and transition this._sbGesture && this._sbGesture.destroy(); this._sbTrns && this._sbTrns.destroy(); this._sbGesture = this._sbTrns = null; + // Unregister navcontroller if (this.parent && this.parent.unregisterChildNav) { this.parent.unregisterChildNav(this); } @@ -857,7 +871,6 @@ export class NavControllerBase extends Ion implements NavController { removeCount: 1, opts: opts, }, null); - } swipeBackProgress(stepValue: number) { @@ -874,39 +887,29 @@ export class NavControllerBase extends Ion implements NavController { swipeBackEnd(shouldComplete: boolean, currentStepValue: number) { if (this._sbTrns && this._sbGesture) { // the swipe back gesture has ended - this._sbTrns.progressEnd(shouldComplete, currentStepValue); + this._sbTrns.progressEnd(shouldComplete, currentStepValue, 300); } } _sbCheck() { - if (this._sbEnabled && !this._isPortal) { - // this nav controller can have swipe to go back - - if (!this._sbGesture) { - // create the swipe back gesture if we haven't already - const opts = { - edge: 'left', - threshold: this._sbThreshold - }; - this._sbGesture = new SwipeBackGesture(this.getNativeElement(), opts, this, this._gestureCtrl); - } + if (!this._sbEnabled && this._isPortal) { + return; + } - if (this.canSwipeBack()) { - // it is be possible to swipe back - if (!this._sbGesture.isListening) { - this._zone.runOutsideAngular(() => { - // start listening if it's not already - console.debug('swipeBack gesture, listen'); - this._sbGesture.listen(); - }); - } + // this nav controller can have swipe to go back + if (!this._sbGesture) { + // create the swipe back gesture if we haven't already + const opts = { + edge: 'left', + threshold: this._sbThreshold + }; + this._sbGesture = new SwipeBackGesture(this, document.body, this._gestureCtrl, opts); + } - } else if (this._sbGesture.isListening) { - // it should not be possible to swipe back - // but the gesture is still listening - console.debug('swipeBack gesture, unlisten'); - this._sbGesture.unlisten(); - } + if (this.canSwipeBack()) { + this._sbGesture.listen(); + } else { + this._sbGesture.unlisten(); } } diff --git a/src/navigation/swipe-back.ts b/src/navigation/swipe-back.ts index 9fb989dee6a..d511035d083 100644 --- a/src/navigation/swipe-back.ts +++ b/src/navigation/swipe-back.ts @@ -1,23 +1,27 @@ -import { assign } from '../util/util'; -import { GestureController, GesturePriority } from '../gestures/gesture-controller'; +import { assign, swipeShouldReset } from '../util/util'; +import { GestureController, GesturePriority, DisableScroll } from '../gestures/gesture-controller'; import { NavControllerBase } from './nav-controller-base'; import { SlideData } from '../gestures/slide-gesture'; import { SlideEdgeGesture } from '../gestures/slide-edge-gesture'; - +import { NativeRafDebouncer } from '../util/debouncer'; export class SwipeBackGesture extends SlideEdgeGesture { - constructor( + private _nav: NavControllerBase, element: HTMLElement, + gestureCtlr: GestureController, options: any, - private _nav: NavControllerBase, - gestureCtlr: GestureController ) { super(element, assign({ direction: 'x', maxEdgeStart: 75, + zone: false, + threshold: 0, + maxAngle: 40, + debouncer: new NativeRafDebouncer(), gesture: gestureCtlr.create('goback-swipe', { priority: GesturePriority.GoBackSwipe, + disableScroll: DisableScroll.DuringCapture }) }, options)); } @@ -32,23 +36,25 @@ export class SwipeBackGesture extends SlideEdgeGesture { ); } - onSlideBeforeStart(ev: any) { - console.debug('swipeBack, onSlideBeforeStart', ev.type); this._nav.swipeBackStart(); } - onSlide(slide: SlideData) { + onSlide(slide: SlideData, ev: any) { + ev.preventDefault(); + ev.stopPropagation(); + let stepValue = (slide.distance / slide.max); - console.debug('swipeBack, onSlide, distance', slide.distance, 'max', slide.max, 'stepValue', stepValue); this._nav.swipeBackProgress(stepValue); } onSlideEnd(slide: SlideData, ev: any) { - let shouldComplete = (Math.abs(slide.velocity) > 0.2 || Math.abs(slide.delta) > Math.abs(slide.max) * 0.5); - let currentStepValue = (slide.distance / slide.max); + const currentStepValue = (slide.distance / slide.max); + const isResetDirecction = slide.velocity < 0; + const isMovingFast = Math.abs(slide.velocity) > 0.4; + const isInResetZone = Math.abs(slide.delta) < Math.abs(slide.max) * 0.5; + const shouldComplete = !swipeShouldReset(isResetDirecction, isMovingFast, isInResetZone); - console.debug('swipeBack, onSlideEnd, shouldComplete', shouldComplete, 'currentStepValue', currentStepValue); this._nav.swipeBackEnd(shouldComplete, currentStepValue); } } diff --git a/src/platform/platform-registry.ts b/src/platform/platform-registry.ts index 809100d347d..f87edad9bb5 100644 --- a/src/platform/platform-registry.ts +++ b/src/platform/platform-registry.ts @@ -110,7 +110,6 @@ export const PLATFORM_CONFIGS: {[key: string]: PlatformConfig} = { swipeBackThreshold: 40, tapPolyfill: isIOSDevice, virtualScrollEventAssist: !(window.indexedDB), - canDisableScroll: isIOSDevice, }, isMatch(p: Platform) { return p.isPlatformMatch('ios', ['iphone', 'ipad', 'ipod'], ['windows phone']); diff --git a/src/transitions/transition-md.ts b/src/transitions/transition-md.ts index 52deeb61b9a..89d5b57799a 100644 --- a/src/transitions/transition-md.ts +++ b/src/transitions/transition-md.ts @@ -53,7 +53,7 @@ export class MDTransition extends PageTransition { // leaving content this.duration(opts.duration || 200).easing('cubic-bezier(0.47,0,0.745,0.715)'); const leavingPage = new Animation(leavingView.pageRef()); - this.add(leavingPage.fromTo(TRANSLATEY, CENTER, OFF_BOTTOM).fromTo('opacity', 0.99, 0)); + this.add(leavingPage.fromTo(TRANSLATEY, CENTER, OFF_BOTTOM).fromTo('opacity', 1, 0)); } } diff --git a/src/util/debouncer.ts b/src/util/debouncer.ts index 327de2eeddf..de3932a9386 100644 --- a/src/util/debouncer.ts +++ b/src/util/debouncer.ts @@ -1,5 +1,20 @@ -export class Debouncer { +import { nativeRaf } from './dom'; + +export interface Debouncer { + debounce(Function); + cancel(); +} + + +export class FakeDebouncer implements Debouncer { + debounce(callback: Function) { + callback(); + } + cancel() {} +} + +export class TimeoutDebouncer implements Debouncer { private timer: number = null; callback: Function; @@ -11,10 +26,7 @@ export class Debouncer { } schedule() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } + this.cancel(); if (this.wait <= 0) { this.callback(); } else { @@ -22,4 +34,44 @@ export class Debouncer { } } + cancel() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + +} + +export class NativeRafDebouncer implements Debouncer { + callback: Function = null; + fireFunc: Function; + ptr: number = null; + + constructor() { + this.fireFunc = this.fire.bind(this); + } + + debounce(callback: Function) { + if (this.callback === null) { + this.callback = callback; + this.ptr = nativeRaf(this.fireFunc); + } + } + + fire() { + this.callback(); + this.callback = null; + this.ptr = null; + } + + cancel() { + if (this.ptr !== null) { + cancelAnimationFrame(this.ptr); + this.ptr = null; + this.callback = null; + } + } + } + diff --git a/src/util/mock-providers.ts b/src/util/mock-providers.ts index f845b3c6698..43578b91086 100644 --- a/src/util/mock-providers.ts +++ b/src/util/mock-providers.ts @@ -72,15 +72,7 @@ export const mockTrasitionController = function(config: Config) { }; export const mockZone = function(): NgZone { - let zone: any = { - run: function(cb: any) { - cb(); - }, - runOutsideAngular: function(cb: any) { - cb(); - } - }; - return zone; + return new NgZone(false); }; export const mockChangeDetectorRef = function(): ChangeDetectorRef { diff --git a/src/util/util.ts b/src/util/util.ts index 2eda92d876f..2546e7cb3cb 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -156,6 +156,28 @@ export function reorderArray(array: any[], indexes: {from: number, to: number}): return array; } +/** + * @private + */ +export function swipeShouldReset(isResetDirection: boolean, isMovingFast: boolean, isOnResetZone: boolean): boolean { + // The logic required to know when the sliding item should close (openAmount=0) + // depends on three booleans (isCloseDirection, isMovingFast, isOnCloseZone) + // and it ended up being too complicated to be written manually without errors + // so the truth table is attached below: (0=false, 1=true) + // isCloseDirection | isMovingFast | isOnCloseZone || shouldClose + // 0 | 0 | 0 || 0 + // 0 | 0 | 1 || 1 + // 0 | 1 | 0 || 0 + // 0 | 1 | 1 || 0 + // 1 | 0 | 0 || 0 + // 1 | 0 | 1 || 1 + // 1 | 1 | 0 || 1 + // 1 | 1 | 1 || 1 + // The resulting expression was generated by resolving the K-map (Karnaugh map): + let shouldClose = (!isMovingFast && isOnResetZone) || (isResetDirection && isMovingFast); + return shouldClose; +} + const ASSERT_ENABLED = true; /**