Skip to content

Commit 2d6a20d

Browse files
authored
[Web] Continuous Pinch and Rotation (#4155)
## Description This PR changes `Pinch` and `Rotation` on web so that they're not cancelled after releasing one pointer. ## Test plan Tested on Transformations example. <details> <summary>Also tested on the following code:</summary> ```tsx import React from 'react'; import { StyleSheet, View } from 'react-native'; import { GestureDetector, usePinchGesture, useRotationGesture, useSimultaneousGestures, } from 'react-native-gesture-handler'; import Animated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; export default function EmptyExample() { const scale = useSharedValue(1); const rotation = useSharedValue(0); const pinch = usePinchGesture({ onBegin: () => { console.log('[Pinch] onBegin'); }, onActivate: () => { console.log('[Pinch] onActivate'); }, onUpdate: (e) => { console.log('[Pinch] onUpdate scaleChange:', e.scaleChange); scale.value *= e.scaleChange; }, onDeactivate: () => { console.log('[Pinch] onDeactivate'); }, onFinalize: () => { console.log('[Pinch] onFinalize'); }, }); const rotate = useRotationGesture({ onBegin: () => { console.log('[Rotation] onBegin'); }, onActivate: () => { console.log('[Rotation] onActivate'); }, onUpdate: (e) => { console.log('[Rotation] onUpdate rotationChange:', e.rotationChange); rotation.value += e.rotationChange; }, onDeactivate: () => { console.log('[Rotation] onDeactivate'); }, onFinalize: () => { console.log('[Rotation] onFinalize'); }, }); const gesture = useSimultaneousGestures(pinch, rotate); const animatedStyle = useAnimatedStyle(() => ({ transform: [ { scale: scale.value }, { rotate: `${(rotation.value * 180) / Math.PI}deg` }, ], })); return ( <View style={styles.container}> <GestureDetector gesture={gesture}> <Animated.View style={[styles.circle, animatedStyle]}> <View style={styles.indicator} /> </Animated.View> </GestureDetector> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, circle: { width: 300, height: 300, borderRadius: 150, backgroundColor: '#b58df1', justifyContent: 'center', alignItems: 'center', }, indicator: { width: 6, height: '50%', borderRadius: 3, backgroundColor: 'rgba(255, 255, 255, 0.5)', position: 'absolute', top: 0, }, }); ``` </details>
1 parent f1f09bc commit 2d6a20d

4 files changed

Lines changed: 34 additions & 20 deletions

File tree

packages/react-native-gesture-handler/src/web/detectors/RotationGestureDetector.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,23 @@ export default class RotationGestureDetector
8484
this.onRotationEnd(this);
8585
}
8686

