Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[web] Add support for two finger pan #3163

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import PointerType from './src/release_tests/pointerType';
import SwipeableReanimation from './src/release_tests/swipeableReanimation';
import NestedGestureHandlerRootViewWithModal from './src/release_tests/nestedGHRootViewWithModal';
import TwoFingerPan from 'src/release_tests/twoFingerPan';
import { PinchableBox } from './src/recipes/scaleAndRotate';
import PanAndScroll from './src/recipes/panAndScroll';
import { BottomSheet } from './src/showcase/bottomSheet';
Expand Down Expand Up @@ -209,6 +210,11 @@
unsupportedPlatforms: new Set(['android', 'ios', 'macos']),
},
{ name: 'Stylus data', component: StylusData },
{
name: 'Two finger Pan',
component: TwoFingerPan,
unsupportedPlatforms: new Set(['android', 'macos']),
},
],
},
{
Expand Down Expand Up @@ -313,7 +319,7 @@
renderSectionHeader={({ section: { sectionTitle } }) => (
<Text style={styles.sectionTitle}>{sectionTitle}</Text>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}

Check warning on line 322 in example/App.tsx

View workflow job for this annotation

GitHub Actions / check (example)

Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component “MainScreen” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true
/>
</SafeAreaView>
);
Expand Down
55 changes: 55 additions & 0 deletions example/src/release_tests/twoFingerPan/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { StyleSheet, View } from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';

import React from 'react';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

const BOX_SIZE = 270;

const clampColor = (v: number) => Math.min(255, Math.max(0, v));

export default function TwoFingerPan() {
const r = useSharedValue(128);
const b = useSharedValue(128);

const pan = Gesture.Pan()
.onChange((event) => {
r.value = clampColor(r.value - event.changeY);
b.value = clampColor(b.value + event.changeX);
})
.runOnJS(true)
.enableTrackpadTwoFingerGesture(true);

const animatedStyles = useAnimatedStyle(() => {
const backgroundColor = `rgb(${r.value}, 128, ${b.value})`;

return {
backgroundColor,
};
});

return (
<View style={styles.container} collapsable={false}>
<GestureDetector gesture={pan}>
<Animated.View style={[styles.box, animatedStyles]} />
</GestureDetector>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
height: '100%',
},
box: {
width: BOX_SIZE,
height: BOX_SIZE,
borderRadius: BOX_SIZE / 2,
},
});
6 changes: 5 additions & 1 deletion src/web/handlers/GestureHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default abstract class GestureHandler implements IGestureHandler {
manager.setOnPointerOutOfBounds(this.onPointerOutOfBounds.bind(this));
manager.setOnPointerMoveOver(this.onPointerMoveOver.bind(this));
manager.setOnPointerMoveOut(this.onPointerMoveOut.bind(this));
manager.setOnWheel(this.onWheel.bind(this));

manager.registerListeners();
}
Expand Down Expand Up @@ -338,7 +339,10 @@ export default abstract class GestureHandler implements IGestureHandler {
protected onPointerMoveOut(_event: AdaptedEvent): void {
// Used only by hover gesture handler atm
}
private tryToSendMoveEvent(out: boolean, event: AdaptedEvent): void {
protected onWheel(_event: AdaptedEvent): void {
// Used only by pan gesture handler
}
protected tryToSendMoveEvent(out: boolean, event: AdaptedEvent): void {
if ((out && this.shouldCancelWhenOutside) || !this.enabled) {
return;
}
Expand Down
70 changes: 69 additions & 1 deletion src/web/handlers/PanGestureHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { State } from '../../State';
import { DEFAULT_TOUCH_SLOP } from '../constants';
import { AdaptedEvent, Config, StylusData } from '../interfaces';
import { AdaptedEvent, Config, StylusData, WheelDevice } from '../interfaces';

import GestureHandler from './GestureHandler';

Expand Down Expand Up @@ -57,6 +57,10 @@ export default class PanGestureHandler extends GestureHandler {
private activateAfterLongPress = 0;
private activationTimeout = 0;

private enableTrackpadTwoFingerGesture = false;
private endWheelTimeout = 0;
private wheelDevice = WheelDevice.UNDETERMINED;

public init(ref: number, propsRef: React.RefObject<unknown>): void {
super.init(ref, propsRef);
}
Expand Down Expand Up @@ -161,6 +165,11 @@ export default class PanGestureHandler extends GestureHandler {
this.failOffsetYStart = Number.MIN_SAFE_INTEGER;
}
}

if (this.config.enableTrackpadTwoFingerGesture !== undefined) {
this.enableTrackpadTwoFingerGesture =
this.config.enableTrackpadTwoFingerGesture;
}
}

protected resetConfig(): void {
Expand Down Expand Up @@ -351,6 +360,65 @@ export default class PanGestureHandler extends GestureHandler {
}
}

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be configurable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, should it? I was to move it to a constant but it seems that I've missed it. Either way, do we want to give users permission to change this value? I think we can stick to one that works ok, but leaving it open for customization also makes sense.

}

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Do we need to schedule in this case instead of ending outright?
  2. Do we need to end here at all? begin is called after this condition, so we shouldn't even get there with mouse, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't schedule cleanup, wheelDevice won't be set to UNDETERMINED. I'm not sure if we want to set it here though. However, we can call end only if handler was active.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also changed in a640c0c

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();

Expand Down
8 changes: 8 additions & 0 deletions src/web/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export interface Config extends Record<string, ConfigArgs> {
shouldActivateOnStart?: boolean;
disallowInterruption?: boolean;
direction?: Directions;
enableTrackpadTwoFingerGesture?: boolean;
}

type NativeEventArgs = number | State | boolean | undefined;
Expand Down Expand Up @@ -151,6 +152,7 @@ export interface AdaptedEvent {
time: number;
button?: MouseButton;
stylusData?: StylusData;
wheelDeltaY?: number;
}

export enum EventTypes {
Expand All @@ -171,3 +173,9 @@ export enum TouchEventType {
UP,
CANCELLED,
}

export enum WheelDevice {
UNDETERMINED,
MOUSE,
TOUCHPAD,
}
4 changes: 4 additions & 0 deletions src/web/tools/EventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default abstract class EventManager<T> {
protected onPointerOutOfBounds(_event: AdaptedEvent): void {}
protected onPointerMoveOver(_event: AdaptedEvent): void {}
protected onPointerMoveOut(_event: AdaptedEvent): void {}
protected onWheel(_event: AdaptedEvent): void {}

public setOnPointerDown(callback: PointerEventCallback): void {
this.onPointerDown = callback;
Expand Down Expand Up @@ -71,6 +72,9 @@ export default abstract class EventManager<T> {
public setOnPointerMoveOut(callback: PointerEventCallback): void {
this.onPointerMoveOut = callback;
}
public setOnWheel(callback: PointerEventCallback): void {
this.onWheel = callback;
}

protected markAsInBounds(pointerId: number): void {
if (this.pointersInBounds.indexOf(pointerId) >= 0) {
Expand Down
2 changes: 2 additions & 0 deletions src/web/tools/GestureHandlerWebDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import EventManager from './EventManager';
import { Config } from '../interfaces';
import { MouseButton } from '../../handlers/gestureHandlerCommon';
import KeyboardEventManager from './KeyboardEventManager';
import WheelEventManager from './WheelEventManager';

interface DefaultViewStyles {
userSelect: string;
Expand Down Expand Up @@ -58,6 +59,7 @@ export class GestureHandlerWebDelegate

this.eventManagers.push(new PointerEventManager(this.view));
this.eventManagers.push(new KeyboardEventManager(this.view));
this.eventManagers.push(new WheelEventManager(this.view));

this.eventManagers.forEach((manager) =>
this.gestureHandler.attachEventManager(manager)
Expand Down
48 changes: 48 additions & 0 deletions src/web/tools/WheelEventManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import EventManager from './EventManager';
import { AdaptedEvent, EventTypes } from '../interfaces';
import { PointerType } from '../../PointerType';

export default class WheelEventManager extends EventManager<HTMLElement> {
private wheelDelta = { x: 0, y: 0 };

private resetDelta = (_event: PointerEvent) => {
this.wheelDelta = { x: 0, y: 0 };
};

private wheelCallback = (event: WheelEvent) => {
this.wheelDelta.x += event.deltaX;
this.wheelDelta.y += event.deltaY;

const adaptedEvent = this.mapEvent(event);
this.onWheel(adaptedEvent);
};

public registerListeners(): void {
this.view.addEventListener('pointermove', this.resetDelta);
this.view.addEventListener('wheel', this.wheelCallback);
}

public unregisterListeners(): void {
this.view.removeEventListener('pointermove', this.resetDelta);
this.view.removeEventListener('wheel', this.wheelCallback);
}

protected mapEvent(event: WheelEvent): AdaptedEvent {
return {
x: event.clientX + this.wheelDelta.x,
y: event.clientY + this.wheelDelta.y,
offsetX: event.offsetX - event.deltaX,
offsetY: event.offsetY - event.deltaY,
pointerId: -1,
eventType: EventTypes.MOVE,
pointerType: PointerType.OTHER,
time: event.timeStamp,
// @ts-ignore It does exist, but it's deprecated
wheelDeltaY: event.wheelDeltaY,
};
}

public resetManager(): void {
super.resetManager();
}
}
Loading