Skip to content

Commit 8a4d05d

Browse files
authored
[Android] Initialize the Handler fields eagerly (#4461)
## Description `Tap`, `LongPress`, `Fling`, `Pan` and `Hover` gesture handlers kept their `Handler` as a nullable field created on first use, guarded by null checks and `!!` at every call site (with a `TODO: lazy init` left in `Tap` since the Kotlin conversion). `Handler` object was never cleared anyway (except `LongPress`, where it used to serve as a flag, and `Hover`). Also adds a comment explaining the 4 ms delay in `HoverGestureHandler`. ## Test plan - `:react-native-gesture-handler:compileDebugKotlin` builds clean in basic-example - `yarn format:android` passes <details> <summary>Tested on the following code:</summary> ```tsx import React, { useCallback, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { Directions, GestureDetector, useFlingGesture, useHoverGesture, useLongPressGesture, usePanGesture, useTapGesture, } from 'react-native-gesture-handler'; import { scheduleOnRN } from 'react-native-worklets'; type Counters = { begin: number; act: number; end: number; fail: number }; type Results = Record<string, Counters>; const EMPTY: Counters = { begin: 0, act: 0, end: 0, fail: 0 }; export default function EmptyExample() { const [results, setResults] = useState<Results>({}); const report = useCallback((key: string, kind: keyof Counters) => { setResults((prev) => { const current = prev[key] ?? EMPTY; return { ...prev, [key]: { ...current, [kind]: current[kind] + 1 } }; }); }, []); const tap = useTapGesture({ onBegin: () => scheduleOnRN(report, 'tap', 'begin'), onActivate: () => scheduleOnRN(report, 'tap', 'act'), onFinalize: (e) => scheduleOnRN(report, 'tap', e.canceled ? 'fail' : 'end'), }); const doubleTap = useTapGesture({ numberOfTaps: 2, onBegin: () => scheduleOnRN(report, 'doubleTap', 'begin'), onActivate: () => scheduleOnRN(report, 'doubleTap', 'act'), onFinalize: (e) => scheduleOnRN(report, 'doubleTap', e.canceled ? 'fail' : 'end'), }); const longPress = useLongPressGesture({ minDurationMs: 400, onBegin: () => scheduleOnRN(report, 'longPress', 'begin'), onActivate: () => scheduleOnRN(report, 'longPress', 'act'), onFinalize: (e) => scheduleOnRN(report, 'longPress', e.canceled ? 'fail' : 'end'), }); const fling = useFlingGesture({ direction: Directions.RIGHT, onBegin: () => scheduleOnRN(report, 'fling', 'begin'), onActivate: () => scheduleOnRN(report, 'fling', 'act'), onFinalize: (e) => scheduleOnRN(report, 'fling', e.canceled ? 'fail' : 'end'), }); const pan = usePanGesture({ activateAfterLongPress: 400, onBegin: () => scheduleOnRN(report, 'pan', 'begin'), onActivate: () => scheduleOnRN(report, 'pan', 'act'), onFinalize: (e) => scheduleOnRN(report, 'pan', e.canceled ? 'fail' : 'end'), }); const hover = useHoverGesture({ onBegin: () => scheduleOnRN(report, 'hover', 'begin'), onActivate: () => scheduleOnRN(report, 'hover', 'act'), onFinalize: (e) => scheduleOnRN(report, 'hover', e.canceled ? 'fail' : 'end'), }); const rows: { key: string; label: string; gesture: any }[] = [ { key: 'tap', label: 'Tap', gesture: tap }, { key: 'doubleTap', label: 'DoubleTap', gesture: doubleTap }, { key: 'longPress', label: 'LongPress 400ms', gesture: longPress }, { key: 'fling', label: 'Fling right', gesture: fling }, { key: 'pan', label: 'Pan holdActivate 400ms', gesture: pan }, { key: 'hover', label: 'Hover', gesture: hover }, ]; return ( <View style={styles.container}> {rows.map(({ key, label, gesture }) => { const c = results[key] ?? EMPTY; return ( <GestureDetector key={key} gesture={gesture}> <View style={styles.box}> <Text style={styles.label}>{label}</Text> <Text style={styles.status}> {`${key} b:${c.begin} a:${c.act} e:${c.end} f:${c.fail}`} </Text> </View> </GestureDetector> ); })} </View> ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 12, gap: 10, }, box: { height: 88, borderRadius: 12, backgroundColor: '#dbe4ff', justifyContent: 'center', alignItems: 'center', }, label: { fontSize: 18, fontWeight: '600', }, status: { fontSize: 15, fontVariant: ['tabular-nums'], }, }); ``` </details>
1 parent 156aca8 commit 8a4d05d

5 files changed

Lines changed: 28 additions & 52 deletions

File tree

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class FlingGestureHandler : GestureHandler() {
1414

1515
private val maxDurationMs = DEFAULT_MAX_DURATION_MS
1616
private val minVelocity = DEFAULT_MIN_VELOCITY
17-
private var handler: Handler? = null
17+
private val handler = Handler(Looper.getMainLooper())
1818
private var maxNumberOfPointersSimultaneously = 0
1919
private val failDelayed = Runnable { fail() }
2020
private var velocityTracker: VelocityTracker? = null
@@ -29,12 +29,8 @@ class FlingGestureHandler : GestureHandler() {
2929
velocityTracker = VelocityTracker.obtain()
3030
begin()
3131
maxNumberOfPointersSimultaneously = 1
32-
if (handler == null) {
33-
handler = Handler(Looper.getMainLooper()) // lazy delegate?
34-
} else {
35-
handler!!.removeCallbacksAndMessages(null)
36-
}
37-
handler!!.postDelayed(failDelayed, maxDurationMs)
32+
handler.removeCallbacksAndMessages(null)
33+
handler.postDelayed(failDelayed, maxDurationMs)
3834
}
3935

4036
private fun tryEndFling(event: MotionEvent): Boolean {
@@ -69,7 +65,7 @@ class FlingGestureHandler : GestureHandler() {
6965
isAligned &&
7066
isFast
7167
) {
72-
handler!!.removeCallbacksAndMessages(null)
68+
handler.removeCallbacksAndMessages(null)
7369
activate()
7470
true
7571
} else {
@@ -109,13 +105,13 @@ class FlingGestureHandler : GestureHandler() {
109105
}
110106

111107
override fun onCancel() {
112-
handler?.removeCallbacksAndMessages(null)
108+
handler.removeCallbacksAndMessages(null)
113109
}
114110

115111
override fun onReset() {
116112
velocityTracker?.recycle()
117113
velocityTracker = null
118-
handler?.removeCallbacksAndMessages(null)
114+
handler.removeCallbacksAndMessages(null)
119115
}
120116

121117
private fun addVelocityMovement(tracker: VelocityTracker?, event: MotionEvent) {

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import com.swmansion.gesturehandler.react.events.eventbuilders.HoverGestureHandl
1212
class HoverGestureHandler : GestureHandler() {
1313
override val isContinuous = true
1414

15-
private var handler: Handler? = null
15+
private val handler = Handler(Looper.getMainLooper())
1616
private var finishRunnable = Runnable { finish() }
1717
var stylusData: StylusData = StylusData()
1818
private set
@@ -85,8 +85,7 @@ class HoverGestureHandler : GestureHandler() {
8585

8686
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
8787
if (event.action == MotionEvent.ACTION_DOWN) {
88-
handler?.removeCallbacksAndMessages(null)
89-
handler = null
88+
handler.removeCallbacksAndMessages(null)
9089
} else if (event.action == MotionEvent.ACTION_UP) {
9190
if (!isWithinBounds) {
9291
finish()
@@ -97,11 +96,9 @@ class HoverGestureHandler : GestureHandler() {
9796
override fun onHandleHover(event: MotionEvent, sourceEvent: MotionEvent) {
9897
when {
9998
event.action == MotionEvent.ACTION_HOVER_EXIT -> {
100-
if (handler == null) {
101-
handler = Handler(Looper.getMainLooper())
102-
}
103-
104-
handler!!.postDelayed(finishRunnable, 4)
99+
// Touching down synthesizes HOVER_EXIT right before ACTION_DOWN, so finish
100+
// with a slight delay - the DOWN cancels it and hover survives the press.
101+
handler.postDelayed(finishRunnable, 4)
105102
}
106103

107104
!isWithinBounds -> {

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

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class LongPressGestureHandler(context: Context) : GestureHandler() {
2020
private var startY = 0f
2121
private var startTime: Long = 0
2222
private var previousTime: Long = 0
23-
private var handler: Handler? = null
23+
private val handler = Handler(Looper.getMainLooper())
2424
private var currentPointers = 0
2525

2626
init {
@@ -112,9 +112,9 @@ class LongPressGestureHandler(context: Context) : GestureHandler() {
112112
sourceEvent.actionMasked == MotionEvent.ACTION_BUTTON_PRESS
113113
)
114114
) {
115-
handler = Handler(Looper.getMainLooper())
115+
handler.removeCallbacksAndMessages(null)
116116
if (minDurationMs > 0) {
117-
handler!!.postDelayed({ activate() }, minDurationMs)
117+
handler.postDelayed({ activate() }, minDurationMs)
118118
} else if (minDurationMs == 0L) {
119119
activate()
120120
}
@@ -124,10 +124,7 @@ class LongPressGestureHandler(context: Context) : GestureHandler() {
124124
) {
125125
currentPointers--
126126

127-
handler?.let {
128-
it.removeCallbacksAndMessages(null)
129-
handler = null
130-
}
127+
handler.removeCallbacksAndMessages(null)
131128

132129
if (state == STATE_ACTIVE) {
133130
end()
@@ -164,10 +161,7 @@ class LongPressGestureHandler(context: Context) : GestureHandler() {
164161
}
165162

166163
override fun onStateChange(newState: Int, previousState: Int) {
167-
handler?.let {
168-
it.removeCallbacksAndMessages(null)
169-
handler = null
170-
}
164+
handler.removeCallbacksAndMessages(null)
171165
}
172166

173167
override fun dispatchStateChange(newState: Int, prevState: Int) {

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class PanGestureHandler(context: Context?) : GestureHandler() {
6060
private var averageTouches = false
6161
private var activateAfterLongPress = DEFAULT_ACTIVATE_AFTER_LONG_PRESS
6262
private val activateDelayed = Runnable { activate() }
63-
private var handler: Handler? = null
63+
private val handler = Handler(Looper.getMainLooper())
6464
var stylusData: StylusData = StylusData()
6565
private set
6666

@@ -140,7 +140,7 @@ class PanGestureHandler(context: Context?) : GestureHandler() {
140140
val dy = lastY - startY + offsetY
141141

142142
if (activateAfterLongPress > 0 && dx * dx + dy * dy > defaultMinDist * defaultMinDist) {
143-
handler?.removeCallbacksAndMessages(null)
143+
handler.removeCallbacksAndMessages(null)
144144
return true
145145
}
146146
if (failOffsetXStart != MAX_VALUE_IGNORE && dx < failOffsetXStart) {
@@ -200,10 +200,7 @@ class PanGestureHandler(context: Context?) : GestureHandler() {
200200
begin()
201201

202202
if (activateAfterLongPress > 0) {
203-
if (handler == null) {
204-
handler = Handler(Looper.getMainLooper())
205-
}
206-
handler!!.postDelayed(activateDelayed, activateAfterLongPress)
203+
handler.postDelayed(activateDelayed, activateAfterLongPress)
207204
}
208205
} else if (velocityTracker != null) {
209206
addVelocityMovement(velocityTracker, sourceEvent)
@@ -252,11 +249,11 @@ class PanGestureHandler(context: Context?) : GestureHandler() {
252249
}
253250

254251
override fun onCancel() {
255-
handler?.removeCallbacksAndMessages(null)
252+
handler.removeCallbacksAndMessages(null)
256253
}
257254

258255
override fun onReset() {
259-
handler?.removeCallbacksAndMessages(null)
256+
handler.removeCallbacksAndMessages(null)
260257
velocityTracker?.let {
261258
it.recycle()
262259
velocityTracker = null

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

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class TapGestureHandler : GestureHandler() {
2626
private var offsetY = 0f
2727
private var lastX = 0f
2828
private var lastY = 0f
29-
private var handler: Handler? = null
29+
private val handler = Handler(Looper.getMainLooper())
3030
private var tapsSoFar = 0
3131
private val failDelayed = Runnable { fail() }
3232

@@ -47,24 +47,16 @@ class TapGestureHandler : GestureHandler() {
4747
}
4848

4949
private fun startTap() {
50-
if (handler == null) {
51-
handler = Handler(Looper.getMainLooper()) // TODO: lazy init (handle else branch correctly)
52-
} else {
53-
handler!!.removeCallbacksAndMessages(null)
54-
}
55-
handler!!.postDelayed(failDelayed, maxDurationMs)
50+
handler.removeCallbacksAndMessages(null)
51+
handler.postDelayed(failDelayed, maxDurationMs)
5652
}
5753

5854
private fun endTap() {
59-
if (handler == null) {
60-
handler = Handler(Looper.getMainLooper())
61-
} else {
62-
handler!!.removeCallbacksAndMessages(null)
63-
}
55+
handler.removeCallbacksAndMessages(null)
6456
if (++tapsSoFar == numberOfTaps && currentMaxNumberOfPointers >= minNumberOfPointers) {
6557
activate()
6658
} else {
67-
handler!!.postDelayed(failDelayed, maxDelayMs)
59+
handler.postDelayed(failDelayed, maxDelayMs)
6860
}
6961
}
7062

@@ -139,13 +131,13 @@ class TapGestureHandler : GestureHandler() {
139131
}
140132

141133
override fun onCancel() {
142-
handler?.removeCallbacksAndMessages(null)
134+
handler.removeCallbacksAndMessages(null)
143135
}
144136

145137
override fun onReset() {
146138
tapsSoFar = 0
147139
currentMaxNumberOfPointers = 0
148-
handler?.removeCallbacksAndMessages(null)
140+
handler.removeCallbacksAndMessages(null)
149141
}
150142

151143
class Factory : GestureHandler.Factory<TapGestureHandler>() {

0 commit comments

Comments
 (0)