Skip to content

Commit

Permalink
fix(sliding): much better UX + performance
Browse files Browse the repository at this point in the history
- sliding should behave exactly like a native one
- much better performance

references #7049
references #7116
closes #6913
closes #6958
  • Loading branch information
manucorporat committed Jul 3, 2016
1 parent bc5a945 commit e1a97b2
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 107 deletions.
10 changes: 6 additions & 4 deletions src/components/item/item-reorder-gesture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ export class ItemReorderGesture {

constructor(public list: ItemReorder) {
let element = this.list.getNativeElement();
this.events.pointerEvents(element,
this.onDragStart.bind(this),
this.onDragMove.bind(this),
this.onDragEnd.bind(this));
this.events.pointerEvents({
element: element,
pointerDown: this.onDragStart.bind(this),
pointerMove: this.onDragMove.bind(this),
pointerUp: this.onDragEnd.bind(this)
});
}

private onDragStart(ev: any): boolean {
Expand Down
205 changes: 139 additions & 66 deletions src/components/item/item-sliding-gesture.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,105 @@
import {DragGesture} from '../../gestures/drag-gesture';
import {ItemSliding} from './item-sliding';
import {List} from '../list/list';
import { ItemSliding } from './item-sliding';
import { List } from '../list/list';

import {closest} from '../../util/dom';
import { closest, Coordinates, pointerCoord } from '../../util/dom';
import { PointerEvents, UIEventManager } from '../../util/ui-event-manager';

const DRAG_THRESHOLD = 20;
const DRAG_THRESHOLD = 10;
const MAX_ATTACK_ANGLE = 20;

export class ItemSlidingGesture extends DragGesture {
onTap: any;
selectedContainer: ItemSliding = null;
openContainer: ItemSliding = null;
export class ItemSlidingGesture {
private preSelectedContainer: ItemSliding = null;
private selectedContainer: ItemSliding = null;
private openContainer: ItemSliding = null;
private events: UIEventManager = new UIEventManager(false);
private panDetector: PanXRecognizer = new PanXRecognizer(DRAG_THRESHOLD, MAX_ATTACK_ANGLE);
private pointerEvents: PointerEvents;
private firstCoordX: number;
private firstTimestamp: number;

constructor(public list: List) {
super(list.getNativeElement(), {
direction: 'x',
threshold: DRAG_THRESHOLD
this.pointerEvents = this.events.pointerEvents({
element: list.getNativeElement(),
pointerDown: this.pointerStart.bind(this),
pointerMove: this.pointerMove.bind(this),
pointerUp: this.pointerEnd.bind(this),
});
this.listen();
}

onTapCallback(ev: any) {
if (isFromOptionButtons(ev)) {
return;
private pointerStart(ev: any): boolean {
if (this.selectedContainer) {
return false;
}
// Get swiped sliding container
let container = getContainer(ev);
if (!container) {
this.closeOpened();
return false;
}
let didClose = this.closeOpened();
if (didClose) {
console.debug('tap close sliding item, preventDefault');
ev.preventDefault();
// Close open container if it is not the selected one.
if (container !== this.openContainer && this.closeOpened()) {
return false;
}

let coord = pointerCoord(ev);
this.preSelectedContainer = container;
this.panDetector.start(coord);
this.firstCoordX = coord.x;
this.firstTimestamp = Date.now();
return true;
}

onDragStart(ev: any): boolean {
let angle = Math.abs(ev.angle);
if (angle > MAX_ATTACK_ANGLE && Math.abs(angle - 180) > MAX_ATTACK_ANGLE) {
this.closeOpened();
return false;
private pointerMove(ev: any) {
if (this.selectedContainer) {
this.onDragMove(ev);
return;
}
let coord = pointerCoord(ev);
if (this.panDetector.detect(coord)) {
if (!this.panDetector.isPanX()) {
this.pointerEvents.stop();
this.closeOpened();
} else {
this.onDragStart(ev, coord);
}
}
}

private pointerEnd(ev: any) {
if (this.selectedContainer) {
console.debug('onDragStart, another container is already selected');
return false;
this.onDragEnd(ev);
} else {
this.closeOpened();
}
}

private onDragStart(ev: any, coord: Coordinates): boolean {
let container = getContainer(ev);
if (!container) {
console.debug('onDragStart, no itemContainerEle');
return false;
}
ev.preventDefault();

// Close open container if it is not the selected one.
if (container !== this.openContainer) {
this.closeOpened();
}

this.selectedContainer = container;
this.openContainer = container;
container.startSliding(ev.center.x);

return true;
this.selectedContainer = this.openContainer = this.preSelectedContainer;
container.startSliding(coord.x);
}

onDrag(ev: any): boolean {
if (this.selectedContainer) {
this.selectedContainer.moveSliding(ev.center.x);
ev.preventDefault();
}
return;
private onDragMove(ev: any) {
let coordX = pointerCoord(ev).x;
ev.preventDefault();
this.selectedContainer.moveSliding(coordX);
}

onDragEnd(ev: any) {
if (!this.selectedContainer) {
return;
}
private onDragEnd(ev: any) {
ev.preventDefault();
let coordX = pointerCoord(ev).x;
let deltaX = (coordX - this.firstCoordX);
let deltaT = (Date.now() - this.firstTimestamp);

let openAmount = this.selectedContainer.endSliding(ev.velocityX);
let openAmount = this.selectedContainer.endSliding(deltaX / deltaT);
this.selectedContainer = null;

// TODO: I am not sure listening for a tap event is the best idea
// we should try mousedown/touchstart
if (openAmount === 0) {
this.openContainer = null;
this.off('tap', this.onTap);
this.onTap = null;
} else if (!this.onTap) {
this.onTap = (event: any) => this.onTapCallback(event);
this.on('tap', this.onTap);
}
this.preSelectedContainer = null;
}

closeOpened(): boolean {
Expand All @@ -97,15 +109,17 @@ export class ItemSlidingGesture extends DragGesture {
this.openContainer.close();
this.openContainer = null;
this.selectedContainer = null;
this.off('tap', this.onTap);
this.onTap = null;
return true;
}

unlisten() {
this.closeOpened();
super.unlisten();
this.events.unlistenAll();

this.list = null;
this.preSelectedContainer = null;
this.selectedContainer = null;
this.openContainer = null;
}
}

Expand All @@ -117,10 +131,69 @@ function getContainer(ev: any): ItemSliding {
return null;
}

function isFromOptionButtons(ev: any): boolean {
let button = closest(ev.target, '.button', true);
if (!button) {
class AngleRecognizer {
private startCoord: Coordinates;
private sumCoord: Coordinates;
private dirty: boolean;
private _angle: any = null;
private threshold: number;

constructor(threshold: number) {
this.threshold = threshold ** 2;
}

start(coord: Coordinates) {
this.startCoord = coord;
this._angle = 0;
this.dirty = true;
}

angle(): any {
return this._angle;
}

detect(coord: Coordinates): boolean {
if (!this.dirty) {
return false;
}
let deltaX = (coord.x - this.startCoord.x);
let deltaY = (coord.y - this.startCoord.y);
let distance = deltaX * deltaX + deltaY * deltaY;
if (distance >= this.threshold) {
this._angle = Math.atan2(deltaY, deltaX);
this.dirty = false;
return true;
}
return false;
}
}

const degresToRadians = Math.PI / 180;

class PanXRecognizer extends AngleRecognizer {
private _isPanX: boolean;
private maxAngle: number;

constructor(threshold: number, maxAngle: number) {
super(threshold);
this.maxAngle = maxAngle * degresToRadians;
}

start(coord: Coordinates) {
super.start(coord);
this._isPanX = false;
}

isPanX(): boolean {
return this._isPanX;
}

detect(coord: Coordinates): boolean {
if (super.detect(coord)) {
let angle = Math.abs(this.angle());
this._isPanX = (angle < this.maxAngle || Math.abs(angle - Math.PI) < this.maxAngle);
return true;
}
return false;
}
return !!closest(button, 'ion-item-options', true);
}
2 changes: 1 addition & 1 deletion src/components/item/item-sliding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Item } from './item';
import { isPresent } from '../../util/util';
import { List } from '../list/list';

const SWIPE_MARGIN = 20;
const SWIPE_MARGIN = 30;
const ELASTIC_FACTOR = 0.55;

export const enum ItemSideFlags {
Expand Down
11 changes: 6 additions & 5 deletions src/components/picker/picker-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,12 @@ export class PickerColumnCmp {
this.setSelected(this.col.selectedIndex, 0);

// Listening for pointer events
this.events.pointerEventsRef(this.elementRef,
(ev: any) => this.pointerStart(ev),
(ev: any) => this.pointerMove(ev),
(ev: any) => this.pointerEnd(ev)
);
this.events.pointerEvents({
elementRef: this.elementRef,
pointerDown: this.pointerStart.bind(this),
pointerMove: this.pointerMove.bind(this),
pointerUp: this.pointerEnd.bind(this)
});
}

ngOnDestroy() {
Expand Down
11 changes: 6 additions & 5 deletions src/components/range/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,11 +364,12 @@ export class Range implements AfterViewInit, ControlValueAccessor, OnDestroy {
this._renderer.setElementStyle(this._bar.nativeElement, 'right', barR);

// add touchstart/mousedown listeners
this._events.pointerEventsRef(this._slider,
this.pointerDown.bind(this),
this.pointerMove.bind(this),
this.pointerUp.bind(this));

this._events.pointerEvents({
elementRef: this._slider,
pointerDown: this.pointerDown.bind(this),
pointerMove: this.pointerMove.bind(this),
pointerUp: this.pointerUp.bind(this)
});
this.createTicks();
}

Expand Down
10 changes: 6 additions & 4 deletions src/components/refresher/refresher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,12 @@ export class Refresher {
this._events.unlistenAll();
this._pointerEvents = null;
if (shouldListen) {
this._pointerEvents = this._events.pointerEvents(this._content.getScrollElement(),
this._onStart.bind(this),
this._onMove.bind(this),
this._onEnd.bind(this));
this._pointerEvents = this._events.pointerEvents({
element: this._content.getScrollElement(),
pointerDown: this._onStart.bind(this),
pointerMove: this._onMove.bind(this),
pointerUp: this._onEnd.bind(this)
});
}
}

Expand Down
11 changes: 6 additions & 5 deletions src/components/toggle/toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,12 @@ export class Toggle implements AfterContentInit, ControlValueAccessor, OnDestroy
*/
ngAfterContentInit() {
this._init = true;
this._events.pointerEventsRef(this._elementRef,
(ev: any) => this.pointerDown(ev),
(ev: any) => this.pointerMove(ev),
(ev: any) => this.pointerUp(ev)
);
this._events.pointerEvents({
elementRef: this._elementRef,
pointerDown: this.pointerDown.bind(this),
pointerMove: this.pointerMove.bind(this),
pointerUp: this.pointerUp.bind(this)
});
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/util/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,10 @@ export function pointerCoord(ev: any): Coordinates {
}

export function hasPointerMoved(threshold: number, startCoord: Coordinates, endCoord: Coordinates) {
return startCoord && endCoord &&
(Math.abs(startCoord.x - endCoord.x) > threshold || Math.abs(startCoord.y - endCoord.y) > threshold);
let deltaX = (startCoord.x - endCoord.x);
let deltaY = (startCoord.y - endCoord.y);
let distance = deltaX * deltaX + deltaY * deltaY;
return distance > (threshold * threshold);
}

export function isActive(ele: HTMLElement) {
Expand Down
Loading

0 comments on commit e1a97b2

Please sign in to comment.