mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-22 21:48:42 +08:00
fix(sliding): much better UX + performance
- sliding should behave exactly like a native one - much better performance references #7049 references #7116 closes #6913 closes #6958
This commit is contained in:
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
let didClose = this.closeOpened();
|
||||
if (didClose) {
|
||||
console.debug('tap close sliding item, preventDefault');
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onDragStart(ev: any): boolean {
|
||||
let angle = Math.abs(ev.angle);
|
||||
if (angle > MAX_ATTACK_ANGLE && Math.abs(angle - 180) > MAX_ATTACK_ANGLE) {
|
||||
// Get swiped sliding container
|
||||
let container = getContainer(ev);
|
||||
if (!container) {
|
||||
this.closeOpened();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.selectedContainer) {
|
||||
console.debug('onDragStart, another container is already selected');
|
||||
// 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
onDrag(ev: any): boolean {
|
||||
if (this.selectedContainer) {
|
||||
this.selectedContainer.moveSliding(ev.center.x);
|
||||
ev.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onDragEnd(ev: any) {
|
||||
if (!this.selectedContainer) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
|
||||
let openAmount = this.selectedContainer.endSliding(ev.velocityX);
|
||||
this.selectedContainer = null;
|
||||
this.selectedContainer = this.openContainer = this.preSelectedContainer;
|
||||
container.startSliding(coord.x);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
private onDragMove(ev: any) {
|
||||
let coordX = pointerCoord(ev).x;
|
||||
ev.preventDefault();
|
||||
this.selectedContainer.moveSliding(coordX);
|
||||
}
|
||||
|
||||
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(deltaX / deltaT);
|
||||
this.selectedContainer = null;
|
||||
this.preSelectedContainer = null;
|
||||
}
|
||||
|
||||
closeOpened(): boolean {
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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) {
|
||||
|
@ -1,5 +1,14 @@
|
||||
import {ElementRef} from '@angular/core';
|
||||
|
||||
export interface PointerEventsConfig {
|
||||
element?: HTMLElement;
|
||||
elementRef?: ElementRef;
|
||||
pointerDown: (ev: any) => boolean;
|
||||
pointerMove: (ev: any) => void;
|
||||
pointerUp: (ev: any) => void;
|
||||
nativeOptions?: any;
|
||||
zone?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
@ -14,6 +23,9 @@ export class PointerEvents {
|
||||
private rmMouseMove: Function = null;
|
||||
private rmMouseUp: Function = null;
|
||||
|
||||
private bindTouchEnd: Function;
|
||||
private bindMouseUp: Function;
|
||||
|
||||
private lastTouchEvent: number = 0;
|
||||
|
||||
mouseWait: number = 2 * 1000;
|
||||
@ -23,7 +35,11 @@ export class PointerEvents {
|
||||
private pointerMove: any,
|
||||
private pointerUp: any,
|
||||
private zone: boolean,
|
||||
private option: any) {
|
||||
private option: any
|
||||
) {
|
||||
|
||||
this.bindTouchEnd = this.handleTouchEnd.bind(this);
|
||||
this.bindMouseUp = this.handleMouseUp.bind(this);
|
||||
|
||||
this.rmTouchStart = listenEvent(ele, 'touchstart', zone, option, this.handleTouchStart.bind(this));
|
||||
this.rmMouseStart = listenEvent(ele, 'mousedown', zone, option, this.handleMouseDown.bind(this));
|
||||
@ -37,12 +53,11 @@ export class PointerEvents {
|
||||
if (!this.rmTouchMove) {
|
||||
this.rmTouchMove = listenEvent(this.ele, 'touchmove', this.zone, this.option, this.pointerMove);
|
||||
}
|
||||
let handleTouchEnd = (ev: any) => this.handleTouchEnd(ev);
|
||||
if (!this.rmTouchEnd) {
|
||||
this.rmTouchEnd = listenEvent(this.ele, 'touchend', this.zone, this.option, handleTouchEnd);
|
||||
this.rmTouchEnd = listenEvent(this.ele, 'touchend', this.zone, this.option, this.bindTouchEnd);
|
||||
}
|
||||
if (!this.rmTouchCancel) {
|
||||
this.rmTouchCancel = listenEvent(this.ele, 'touchcancel', this.zone, this.option, handleTouchEnd);
|
||||
this.rmTouchCancel = listenEvent(this.ele, 'touchcancel', this.zone, this.option, this.bindTouchEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,7 +73,7 @@ export class PointerEvents {
|
||||
this.rmMouseMove = listenEvent(window, 'mousemove', this.zone, this.option, this.pointerMove);
|
||||
}
|
||||
if (!this.rmMouseUp) {
|
||||
this.rmMouseUp = listenEvent(window, 'mouseup', this.zone, this.option, (ev: any) => this.handleMouseUp(ev));
|
||||
this.rmMouseUp = listenEvent(window, 'mouseup', this.zone, this.option, this.bindMouseUp);
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,21 +141,26 @@ export class UIEventManager {
|
||||
return this.listen(ref.nativeElement, eventName, callback, option);
|
||||
}
|
||||
|
||||
pointerEventsRef(ref: ElementRef, pointerStart: any, pointerMove: any, pointerEnd: any, option?: any): PointerEvents {
|
||||
return this.pointerEvents(ref.nativeElement, pointerStart, pointerMove, pointerEnd, option);
|
||||
}
|
||||
|
||||
pointerEvents(element: any, pointerDown: any, pointerMove: any, pointerUp: any, option: any = false): PointerEvents {
|
||||
pointerEvents(config: PointerEventsConfig): PointerEvents {
|
||||
let element = config.element;
|
||||
if (!element) {
|
||||
element = config.elementRef.nativeElement;
|
||||
}
|
||||
|
||||
if (!element || !config.pointerDown || !config.pointerMove || !config.pointerUp) {
|
||||
console.error('PointerEvents config is invalid');
|
||||
return;
|
||||
}
|
||||
let zone = config.zone || this.zoneWrapped;
|
||||
let options = config.nativeOptions || false;
|
||||
|
||||
let submanager = new PointerEvents(
|
||||
element,
|
||||
pointerDown,
|
||||
pointerMove,
|
||||
pointerUp,
|
||||
this.zoneWrapped,
|
||||
option);
|
||||
config.pointerDown,
|
||||
config.pointerMove,
|
||||
config.pointerUp,
|
||||
zone,
|
||||
options);
|
||||
|
||||
let removeFunc = () => submanager.destroy();
|
||||
this.events.push(removeFunc);
|
||||
|
Reference in New Issue
Block a user