react-native-gesture-handler
Version:
Declarative API exposing native platform touch and gesture system to React Native
584 lines (458 loc) • 15.4 kB
text/typescript
import { State } from '../../State';
import { DEFAULT_TOUCH_SLOP } from '../constants';
import { AdaptedEvent, Config, StylusData, WheelDevice } from '../interfaces';
import GestureHandler from './GestureHandler';
const DEFAULT_MIN_POINTERS = 1;
const DEFAULT_MAX_POINTERS = 10;
const DEFAULT_MIN_DIST_SQ = DEFAULT_TOUCH_SLOP * DEFAULT_TOUCH_SLOP;
export default class PanGestureHandler extends GestureHandler {
private readonly customActivationProperties: string[] = [
'activeOffsetXStart',
'activeOffsetXEnd',
'failOffsetXStart',
'failOffsetXEnd',
'activeOffsetYStart',
'activeOffsetYEnd',
'failOffsetYStart',
'failOffsetYEnd',
'minVelocityX',
'minVelocityY',
'minVelocity',
];
public velocityX = 0;
public velocityY = 0;
private minDistSq = DEFAULT_MIN_DIST_SQ;
private activeOffsetXStart = -Number.MAX_SAFE_INTEGER;
private activeOffsetXEnd = Number.MIN_SAFE_INTEGER;
private failOffsetXStart = Number.MIN_SAFE_INTEGER;
private failOffsetXEnd = Number.MAX_SAFE_INTEGER;
private activeOffsetYStart = Number.MAX_SAFE_INTEGER;
private activeOffsetYEnd = Number.MIN_SAFE_INTEGER;
private failOffsetYStart = Number.MIN_SAFE_INTEGER;
private failOffsetYEnd = Number.MAX_SAFE_INTEGER;
private minVelocityX = Number.MAX_SAFE_INTEGER;
private minVelocityY = Number.MAX_SAFE_INTEGER;
private minVelocitySq = Number.MAX_SAFE_INTEGER;
private minPointers = DEFAULT_MIN_POINTERS;
private maxPointers = DEFAULT_MAX_POINTERS;
private startX = 0;
private startY = 0;
private offsetX = 0;
private offsetY = 0;
private lastX = 0;
private lastY = 0;
private stylusData: StylusData | undefined;
private activateAfterLongPress = 0;
private activationTimeout = 0;
private enableTrackpadTwoFingerGesture = false;
private endWheelTimeout = 0;
private wheelDevice = WheelDevice.UNDETERMINED;
public updateGestureConfig({ enabled = true, ...props }: Config): void {
this.resetConfig();
super.updateGestureConfig({ enabled: enabled, ...props });
this.checkCustomActivationCriteria(this.customActivationProperties);
if (this.config.minDist !== undefined) {
this.minDistSq = this.config.minDist * this.config.minDist;
} else if (this.hasCustomActivationCriteria) {
this.minDistSq = Number.MAX_SAFE_INTEGER;
}
if (this.config.minPointers !== undefined) {
this.minPointers = this.config.minPointers;
}
if (this.config.maxPointers !== undefined) {
this.maxPointers = this.config.maxPointers;
}
if (this.config.minVelocity !== undefined) {
this.minVelocityX = this.config.minVelocity;
this.minVelocityY = this.config.minVelocity;
}
if (this.config.minVelocityX !== undefined) {
this.minVelocityX = this.config.minVelocityX;
}
if (this.config.minVelocityY !== undefined) {
this.minVelocityY = this.config.minVelocityY;
}
if (this.config.activateAfterLongPress !== undefined) {
this.activateAfterLongPress = this.config.activateAfterLongPress;
}
if (this.config.activeOffsetXStart !== undefined) {
this.activeOffsetXStart = this.config.activeOffsetXStart;
if (this.config.activeOffsetXEnd === undefined) {
this.activeOffsetXEnd = Number.MAX_SAFE_INTEGER;
}
}
if (this.config.activeOffsetXEnd !== undefined) {
this.activeOffsetXEnd = this.config.activeOffsetXEnd;
if (this.config.activeOffsetXStart === undefined) {
this.activeOffsetXStart = Number.MIN_SAFE_INTEGER;
}
}
if (this.config.failOffsetXStart !== undefined) {
this.failOffsetXStart = this.config.failOffsetXStart;
if (this.config.failOffsetXEnd === undefined) {
this.failOffsetXEnd = Number.MAX_SAFE_INTEGER;
}
}
if (this.config.failOffsetXEnd !== undefined) {
this.failOffsetXEnd = this.config.failOffsetXEnd;
if (this.config.failOffsetXStart === undefined) {
this.failOffsetXStart = Number.MIN_SAFE_INTEGER;
}
}
if (this.config.activeOffsetYStart !== undefined) {
this.activeOffsetYStart = this.config.activeOffsetYStart;
if (this.config.activeOffsetYEnd === undefined) {
this.activeOffsetYEnd = Number.MAX_SAFE_INTEGER;
}
}
if (this.config.activeOffsetYEnd !== undefined) {
this.activeOffsetYEnd = this.config.activeOffsetYEnd;
if (this.config.activeOffsetYStart === undefined) {
this.activeOffsetYStart = Number.MIN_SAFE_INTEGER;
}
}
if (this.config.failOffsetYStart !== undefined) {
this.failOffsetYStart = this.config.failOffsetYStart;
if (this.config.failOffsetYEnd === undefined) {
this.failOffsetYEnd = Number.MAX_SAFE_INTEGER;
}
}
if (this.config.failOffsetYEnd !== undefined) {
this.failOffsetYEnd = this.config.failOffsetYEnd;
if (this.config.failOffsetYStart === undefined) {
this.failOffsetYStart = Number.MIN_SAFE_INTEGER;
}
}
if (this.config.enableTrackpadTwoFingerGesture !== undefined) {
this.enableTrackpadTwoFingerGesture =
this.config.enableTrackpadTwoFingerGesture;
}
}
protected resetConfig(): void {
super.resetConfig();
this.activeOffsetXStart = -Number.MAX_SAFE_INTEGER;
this.activeOffsetXEnd = Number.MIN_SAFE_INTEGER;
this.failOffsetXStart = Number.MIN_SAFE_INTEGER;
this.failOffsetXEnd = Number.MAX_SAFE_INTEGER;
this.activeOffsetYStart = Number.MAX_SAFE_INTEGER;
this.activeOffsetYEnd = Number.MIN_SAFE_INTEGER;
this.failOffsetYStart = Number.MIN_SAFE_INTEGER;
this.failOffsetYEnd = Number.MAX_SAFE_INTEGER;
this.minVelocityX = Number.MAX_SAFE_INTEGER;
this.minVelocityY = Number.MAX_SAFE_INTEGER;
this.minVelocitySq = Number.MAX_SAFE_INTEGER;
this.minDistSq = DEFAULT_MIN_DIST_SQ;
this.minPointers = DEFAULT_MIN_POINTERS;
this.maxPointers = DEFAULT_MAX_POINTERS;
this.activateAfterLongPress = 0;
}
protected transformNativeEvent() {
const translationX: number = this.getTranslationX();
const translationY: number = this.getTranslationY();
return {
...super.transformNativeEvent(),
translationX: isNaN(translationX) ? 0 : translationX,
translationY: isNaN(translationY) ? 0 : translationY,
velocityX: this.velocityX,
velocityY: this.velocityY,
stylusData: this.stylusData,
};
}
private getTranslationX(): number {
return this.lastX - this.startX + this.offsetX;
}
private getTranslationY(): number {
return this.lastY - this.startY + this.offsetY;
}
private clearActivationTimeout(): void {
clearTimeout(this.activationTimeout);
}
// Events Handling
protected onPointerDown(event: AdaptedEvent): void {
if (!this.isButtonInConfig(event.button)) {
return;
}
this.tracker.addToTracker(event);
this.stylusData = event.stylusData;
super.onPointerDown(event);
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
this.lastX = lastCoords.x;
this.lastY = lastCoords.y;
this.startX = this.lastX;
this.startY = this.lastY;
this.tryBegin(event);
this.checkBegan();
this.tryToSendTouchEvent(event);
}
protected onPointerAdd(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerAdd(event);
this.tryBegin(event);
this.offsetX += this.lastX - this.startX;
this.offsetY += this.lastY - this.startY;
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
this.lastX = lastCoords.x;
this.lastY = lastCoords.y;
this.startX = this.lastX;
this.startY = this.lastY;
if (this.tracker.getTrackedPointersCount() > this.maxPointers) {
if (this.currentState === State.ACTIVE) {
this.cancel();
} else {
this.fail();
}
} else {
this.checkBegan();
}
}
protected onPointerUp(event: AdaptedEvent): void {
this.stylusData = event.stylusData;
super.onPointerUp(event);
if (this.currentState === State.ACTIVE) {
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
this.lastX = lastCoords.x;
this.lastY = lastCoords.y;
}
this.tracker.removeFromTracker(event.pointerId);
if (this.tracker.getTrackedPointersCount() === 0) {
this.clearActivationTimeout();
}
if (this.currentState === State.ACTIVE) {
this.end();
} else {
this.resetProgress();
this.fail();
}
}
protected onPointerRemove(event: AdaptedEvent): void {
super.onPointerRemove(event);
this.tracker.removeFromTracker(event.pointerId);
this.offsetX += this.lastX - this.startX;
this.offsetY += this.lastY - this.startY;
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
this.lastX = lastCoords.x;
this.lastY = lastCoords.y;
this.startX = this.lastX;
this.startY = this.lastY;
if (
!(
this.currentState === State.ACTIVE &&
this.tracker.getTrackedPointersCount() < this.minPointers
)
) {
this.checkBegan();
}
}
protected onPointerMove(event: AdaptedEvent): void {
this.tracker.track(event);
this.stylusData = event.stylusData;
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
this.lastX = lastCoords.x;
this.lastY = lastCoords.y;
const velocity = this.tracker.getVelocity(event.pointerId);
this.velocityX = velocity.x;
this.velocityY = velocity.y;
this.checkBegan();
super.onPointerMove(event);
}
protected onPointerOutOfBounds(event: AdaptedEvent): void {
if (this.getShouldCancelWhenOutside()) {
return;
}
this.tracker.track(event);
this.stylusData = event.stylusData;
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
this.lastX = lastCoords.x;
this.lastY = lastCoords.y;
const velocity = this.tracker.getVelocity(event.pointerId);
this.velocityX = velocity.x;
this.velocityY = velocity.y;
this.checkBegan();
if (this.currentState === State.ACTIVE) {
super.onPointerOutOfBounds(event);
}
}
private scheduleWheelEnd(event: AdaptedEvent) {
clearTimeout(this.endWheelTimeout);
this.endWheelTimeout = setTimeout(() => {
if (this.currentState === State.ACTIVE) {
this.end();
this.tracker.removeFromTracker(event.pointerId);
this.currentState = State.UNDETERMINED;
}
this.wheelDevice = WheelDevice.UNDETERMINED;
}, 30);
}
protected onWheel(event: AdaptedEvent): void {
if (
this.wheelDevice === WheelDevice.MOUSE ||
!this.enableTrackpadTwoFingerGesture
) {
return;
}
if (this.currentState === State.UNDETERMINED) {
this.wheelDevice =
event.wheelDeltaY! % 120 !== 0
? WheelDevice.TOUCHPAD
: WheelDevice.MOUSE;
if (this.wheelDevice === WheelDevice.MOUSE) {
this.scheduleWheelEnd(event);
return;
}
this.tracker.addToTracker(event);
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
this.lastX = lastCoords.x;
this.lastY = lastCoords.y;
this.startX = this.lastX;
this.startY = this.lastY;
this.begin();
this.activate();
}
this.tracker.track(event);
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
this.lastX = lastCoords.x;
this.lastY = lastCoords.y;
const velocity = this.tracker.getVelocity(event.pointerId);
this.velocityX = velocity.x;
this.velocityY = velocity.y;
this.tryToSendMoveEvent(false, event);
this.scheduleWheelEnd(event);
}
private shouldActivate(): boolean {
const dx: number = this.getTranslationX();
if (
this.activeOffsetXStart !== Number.MAX_SAFE_INTEGER &&
dx < this.activeOffsetXStart
) {
return true;
}
if (
this.activeOffsetXEnd !== Number.MIN_SAFE_INTEGER &&
dx > this.activeOffsetXEnd
) {
return true;
}
const dy: number = this.getTranslationY();
if (
this.activeOffsetYStart !== Number.MAX_SAFE_INTEGER &&
dy < this.activeOffsetYStart
) {
return true;
}
if (
this.activeOffsetYEnd !== Number.MIN_SAFE_INTEGER &&
dy > this.activeOffsetYEnd
) {
return true;
}
const distanceSq: number = dx * dx + dy * dy;
if (
this.minDistSq !== Number.MAX_SAFE_INTEGER &&
distanceSq >= this.minDistSq
) {
return true;
}
const vx: number = this.velocityX;
if (
this.minVelocityX !== Number.MAX_SAFE_INTEGER &&
((this.minVelocityX < 0 && vx <= this.minVelocityX) ||
(this.minVelocityX >= 0 && this.minVelocityX <= vx))
) {
return true;
}
const vy: number = this.velocityY;
if (
this.minVelocityY !== Number.MAX_SAFE_INTEGER &&
((this.minVelocityY < 0 && vy <= this.minVelocityY) ||
(this.minVelocityY >= 0 && this.minVelocityY <= vy))
) {
return true;
}
const velocitySq: number = vx * vx + vy * vy;
return (
this.minVelocitySq !== Number.MAX_SAFE_INTEGER &&
velocitySq >= this.minVelocitySq
);
}
private shouldFail(): boolean {
const dx: number = this.getTranslationX();
const dy: number = this.getTranslationY();
const distanceSq = dx * dx + dy * dy;
if (this.activateAfterLongPress > 0 && distanceSq > DEFAULT_MIN_DIST_SQ) {
this.clearActivationTimeout();
return true;
}
if (
this.failOffsetXStart !== Number.MIN_SAFE_INTEGER &&
dx < this.failOffsetXStart
) {
return true;
}
if (
this.failOffsetXEnd !== Number.MAX_SAFE_INTEGER &&
dx > this.failOffsetXEnd
) {
return true;
}
if (
this.failOffsetYStart !== Number.MIN_SAFE_INTEGER &&
dy < this.failOffsetYStart
) {
return true;
}
return (
this.failOffsetYEnd !== Number.MAX_SAFE_INTEGER &&
dy > this.failOffsetYEnd
);
}
private tryBegin(event: AdaptedEvent): void {
if (
this.currentState === State.UNDETERMINED &&
this.tracker.getTrackedPointersCount() >= this.minPointers
) {
this.resetProgress();
this.offsetX = 0;
this.offsetY = 0;
this.velocityX = 0;
this.velocityY = 0;
this.begin();
if (this.activateAfterLongPress > 0) {
this.activationTimeout = setTimeout(() => {
this.activate();
}, this.activateAfterLongPress);
}
} else {
const velocity = this.tracker.getVelocity(event.pointerId);
this.velocityX = velocity.x;
this.velocityY = velocity.y;
}
}
private checkBegan(): void {
if (this.currentState === State.BEGAN) {
if (this.shouldFail()) {
this.fail();
} else if (this.shouldActivate()) {
this.activate();
}
}
}
public activate(force = false): void {
if (this.currentState !== State.ACTIVE) {
this.resetProgress();
}
super.activate(force);
}
protected onCancel(): void {
this.clearActivationTimeout();
}
protected onReset(): void {
this.clearActivationTimeout();
}
protected resetProgress(): void {
if (this.currentState === State.ACTIVE) {
return;
}
this.startX = this.lastX;
this.startY = this.lastY;
}
}