87-
private setKeyPointers(tracker: PointerTracker): void {
87+
private setKeyPointers(tracker: PointerTracker, excludeId?: number): void {
8888
if (this.keyPointers[0] && this.keyPointers[1]) {
8989
return;
9090
}
9191

92-
const pointerIDs: IterableIterator<number> = tracker.trackedPointers.keys();
92+
let assigned = 0;
9393

94-
this.keyPointers[0] = pointerIDs.next().value as number;
95-
this.keyPointers[1] = pointerIDs.next().value as number;
94+
for (const id of tracker.trackedPointers.keys()) {
95+
if (id === excludeId) {
96+
continue;
97+
}
98+
99+
this.keyPointers[assigned++] = id;
100+
if (assigned === 2) {
101+
break;
102+
}
103+
}
96104
}
97105

98106
public onTouchEvent(event: AdaptedEvent, tracker: PointerTracker): boolean {
@@ -132,7 +140,13 @@ export default class RotationGestureDetector
132140
}
133141

134142
if (this.keyPointers.indexOf(event.pointerId) >= 0) {
135-
this.finish();
143+
if (tracker.trackedPointersCount <= 2) {
144+
this.reset();
145+
} else {
146+
this.keyPointers = [NaN, NaN];
147+
this.setKeyPointers(tracker, event.pointerId);
148+
this.previousAngle = NaN;
149+
}
136150
}
137151

138152
break;

packages/react-native-gesture-handler/src/web/detectors/ScaleGestureDetector.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,16 @@ export default class ScaleGestureDetector implements ScaleGestureListener {
4444
const action: EventTypes = event.eventType;
4545
const numOfPointers = tracker.trackedPointersCount;
4646

47+
// When the last second pointer lifts (going down to 1), pause without
48+
// touching span/time state so the gesture resumes cleanly on re-add.
49+
// When 3+ → 2+ pointers, fall through so configChanged resets the span
50+
// baseline to the remaining pointer set and avoids a scale jump.
51+
if (action === EventTypes.ADDITIONAL_POINTER_UP && numOfPointers <= 2) {
52+
return true;
53+
}
54+
4755
const streamComplete: boolean =
48-
action === EventTypes.UP ||
49-
action === EventTypes.ADDITIONAL_POINTER_UP ||
50-
action === EventTypes.CANCEL;
56+
action === EventTypes.UP || action === EventTypes.CANCEL;
5157

5258
if (action === EventTypes.DOWN || streamComplete) {
5359
if (this.inProgress) {

packages/react-native-gesture-handler/src/web/handlers/PinchGestureHandler.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ export default class PinchGestureHandler extends GestureHandler {
9191
protected override onPointerAdd(event: AdaptedEvent): void {
9292
this.tracker.addToTracker(event);
9393
super.onPointerAdd(event);
94-
this.tryBegin();
9594
this.scaleGestureDetector.onTouchEvent(event, this.tracker);
95+
this.tryBegin();
9696
}
9797

9898
protected override onPointerUp(event: AdaptedEvent): void {
@@ -113,26 +113,22 @@ export default class PinchGestureHandler extends GestureHandler {
113113
super.onPointerRemove(event);
114114
this.scaleGestureDetector.onTouchEvent(event, this.tracker);
115115
this.tracker.removeFromTracker(event.pointerId);
116-
117-
if (this.state === State.ACTIVE && this.tracker.trackedPointersCount < 2) {
118-
this.end();
119-
}
120116
}
121117

122118
protected override onPointerMove(event: AdaptedEvent): void {
119+
this.tracker.track(event);
123120
if (this.tracker.trackedPointersCount < 2) {
124121
return;
125122
}
126-
this.tracker.track(event);
127123

128124
this.scaleGestureDetector.onTouchEvent(event, this.tracker);
129125
super.onPointerMove(event);
130126
}
131127
protected override onPointerOutOfBounds(event: AdaptedEvent): void {
128+
this.tracker.track(event);
132129
if (this.tracker.trackedPointersCount < 2) {
133130
return;
134131
}
135-
this.tracker.track(event);
136132

137133
this.scaleGestureDetector.onTouchEvent(event, this.tracker);
138134
super.onPointerOutOfBounds(event);

packages/react-native-gesture-handler/src/web/handlers/RotationGestureHandler.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,12 @@ export default class RotationGestureHandler extends GestureHandler {
100100
this.tracker.addToTracker(event);
101101
super.onPointerAdd(event);
102102

103-
this.tryBegin();
104103
this.rotationGestureDetector.onTouchEvent(event, this.tracker);
104+
this.tryBegin();
105105
}
106106

107107
protected override onPointerMove(event: AdaptedEvent): void {
108+
this.tracker.track(event);
108109
if (this.tracker.trackedPointersCount < 2) {
109110
return;
110111
}
@@ -113,14 +114,13 @@ export default class RotationGestureHandler extends GestureHandler {
113114
this.cachedAnchorX = anchor.x;
114115
this.cachedAnchorY = anchor.y;
115116

116-
this.tracker.track(event);
117-
118117
this.rotationGestureDetector.onTouchEvent(event, this.tracker);
119118

120119
super.onPointerMove(event);
121120
}
122121

123122
protected override onPointerOutOfBounds(event: AdaptedEvent): void {
123+
this.tracker.track(event);
124124
if (this.tracker.trackedPointersCount < 2) {
125125
return;
126126
}
@@ -129,8 +129,6 @@ export default class RotationGestureHandler extends GestureHandler {
129129
this.cachedAnchorX = anchor.x;
130130
this.cachedAnchorY = anchor.y;
131131

132-
this.tracker.track(event);
133-
134132
this.rotationGestureDetector.onTouchEvent(event, this.tracker);
135133

136134
super.onPointerOutOfBounds(event);

0 commit comments

Comments
 (0)