Skip to content

Commit

Permalink
fix(nav): swipe to go back gesture
Browse files Browse the repository at this point in the history
- smoother by debouncing touch events (reduces bank)
- dynamic animation duration
- intelligent behavior based in the position, speed and direccion of the swipe (sharing logic with sliding item)

fixes ionic-team#8919
fixes ionic-team#8958
fixes ionic-team#7934
  • Loading branch information
manucorporat committed Oct 31, 2016
1 parent 855f137 commit f65d838
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 93 deletions.
32 changes: 28 additions & 4 deletions src/animations/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ****************
Expand Down Expand Up @@ -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);

Expand All @@ -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
Expand Down
25 changes: 3 additions & 22 deletions src/components/item/item-sliding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
1 change: 0 additions & 1 deletion src/components/menu/menu-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ export class MenuController {
}
return menu.open();
}

return Promise.resolve(false);
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/menu/menu-gestures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export class MenuContentGesture extends SlideEdgeGesture {

constructor(
public menu: Menu,
gestureCtrl: GestureController,
contentEle: HTMLElement,
gestureCtrl: GestureController,
options: any = {}) {
super(contentEle, assign({
direction: 'x',
Expand Down
19 changes: 15 additions & 4 deletions src/components/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -199,6 +199,7 @@ export class Menu {
private _isPers: boolean = false;
private _init: boolean = false;
private _events: UIEventManager = new UIEventManager();
private _gestureID: number;

/**
* @private
Expand Down Expand Up @@ -304,7 +305,9 @@ export class Menu {
private _keyboard: Keyboard,
private _zone: NgZone,
private _gestureCtrl: GestureController
) {}
) {
this._gestureID = _gestureCtrl.newID();
}

/**
* @private
Expand Down Expand Up @@ -333,7 +336,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
Expand Down Expand Up @@ -472,6 +475,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();
Expand All @@ -482,6 +487,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
Expand All @@ -491,8 +497,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,
Expand All @@ -505,6 +513,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);
Expand Down
21 changes: 13 additions & 8 deletions src/components/modal/test/basic/app-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,15 @@ export class E2EPage {
</ion-item>
</ion-list>
<button ion-button full (click)="submit()">Submit</button>
<p>ionViewCanEnter ({{called.ionViewCanEnter}})</p>
<p>ionViewCanLeave ({{called.ionViewCanLeave}})</p>
<p>ionViewDidLoad ({{called.ionViewDidLoad}})</p>
<p>ionViewWillEnter ({{called.ionViewWillEnter}})</p>
<p>ionViewDidEnter ({{called.ionViewDidEnter}})</p>
<p>ionViewWillLeave ({{called.ionViewWillLeave}})</p>
<p>ionViewDidLeave ({{called.ionViewDidLeave}})</p>
<div padding>
<p>ionViewCanEnter ({{called.ionViewCanEnter}})</p>
<p>ionViewCanLeave ({{called.ionViewCanLeave}})</p>
<p>ionViewDidLoad ({{called.ionViewDidLoad}})</p>
<p>ionViewWillEnter ({{called.ionViewWillEnter}})</p>
<p>ionViewDidEnter ({{called.ionViewDidEnter}})</p>
<p>ionViewWillLeave ({{called.ionViewWillLeave}})</p>
<p>ionViewDidLeave ({{called.ionViewDidLeave}})</p>
</div>
</ion-content>
`,
providers: [SomeComponentProvider]
Expand Down Expand Up @@ -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++;
}

Expand Down Expand Up @@ -612,7 +616,8 @@ export class E2EApp {
],
imports: [
IonicModule.forRoot(E2EApp, {
statusbarPadding: true
statusbarPadding: true,
swipeBackEnabled: true
})
],
bootstrap: [IonicApp],
Expand Down
2 changes: 1 addition & 1 deletion src/components/tabs/test/advanced/app-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ export const deepLinkConfig: DeepLinkConfig = {
Tab3Page1
],
imports: [
IonicModule.forRoot(E2EApp, null, deepLinkConfig)
IonicModule.forRoot(E2EApp, {tabsHideOnSubPages: true}, deepLinkConfig)
],
bootstrap: [IonicApp],
entryComponents: [
Expand Down
34 changes: 24 additions & 10 deletions src/gestures/drag-gesture.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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';

/**
Expand All @@ -12,6 +12,8 @@ export interface PanGestureConfig {
maxAngle?: number;
direction?: 'x' | 'y';
gesture?: GestureDelegate;
zone?: boolean;
capture?: boolean;
}

/**
Expand All @@ -26,38 +28,50 @@ 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.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;
}

destroy() {
this.gestute && this.gestute.destroy();
this.gestute = null;
this.unlisten();
this.element = null;
}
Expand Down
Loading

0 comments on commit f65d838

Please sign in to comment.