-
-
Notifications
You must be signed in to change notification settings - Fork 978
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
base: main
Are you sure you want to change the base?
Changes from all commits
02e670f
452b3e6
d21e1a3
ad5681e
080e797
f4d2f7d
a640c0c
09a4dc1
dc59483
ce259ea
bf008b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||
}, | ||
}); |
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'; | ||
|
||
|
@@ -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); | ||
} | ||
|
@@ -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 { | ||
|
@@ -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); | ||
} | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we don't schedule cleanup, wheelDevice won't be set to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
||
|
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(); | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.