-
-
Notifications
You must be signed in to change notification settings - Fork 980
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[web] Add support for two finger pan (#3163)
## Description This PR adds support for two finger panning on touchpad on `web`. >[!WARNING] > Two finger gestures may be used as system/browser gestures (for example `swipe` to go back). This PR doesn't handle these cases. ## Implementation Implementation is based on [WheelEvents](https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event). This leads to some limitations - whole state flow of `Pan` has to be managed inside one callback (`onWheel`). ### Ending gesture Starting gesture is easy, in contrast to ending it. To finish gesture we use `timeout` - if no `wheel event` was received since `setTimeout` was called, we can end gesture. ### `Mouse` vs `Touchpad` It is hard to determine whether `mouse` or `touchpad` was used to start the gesture. `WheelEvent` doesn't have any information about the device, therefore we have to use heuristics to check that. >[!NOTE] > You cannot start gesture with mouse scroll. To see if events were generated with mouse, we check whether `wheelDeltaY` property (which is now [deprecated](https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event#event_properties)) of event is multiple of `120`. In short, this is standard `wheel delta` for mouse. Here you can find useful links that will tell more about why it works: - https://stackoverflow.com/questions/10744645/detect-touchpad-vs-mouse-in-javascript - https://devblogs.microsoft.com/oldnewthing/20130123-00/?p=5473 >[!CAUTION] > While this will work most of the times, it is possible that user will somehow generate this specific `wheel delta` with touchpad and gesture will not be recognized correctly. Closes #800 ## Test plan Tested on new _**Two finger Pan**_ example
- Loading branch information
Showing
8 changed files
with
197 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |