mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
perf(gesture): lazy loaded dynamic ES module
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Animation } from '../../interface';
|
||||
import { TransitionOptions } from '../../utils/transition';
|
||||
import { TransitionOptions } from '../transition';
|
||||
|
||||
const DURATION = 500;
|
||||
const EASING = 'cubic-bezier(0.36,0.66,0.04,1)';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Animation } from '../../interface';
|
||||
import { TransitionOptions } from '../../utils/transition';
|
||||
import { TransitionOptions } from '../transition';
|
||||
|
||||
const TRANSLATEY = 'translateY';
|
||||
const OFF_BOTTOM = '40px';
|
||||
|
||||
256
core/src/utils/gesture/gesture-controller.ts
Normal file
256
core/src/utils/gesture/gesture-controller.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
|
||||
export class GestureController {
|
||||
|
||||
private gestureId = 0;
|
||||
private requestedStart = new Map<number, number>();
|
||||
private disabledGestures = new Map<string, Set<number>>();
|
||||
private disabledScroll = new Set<number>();
|
||||
private capturedId: number | null = null;
|
||||
|
||||
constructor(
|
||||
private doc: Document
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a gesture delegate based on the GestureConfig passed
|
||||
*/
|
||||
createGesture(config: GestureConfig): GestureDelegate {
|
||||
return new GestureDelegate(
|
||||
this,
|
||||
this.newID(),
|
||||
config.name,
|
||||
config.priority ? config.priority : 0,
|
||||
!!config.disableScroll
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a blocker that will block any other gesture events from firing. Set in the ion-gesture component.
|
||||
*/
|
||||
createBlocker(opts: BlockerConfig = {}): BlockerDelegate {
|
||||
return new BlockerDelegate(
|
||||
this.newID(),
|
||||
this,
|
||||
opts.disable,
|
||||
!!opts.disableScroll
|
||||
);
|
||||
}
|
||||
|
||||
start(gestureName: string, id: number, priority: number): boolean {
|
||||
if (!this.canStart(gestureName)) {
|
||||
this.requestedStart.delete(id);
|
||||
return false;
|
||||
}
|
||||
this.requestedStart.set(id, priority);
|
||||
return true;
|
||||
}
|
||||
|
||||
capture(gestureName: string, id: number, priority: number): boolean {
|
||||
if (!this.start(gestureName, id, priority)) {
|
||||
return false;
|
||||
}
|
||||
const requestedStart = this.requestedStart;
|
||||
let maxPriority = -10000;
|
||||
|
||||
requestedStart.forEach(value => {
|
||||
maxPriority = Math.max(maxPriority, value);
|
||||
});
|
||||
|
||||
if (maxPriority === priority) {
|
||||
this.capturedId = id;
|
||||
requestedStart.clear();
|
||||
|
||||
const event = new CustomEvent('ionGestureCaptured', { detail: gestureName });
|
||||
this.doc.body.dispatchEvent(event);
|
||||
return true;
|
||||
}
|
||||
requestedStart.delete(id);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
release(id: number) {
|
||||
this.requestedStart.delete(id);
|
||||
|
||||
if (this.capturedId && id === this.capturedId) {
|
||||
this.capturedId = null;
|
||||
}
|
||||
}
|
||||
|
||||
disableGesture(gestureName: string, id: number) {
|
||||
let set = this.disabledGestures.get(gestureName);
|
||||
if (!set) {
|
||||
set = new Set<number>();
|
||||
this.disabledGestures.set(gestureName, set);
|
||||
}
|
||||
set.add(id);
|
||||
}
|
||||
|
||||
enableGesture(gestureName: string, id: number) {
|
||||
const set = this.disabledGestures.get(gestureName);
|
||||
if (set) {
|
||||
set.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
disableScroll(id: number) {
|
||||
this.disabledScroll.add(id);
|
||||
}
|
||||
|
||||
enableScroll(id: number) {
|
||||
this.disabledScroll.delete(id);
|
||||
}
|
||||
|
||||
canStart(gestureName: string): boolean {
|
||||
if (this.capturedId) {
|
||||
// a gesture already captured
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isDisabled(gestureName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
isCaptured(): boolean {
|
||||
return !!this.capturedId;
|
||||
}
|
||||
|
||||
isScrollDisabled(): boolean {
|
||||
return this.disabledScroll.size > 0;
|
||||
}
|
||||
|
||||
isDisabled(gestureName: string): boolean {
|
||||
const disabled = this.disabledGestures.get(gestureName);
|
||||
if (disabled && disabled.size > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private newID(): number {
|
||||
this.gestureId++;
|
||||
return this.gestureId;
|
||||
}
|
||||
}
|
||||
|
||||
export class GestureDelegate {
|
||||
private ctrl?: GestureController;
|
||||
|
||||
constructor(
|
||||
ctrl: any,
|
||||
private id: number,
|
||||
private name: string,
|
||||
private priority: number,
|
||||
private disableScroll: boolean
|
||||
) {
|
||||
this.ctrl = ctrl;
|
||||
}
|
||||
|
||||
canStart(): boolean {
|
||||
if (!this.ctrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.ctrl.canStart(this.name);
|
||||
}
|
||||
|
||||
start(): boolean {
|
||||
if (!this.ctrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.ctrl.start(this.name, this.id, this.priority);
|
||||
}
|
||||
|
||||
capture(): boolean {
|
||||
if (!this.ctrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const captured = this.ctrl.capture(this.name, this.id, this.priority);
|
||||
if (captured && this.disableScroll) {
|
||||
this.ctrl.disableScroll(this.id);
|
||||
}
|
||||
|
||||
return captured;
|
||||
}
|
||||
|
||||
release() {
|
||||
if (this.ctrl) {
|
||||
this.ctrl.release(this.id);
|
||||
|
||||
if (this.disableScroll) {
|
||||
this.ctrl.enableScroll(this.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.release();
|
||||
this.ctrl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockerDelegate {
|
||||
|
||||
private ctrl?: GestureController;
|
||||
|
||||
constructor(
|
||||
private id: number,
|
||||
ctrl: any,
|
||||
private disable: string[] | undefined,
|
||||
private disableScroll: boolean
|
||||
) {
|
||||
this.ctrl = ctrl;
|
||||
}
|
||||
|
||||
block() {
|
||||
if (!this.ctrl) {
|
||||
return;
|
||||
}
|
||||
if (this.disable) {
|
||||
for (const gesture of this.disable) {
|
||||
this.ctrl.disableGesture(gesture, this.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.disableScroll) {
|
||||
this.ctrl.disableScroll(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
unblock() {
|
||||
if (!this.ctrl) {
|
||||
return;
|
||||
}
|
||||
if (this.disable) {
|
||||
for (const gesture of this.disable) {
|
||||
this.ctrl.enableGesture(gesture, this.id);
|
||||
}
|
||||
}
|
||||
if (this.disableScroll) {
|
||||
this.ctrl.enableScroll(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.unblock();
|
||||
this.ctrl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GestureConfig {
|
||||
name: string;
|
||||
priority?: number;
|
||||
disableScroll?: boolean;
|
||||
}
|
||||
|
||||
export interface BlockerConfig {
|
||||
disable?: string[];
|
||||
disableScroll?: boolean;
|
||||
}
|
||||
|
||||
export const gestureController = new GestureController(document);
|
||||
346
core/src/utils/gesture/gesture.ts
Normal file
346
core/src/utils/gesture/gesture.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { QueueApi } from '@stencil/core';
|
||||
|
||||
import { PanRecognizer } from './recognizers';
|
||||
|
||||
import { GestureDelegate, gestureController } from './gesture-controller';
|
||||
import { PointerEvents } from './pointer-events';
|
||||
|
||||
export interface GestureDetail {
|
||||
type: string;
|
||||
startX: number;
|
||||
startY: number;
|
||||
startTimeStamp: number;
|
||||
currentX: number;
|
||||
currentY: number;
|
||||
velocityX: number;
|
||||
velocityY: number;
|
||||
deltaX: number;
|
||||
deltaY: number;
|
||||
timeStamp: number;
|
||||
event: UIEvent;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export type GestureCallback = (detail?: GestureDetail) => boolean | void;
|
||||
|
||||
export interface GestureConfig {
|
||||
el: Node;
|
||||
disableScroll?: boolean;
|
||||
|
||||
queue: QueueApi;
|
||||
direction?: 'x' | 'y';
|
||||
gestureName: string;
|
||||
gesturePriority?: number;
|
||||
passive?: boolean;
|
||||
maxAngle?: number;
|
||||
threshold?: number;
|
||||
|
||||
canStart?: GestureCallback;
|
||||
onWillStart?: (_: GestureDetail) => Promise<void>;
|
||||
onStart?: GestureCallback;
|
||||
onMove?: GestureCallback;
|
||||
onEnd?: GestureCallback;
|
||||
notCaptured?: GestureCallback;
|
||||
}
|
||||
|
||||
export function create(config: GestureConfig) {
|
||||
return new Gesture(config);
|
||||
}
|
||||
|
||||
export class Gesture {
|
||||
|
||||
private detail: GestureDetail;
|
||||
private positions: number[] = [];
|
||||
private gesture: GestureDelegate;
|
||||
private pan: PanRecognizer;
|
||||
private hasCapturedPan = false;
|
||||
private hasStartedPan = false;
|
||||
private hasFiredStart = true;
|
||||
private isMoveQueued = false;
|
||||
private pointerEvents: PointerEvents;
|
||||
|
||||
private canStart?: GestureCallback;
|
||||
private onWillStart?: (_: GestureDetail) => Promise<void>;
|
||||
private onStart?: GestureCallback;
|
||||
private onMove?: GestureCallback;
|
||||
private onEnd?: GestureCallback;
|
||||
private notCaptured?: GestureCallback;
|
||||
private threshold: number;
|
||||
private queue: QueueApi;
|
||||
|
||||
constructor(config: GestureConfig) {
|
||||
const finalConfig = {
|
||||
disableScroll: false,
|
||||
direction: 'x',
|
||||
gesturePriority: 0,
|
||||
passive: true,
|
||||
maxAngle: 40,
|
||||
threshold: 10,
|
||||
|
||||
...config
|
||||
};
|
||||
|
||||
this.canStart = finalConfig.canStart;
|
||||
this.onWillStart = finalConfig.onWillStart;
|
||||
this.onStart = finalConfig.onStart;
|
||||
this.onEnd = finalConfig.onEnd;
|
||||
this.onMove = finalConfig.onMove;
|
||||
this.threshold = finalConfig.threshold;
|
||||
this.queue = finalConfig.queue;
|
||||
|
||||
this.detail = {
|
||||
type: 'pan',
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
startTimeStamp: 0,
|
||||
currentX: 0,
|
||||
currentY: 0,
|
||||
velocityX: 0,
|
||||
velocityY: 0,
|
||||
deltaX: 0,
|
||||
deltaY: 0,
|
||||
timeStamp: 0,
|
||||
event: undefined as any,
|
||||
data: undefined
|
||||
};
|
||||
|
||||
this.pointerEvents = new PointerEvents(
|
||||
finalConfig.el,
|
||||
this.pointerDown.bind(this),
|
||||
this.pointerMove.bind(this),
|
||||
this.pointerUp.bind(this),
|
||||
{
|
||||
capture: false,
|
||||
}
|
||||
);
|
||||
|
||||
this.pan = new PanRecognizer(finalConfig.direction, finalConfig.threshold, finalConfig.maxAngle);
|
||||
this.gesture = gestureController.createGesture({
|
||||
name: config.gestureName,
|
||||
priority: config.gesturePriority,
|
||||
disableScroll: config.disableScroll
|
||||
});
|
||||
}
|
||||
|
||||
set disabled(disabled: boolean) {
|
||||
this.pointerEvents.disabled = disabled;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.gesture.destroy();
|
||||
this.pointerEvents.destroy();
|
||||
}
|
||||
|
||||
private pointerDown(ev: UIEvent): boolean {
|
||||
const timeStamp = now(ev);
|
||||
if (this.hasStartedPan || !this.hasFiredStart) {
|
||||
return false;
|
||||
}
|
||||
const detail = this.detail;
|
||||
|
||||
updateDetail(ev, detail);
|
||||
detail.startX = detail.currentX;
|
||||
detail.startY = detail.currentY;
|
||||
detail.startTimeStamp = detail.timeStamp = timeStamp;
|
||||
detail.velocityX = detail.velocityY = detail.deltaX = detail.deltaY = 0;
|
||||
detail.event = ev;
|
||||
this.positions.length = 0;
|
||||
|
||||
// Check if gesture can start
|
||||
if (this.canStart && this.canStart(detail) === false) {
|
||||
return false;
|
||||
}
|
||||
// Release fallback
|
||||
this.gesture.release();
|
||||
|
||||
// Start gesture
|
||||
if (!this.gesture.start()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.positions.push(detail.currentX, detail.currentY, timeStamp);
|
||||
this.hasStartedPan = true;
|
||||
if (this.threshold === 0) {
|
||||
return this.tryToCapturePan();
|
||||
}
|
||||
this.pan.start(detail.startX, detail.startY);
|
||||
return true;
|
||||
}
|
||||
|
||||
private pointerMove(ev: UIEvent) {
|
||||
// fast path, if gesture is currently captured
|
||||
// do minimun job to get user-land even dispatched
|
||||
if (this.hasCapturedPan) {
|
||||
if (!this.isMoveQueued && this.hasFiredStart) {
|
||||
this.isMoveQueued = true;
|
||||
this.calcGestureData(ev);
|
||||
this.queue.write(this.fireOnMove.bind(this));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// gesture is currently being detected
|
||||
const detail = this.detail;
|
||||
this.calcGestureData(ev);
|
||||
if (this.pan.detect(detail.currentX, detail.currentY)) {
|
||||
if (this.pan.isGesture()) {
|
||||
if (!this.tryToCapturePan()) {
|
||||
this.abortGesture();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fireOnMove() {
|
||||
// Since fireOnMove is called inside a RAF, onEnd() might be called,
|
||||
// we must double check hasCapturedPan
|
||||
if (!this.hasCapturedPan) {
|
||||
return;
|
||||
}
|
||||
const detail = this.detail;
|
||||
this.isMoveQueued = false;
|
||||
if (this.onMove) {
|
||||
this.onMove(detail);
|
||||
}
|
||||
}
|
||||
|
||||
private calcGestureData(ev: UIEvent) {
|
||||
const detail = this.detail;
|
||||
updateDetail(ev, detail);
|
||||
|
||||
const currentX = detail.currentX;
|
||||
const currentY = detail.currentY;
|
||||
const timestamp = detail.timeStamp = now(ev);
|
||||
detail.deltaX = currentX - detail.startX;
|
||||
detail.deltaY = currentY - detail.startY;
|
||||
detail.event = ev;
|
||||
|
||||
const timeRange = timestamp - 100;
|
||||
const positions = this.positions;
|
||||
let startPos = positions.length - 1;
|
||||
|
||||
// move pointer to position measured 100ms ago
|
||||
while (startPos > 0 && positions[startPos] > timeRange) {
|
||||
startPos -= 3;
|
||||
}
|
||||
|
||||
if (startPos > 1) {
|
||||
// compute relative movement between these two points
|
||||
const frequency = 1 / (positions[startPos] - timestamp);
|
||||
const movedY = positions[startPos - 1] - currentY;
|
||||
const movedX = positions[startPos - 2] - currentX;
|
||||
|
||||
// based on XXms compute the movement to apply for each render step
|
||||
// velocity = space/time = s*(1/t) = s*frequency
|
||||
detail.velocityX = movedX * frequency;
|
||||
detail.velocityY = movedY * frequency;
|
||||
} else {
|
||||
detail.velocityX = 0;
|
||||
detail.velocityY = 0;
|
||||
}
|
||||
positions.push(currentX, currentY, timestamp);
|
||||
}
|
||||
|
||||
private tryToCapturePan(): boolean {
|
||||
if (this.gesture && !this.gesture.capture()) {
|
||||
return false;
|
||||
}
|
||||
this.hasCapturedPan = true;
|
||||
this.hasFiredStart = false;
|
||||
|
||||
// reset start position since the real user-land event starts here
|
||||
// If the pan detector threshold is big, not reseting the start position
|
||||
// will cause a jump in the animation equal to the detector threshold.
|
||||
// the array of positions used to calculate the gesture velocity does not
|
||||
// need to be cleaned, more points in the positions array always results in a
|
||||
// more acurate value of the velocity.
|
||||
const detail = this.detail;
|
||||
detail.startX = detail.currentX;
|
||||
detail.startY = detail.currentY;
|
||||
detail.startTimeStamp = detail.timeStamp;
|
||||
|
||||
if (this.onWillStart) {
|
||||
this.onWillStart(this.detail).then(this.fireOnStart.bind(this));
|
||||
} else {
|
||||
this.fireOnStart();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private fireOnStart() {
|
||||
if (this.onStart) {
|
||||
this.onStart(this.detail);
|
||||
}
|
||||
this.hasFiredStart = true;
|
||||
}
|
||||
|
||||
private abortGesture() {
|
||||
this.reset();
|
||||
this.pointerEvents.stop();
|
||||
if (this.notCaptured) {
|
||||
this.notCaptured(this.detail);
|
||||
}
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.hasCapturedPan = false;
|
||||
this.hasStartedPan = false;
|
||||
this.isMoveQueued = false;
|
||||
this.hasFiredStart = true;
|
||||
if (this.gesture) {
|
||||
this.gesture.release();
|
||||
}
|
||||
}
|
||||
|
||||
// END *************************
|
||||
|
||||
private pointerUp(ev: UIEvent) {
|
||||
const hasCaptured = this.hasCapturedPan;
|
||||
const hasFiredStart = this.hasFiredStart;
|
||||
this.reset();
|
||||
|
||||
if (!hasFiredStart) {
|
||||
return;
|
||||
}
|
||||
this.calcGestureData(ev);
|
||||
|
||||
const detail = this.detail;
|
||||
|
||||
// Try to capture press
|
||||
if (hasCaptured) {
|
||||
if (this.onEnd) {
|
||||
this.onEnd(detail);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Not captured any event
|
||||
if (this.notCaptured) {
|
||||
this.notCaptured(detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateDetail(ev: any, detail: GestureDetail) {
|
||||
// get X coordinates for either a mouse click
|
||||
// or a touch depending on the given event
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
if (ev) {
|
||||
const changedTouches = ev.changedTouches;
|
||||
if (changedTouches && changedTouches.length > 0) {
|
||||
const touch = changedTouches[0];
|
||||
x = touch.clientX;
|
||||
y = touch.clientY;
|
||||
} else if (ev.pageX !== undefined) {
|
||||
x = ev.pageX;
|
||||
y = ev.pageY;
|
||||
}
|
||||
}
|
||||
detail.currentX = x;
|
||||
detail.currentY = y;
|
||||
}
|
||||
|
||||
function now(ev: UIEvent) {
|
||||
return ev.timeStamp || Date.now();
|
||||
}
|
||||
50
core/src/utils/gesture/listener.ts
Normal file
50
core/src/utils/gesture/listener.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
let _sPassive: boolean | undefined;
|
||||
|
||||
function supportsPassive(node: Node) {
|
||||
if (_sPassive === undefined) {
|
||||
try {
|
||||
const opts = Object.defineProperty({}, 'passive', {
|
||||
get: () => {
|
||||
_sPassive = true;
|
||||
}
|
||||
});
|
||||
node.addEventListener('optsTest', () => { return; }, opts);
|
||||
} catch {
|
||||
_sPassive = false;
|
||||
}
|
||||
}
|
||||
return !!_sPassive;
|
||||
}
|
||||
|
||||
export function addEventListener(
|
||||
el: any,
|
||||
eventName: string,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
opts: {
|
||||
passive?: boolean;
|
||||
capture?: boolean
|
||||
}
|
||||
): () => void {
|
||||
// use event listener options when supported
|
||||
// otherwise it's just a boolean for the "capture" arg
|
||||
const listenerOpts = supportsPassive(el) ? {
|
||||
'capture': !!opts.capture,
|
||||
'passive': !!opts.passive,
|
||||
} : !!opts.capture;
|
||||
|
||||
let add: string;
|
||||
let remove: string;
|
||||
if (el['__zone_symbol__addEventListener']) {
|
||||
add = '__zone_symbol__addEventListener';
|
||||
remove = '__zone_symbol__removeEventListener';
|
||||
} else {
|
||||
add = 'addEventListener';
|
||||
remove = 'removeEventListener';
|
||||
}
|
||||
|
||||
el[add](eventName, callback, listenerOpts);
|
||||
return () => {
|
||||
el[remove](eventName, callback, listenerOpts);
|
||||
};
|
||||
}
|
||||
144
core/src/utils/gesture/pointer-events.ts
Normal file
144
core/src/utils/gesture/pointer-events.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { addEventListener } from './listener';
|
||||
|
||||
const MOUSE_WAIT = 2000;
|
||||
|
||||
export class PointerEvents {
|
||||
|
||||
private rmTouchStart?: () => void;
|
||||
private rmTouchMove?: () => void;
|
||||
private rmTouchEnd?: () => void;
|
||||
private rmTouchCancel?: () => void;
|
||||
|
||||
private rmMouseStart?: () => void;
|
||||
private rmMouseMove?: () => void;
|
||||
private rmMouseUp?: () => void;
|
||||
|
||||
private bindTouchEnd: any;
|
||||
private bindMouseUp: any;
|
||||
|
||||
private lastTouchEvent = 0;
|
||||
|
||||
constructor(
|
||||
private el: Node,
|
||||
private pointerDown: any,
|
||||
private pointerMove: any,
|
||||
private pointerUp: any,
|
||||
private options: EventListenerOptions
|
||||
) {
|
||||
this.bindTouchEnd = this.handleTouchEnd.bind(this);
|
||||
this.bindMouseUp = this.handleMouseUp.bind(this);
|
||||
}
|
||||
|
||||
set disabled(disabled: boolean) {
|
||||
if (disabled) {
|
||||
if (this.rmTouchStart) {
|
||||
this.rmTouchStart();
|
||||
}
|
||||
if (this.rmMouseStart) {
|
||||
this.rmMouseStart();
|
||||
}
|
||||
this.rmTouchStart = this.rmMouseStart = undefined;
|
||||
this.stop();
|
||||
|
||||
} else {
|
||||
if (!this.rmTouchStart) {
|
||||
this.rmTouchStart = addEventListener(this.el, 'touchstart', this.handleTouchStart.bind(this), this.options);
|
||||
}
|
||||
if (!this.rmMouseStart) {
|
||||
this.rmMouseStart = addEventListener(this.el, 'mousedown', this.handleMouseDown.bind(this), this.options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.stopTouch();
|
||||
this.stopMouse();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.disabled = true;
|
||||
this.pointerUp = this.pointerMove = this.pointerDown = undefined;
|
||||
}
|
||||
|
||||
private handleTouchStart(ev: any) {
|
||||
this.lastTouchEvent = Date.now() + MOUSE_WAIT;
|
||||
if (!this.pointerDown(ev, POINTER_EVENT_TYPE_TOUCH)) {
|
||||
return;
|
||||
}
|
||||
if (!this.rmTouchMove && this.pointerMove) {
|
||||
this.rmTouchMove = addEventListener(this.el, 'touchmove', this.pointerMove, this.options);
|
||||
}
|
||||
if (!this.rmTouchEnd) {
|
||||
this.rmTouchEnd = addEventListener(this.el, 'touchend', this.bindTouchEnd, this.options);
|
||||
}
|
||||
if (!this.rmTouchCancel) {
|
||||
this.rmTouchCancel = addEventListener(this.el, 'touchcancel', this.bindTouchEnd, this.options);
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseDown(ev: any) {
|
||||
if (this.lastTouchEvent > Date.now()) {
|
||||
console.debug('mousedown event dropped because of previous touch');
|
||||
return;
|
||||
}
|
||||
if (!this.pointerDown(ev, POINTER_EVENT_TYPE_MOUSE)) {
|
||||
return;
|
||||
}
|
||||
if (!this.rmMouseMove && this.pointerMove) {
|
||||
this.rmMouseMove = addEventListener(this.el.ownerDocument, 'mousemove', this.pointerMove, this.options);
|
||||
}
|
||||
if (!this.rmMouseUp) {
|
||||
this.rmMouseUp = addEventListener(this.el.ownerDocument, 'mouseup', this.bindMouseUp, this.options);
|
||||
}
|
||||
}
|
||||
|
||||
private handleTouchEnd(ev: any) {
|
||||
this.stopTouch();
|
||||
if (this.pointerUp) {
|
||||
this.pointerUp(ev, POINTER_EVENT_TYPE_TOUCH);
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseUp(ev: any) {
|
||||
this.stopMouse();
|
||||
if (this.pointerUp) {
|
||||
this.pointerUp(ev, POINTER_EVENT_TYPE_MOUSE);
|
||||
}
|
||||
}
|
||||
|
||||
private stopTouch() {
|
||||
if (this.rmTouchMove) {
|
||||
this.rmTouchMove();
|
||||
}
|
||||
if (this.rmTouchEnd) {
|
||||
this.rmTouchEnd();
|
||||
}
|
||||
if (this.rmTouchCancel) {
|
||||
this.rmTouchCancel();
|
||||
}
|
||||
this.rmTouchMove = this.rmTouchEnd = this.rmTouchCancel = undefined;
|
||||
}
|
||||
|
||||
private stopMouse() {
|
||||
if (this.rmMouseMove) {
|
||||
this.rmMouseMove();
|
||||
}
|
||||
if (this.rmMouseUp) {
|
||||
this.rmMouseUp();
|
||||
}
|
||||
this.rmMouseMove = this.rmMouseUp = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const POINTER_EVENT_TYPE_MOUSE = 1;
|
||||
export const POINTER_EVENT_TYPE_TOUCH = 2;
|
||||
|
||||
export interface PointerEventsConfig {
|
||||
element?: HTMLElement;
|
||||
pointerDown: (ev: any) => boolean;
|
||||
pointerMove?: (ev: any) => void;
|
||||
pointerUp?: (ev: any) => void;
|
||||
zone?: boolean;
|
||||
capture?: boolean;
|
||||
passive?: boolean;
|
||||
}
|
||||
63
core/src/utils/gesture/recognizers.ts
Normal file
63
core/src/utils/gesture/recognizers.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
|
||||
export class PanRecognizer {
|
||||
|
||||
private startX!: number;
|
||||
private startY!: number;
|
||||
|
||||
private dirty = false;
|
||||
private threshold: number;
|
||||
private maxCosine: number;
|
||||
private isDirX: boolean;
|
||||
private isPan = 0;
|
||||
|
||||
constructor(direction: string, threshold: number, maxAngle: number) {
|
||||
const radians = maxAngle * (Math.PI / 180);
|
||||
this.isDirX = direction === 'x';
|
||||
this.maxCosine = Math.cos(radians);
|
||||
this.threshold = threshold * threshold;
|
||||
}
|
||||
|
||||
start(x: number, y: number) {
|
||||
this.startX = x;
|
||||
this.startY = y;
|
||||
this.isPan = 0;
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
detect(x: number, y: number): boolean {
|
||||
if (!this.dirty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const deltaX = (x - this.startX);
|
||||
const deltaY = (y - this.startY);
|
||||
const distance = deltaX * deltaX + deltaY * deltaY;
|
||||
|
||||
if (distance < this.threshold) {
|
||||
return false;
|
||||
}
|
||||
const hypotenuse = Math.sqrt(distance);
|
||||
const cosine = (this.isDirX ? deltaX : deltaY) / hypotenuse;
|
||||
|
||||
if (cosine > this.maxCosine) {
|
||||
this.isPan = 1;
|
||||
|
||||
} else if (cosine < -this.maxCosine) {
|
||||
this.isPan = -1;
|
||||
|
||||
} else {
|
||||
this.isPan = 0;
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
isGesture(): boolean {
|
||||
return this.isPan !== 0;
|
||||
}
|
||||
|
||||
getDirection(): number {
|
||||
return this.isPan;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pointerCoord } from '../../../utils/helpers';
|
||||
import { pointerCoord } from '../../helpers';
|
||||
|
||||
import { isFocused, relocateInput } from './common';
|
||||
import { getScrollData } from './scroll-data';
|
||||
|
||||
Reference in New Issue
Block a user