Skip to content

Commit 99daffa

Browse files
authored
[Android] Remove workaround for touch events allowing to change state too soon (#3793)
## Description Supersedes #2283 On Android, `onTouchesDown` was delayed to be dispatched after `onBegin` so that the handlers had the time to initialize themselves (which happens in `onHandle`) so that the non-touch events would have correct data. The old approach was saving the event, which triggered the touch event, and using that event to initialize the handler during imperative state change. The issues with that approach were: 1. Storing the event for the duration of the event handling 2. It wouldn't work with asynchronous state changes, which are now possible The new approach is to add an option to force gestures to initialize regardless of the state they are in. This way, when the state is updated and the current state of the handler is `UNDETERMINED`, the flag is set on the handler, and when it starts handling the event, it will first initialize itself. ## Test plan Tested on the following snippet, with affected gestures ``` import React from 'react'; import { StyleSheet, View } from 'react-native'; import { usePan, NativeDetector } from 'react-native-gesture-handler'; export default function EmptyExample() { const pan = usePan({ onTouchesDown: () => { 'worklet'; console.log('Touch down'); globalThis._setGestureStateSync(57, 4); }, onTouchesMove: () => { 'worklet'; console.log('Touch move'); }, onTouchesUp: () => { 'worklet'; console.log('Touch up'); }, onTouchesCancelled: () => { 'worklet'; console.log('Touch cancel'); }, onBegin: () => { 'worklet'; console.log('Gesture begin'); }, onStart: (e) => { 'worklet'; console.log('Gesture start'); }, onUpdate: (e) => { 'worklet'; console.log('Gesture update', e.handlerData.translationX, e.handlerData.translationY); }, onEnd: () => { 'worklet'; console.log('Gesture end'); }, onFinalize: () => { 'worklet'; console.log('Gesture finalize'); }, }); console.log('Rendering EmptyExamplee', pan.tag); return ( <View style={styles.container}> <NativeDetector gesture={pan}> <View style={{ width: 300, height: 300, backgroundColor: 'green' }} /> </NativeDetector> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, }); ```
1 parent 2d739ec commit 99daffa

File tree

8 files changed

+85
-42
lines changed

8 files changed

+85
-42
lines changed

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ open class GestureHandler {
8282
}
8383
var actionType = 0
8484

85+
var forceReinitializeDuringOnHandle = false
8586
var changedTouchesPayload: WritableArray? = null
8687
private set
8788
var allTouchesPayload: WritableArray? = null
@@ -755,6 +756,7 @@ open class GestureHandler {
755756
// if the handler is waiting for failure of other one)
756757
open fun resetProgress() {}
757758

759+
protected open fun initialize(event: MotionEvent, sourceEvent: MotionEvent) {}
758760
protected open fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
759761
moveToState(STATE_FAILED)
760762
}

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -287,15 +287,7 @@ class GestureHandlerOrchestrator(
287287
val action = sourceEvent.actionMasked
288288
val event = transformEventToViewCoords(handler.view, MotionEvent.obtain(sourceEvent))
289289

290-
// Touch events are sent before the handler itself has a chance to process them,
291-
// mainly because `onTouchesUp` should be send before gesture finishes. This means that
292-
// the first `onTouchesDown` event is sent before a gesture begins, activation in
293-
// callback for this event causes problems because the handler doesn't have a chance
294-
// to initialize itself with starting values of pointer (in pan this causes translation
295-
// to be equal to the coordinates of the pointer). The simplest solution is to send
296-
// the first `onTouchesDown` event after the handler processes it and changes state
297-
// to `BEGAN`.
298-
if (handler.needsPointerData && handler.state != 0) {
290+
if (handler.needsPointerData) {
299291
handler.updatePointerData(event, sourceEvent)
300292
}
301293

@@ -317,10 +309,6 @@ class GestureHandlerOrchestrator(
317309
handler.dispatchHandlerUpdate(event)
318310
}
319311

320-
if (handler.needsPointerData && isFirstEvent) {
321-
handler.updatePointerData(event, sourceEvent)
322-
}
323-
324312
// if event was of type UP or POINTER_UP we request handler to stop tracking now that
325313
// the event has been dispatched
326314
if (action == MotionEvent.ACTION_UP ||

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/LongPressGestureHandler.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,23 @@ class LongPressGestureHandler(context: Context) : GestureHandler() {
6565
return Pair(x, y)
6666
}
6767

68+
override fun initialize(event: MotionEvent, sourceEvent: MotionEvent) {
69+
previousTime = SystemClock.uptimeMillis()
70+
startTime = previousTime
71+
}
72+
6873
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
6974
if (!shouldActivateWithMouse(sourceEvent)) {
7075
return
7176
}
7277

78+
if (forceReinitializeDuringOnHandle) {
79+
forceReinitializeDuringOnHandle = false
80+
initialize(event, sourceEvent)
81+
}
82+
7383
if (state == STATE_UNDETERMINED) {
74-
previousTime = SystemClock.uptimeMillis()
75-
startTime = previousTime
84+
initialize(event, sourceEvent)
7685
begin()
7786

7887
val (x, y) = getAverageCoords(sourceEvent)

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/PanGestureHandler.kt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,25 @@ class PanGestureHandler(context: Context?) : GestureHandler() {
148148
return failOffsetYEnd != MIN_VALUE_IGNORE && dy > failOffsetYEnd
149149
}
150150

151+
override fun initialize(event: MotionEvent, sourceEvent: MotionEvent) {
152+
resetProgress()
153+
offsetX = 0f
154+
offsetY = 0f
155+
velocityX = 0f
156+
velocityY = 0f
157+
velocityTracker = VelocityTracker.obtain()
158+
}
159+
151160
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
152161
if (!shouldActivateWithMouse(sourceEvent)) {
153162
return
154163
}
155164

165+
if (forceReinitializeDuringOnHandle) {
166+
forceReinitializeDuringOnHandle = false
167+
initialize(event, sourceEvent)
168+
}
169+
156170
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
157171
stylusData = StylusData.fromEvent(event)
158172
}
@@ -174,12 +188,7 @@ class PanGestureHandler(context: Context?) : GestureHandler() {
174188
lastY = getLastPointerY(sourceEvent, averageTouches)
175189
}
176190
if (state == STATE_UNDETERMINED && sourceEvent.pointerCount >= minPointers) {
177-
resetProgress()
178-
offsetX = 0f
179-
offsetY = 0f
180-
velocityX = 0f
181-
velocityY = 0f
182-
velocityTracker = VelocityTracker.obtain()
191+
initialize(event, sourceEvent)
183192
addVelocityMovement(velocityTracker, sourceEvent)
184193
begin()
185194

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/PinchGestureHandler.kt

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,26 @@ class PinchGestureHandler : GestureHandler() {
4949
}
5050
}
5151

52-
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
53-
if (state == STATE_UNDETERMINED) {
54-
val context = view!!.context
55-
resetProgress()
56-
scaleGestureDetector = ScaleGestureDetector(context, gestureListener)
57-
val configuration = ViewConfiguration.get(context)
58-
spanSlop = configuration.scaledTouchSlop.toFloat()
52+
override fun initialize(event: MotionEvent, sourceEvent: MotionEvent) {
53+
val context = view!!.context
54+
resetProgress()
55+
scaleGestureDetector = ScaleGestureDetector(context, gestureListener)
56+
val configuration = ViewConfiguration.get(context)
57+
spanSlop = configuration.scaledTouchSlop.toFloat()
5958

60-
// set the focal point to the position of the first pointer as NaN causes the event not to arrive
61-
this.focalPointX = event.x
62-
this.focalPointY = event.y
59+
// set the focal point to the position of the first pointer as NaN causes the event not to arrive
60+
this.focalPointX = event.x
61+
this.focalPointY = event.y
62+
}
6363

64+
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
65+
if (forceReinitializeDuringOnHandle) {
66+
forceReinitializeDuringOnHandle = false
67+
initialize(event, sourceEvent)
68+
}
69+
70+
if (state == STATE_UNDETERMINED) {
71+
initialize(event, sourceEvent)
6472
begin()
6573
}
6674
scaleGestureDetector?.onTouchEvent(sourceEvent)

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/RotationGestureHandler.kt

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,23 @@ class RotationGestureHandler : GestureHandler() {
3939
}
4040
}
4141

42-
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
43-
if (state == STATE_UNDETERMINED) {
44-
resetProgress()
45-
rotationGestureDetector = RotationGestureDetector(gestureListener)
42+
override fun initialize(event: MotionEvent, sourceEvent: MotionEvent) {
43+
resetProgress()
44+
rotationGestureDetector = RotationGestureDetector(gestureListener)
4645

47-
// set the anchor to the position of the first pointer as NaN causes the event not to arrive
48-
this.anchorX = event.x
49-
this.anchorY = event.y
46+
// set the anchor to the position of the first pointer as NaN causes the event not to arrive
47+
this.anchorX = event.x
48+
this.anchorY = event.y
49+
}
5050

51+
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
52+
if (forceReinitializeDuringOnHandle) {
53+
forceReinitializeDuringOnHandle = false
54+
initialize(event, sourceEvent)
55+
}
56+
57+
if (state == STATE_UNDETERMINED) {
58+
initialize(event, sourceEvent)
5159
begin()
5260
}
5361
rotationGestureDetector?.onTouchEvent(sourceEvent)

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/TapGestureHandler.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,27 @@ class TapGestureHandler : GestureHandler() {
8181
return maxDist != MAX_VALUE_IGNORE && dist > maxDist * maxDist
8282
}
8383

84+
override fun initialize(event: MotionEvent, sourceEvent: MotionEvent) {
85+
offsetX = 0f
86+
offsetY = 0f
87+
startX = getLastPointerX(sourceEvent, true)
88+
startY = getLastPointerY(sourceEvent, true)
89+
}
90+
8491
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
8592
if (!shouldActivateWithMouse(sourceEvent)) {
8693
return
8794
}
8895

96+
if (forceReinitializeDuringOnHandle) {
97+
forceReinitializeDuringOnHandle = false
98+
initialize(event, sourceEvent)
99+
}
100+
89101
val state = state
90102
val action = sourceEvent.actionMasked
91103
if (state == STATE_UNDETERMINED) {
92-
offsetX = 0f
93-
offsetY = 0f
94-
startX = getLastPointerX(sourceEvent, true)
95-
startY = getLastPointerY(sourceEvent, true)
104+
initialize(event, sourceEvent)
96105
}
97106
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN) {
98107
offsetX += lastX - startX

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,16 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) :
140140
UiThreadUtil.assertOnUiThread()
141141

142142
registry.getHandler(handlerTag)?.let { handler ->
143+
if (handler.state == GestureHandler.STATE_UNDETERMINED) {
144+
handler.forceReinitializeDuringOnHandle = true
145+
146+
// When going from UNDETERMINED to ACTIVE, force going through BEGAN to preserve
147+
// the correct state flow
148+
if (newState == GestureHandler.STATE_ACTIVE) {
149+
handler.begin()
150+
}
151+
}
152+
143153
when (newState) {
144154
GestureHandler.STATE_ACTIVE -> handler.activate(force = true)
145155
GestureHandler.STATE_BEGAN -> handler.begin()

0 commit comments

Comments
 (0)