Skip to content

Commit 0fbe872

Browse files
authored
[Android] Configure the button's handler directly instead of through a ReadableMap (#4358)
## Description The button's managed handler was configured by building a `ReadableMap` and pushing it through `updateGestureHandlerConfig`, which then parsed it straight back out. Since both ends are native, the map is pure overhead on every prop transaction. `NativeViewGestureHandler` gained a typed `Config` (plus a `HitSlop`) data class and an `updateConfig` that applies it directly, and the view manager now builds that instead of a map. `gestureHitSlop` is converted from DIPs to pixels once, when the prop is set, rather than on every config write. Two things fell out of the rewrite: - Handlers are created with an empty config and attached only *after* `updateConfig` runs. Setting `enabled = false` on an already-attached handler cancels it, so a button mounted as disabled used to dispatch a pointless cancel event right after mount. - `GestureHandler.isEnabled`'s setter is now `protected` so subclasses can apply it. ## Test plan - Buttons behave the same as before across press/cancel/hit-slop/testID/enabled changes. - Mounting a button with `enabled={false}` no longer emits a cancel event. - Confirmed `hitSlop` values still land in pixels (visually, against a button with a large slop).
1 parent 7dc8dec commit 0fbe872

3 files changed

Lines changed: 82 additions & 23 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ open class GestureHandler {
7575
var isWithinBounds = false
7676
private set
7777
var isEnabled = true
78-
private set(enabled) {
78+
protected set(enabled) {
7979
// Don't cancel handler when not changing the value of the isEnabled, executing it always caused
8080
// handlers to be cancelled on re-render because that's the moment when the config is updated.
8181
// If the enabled prop "changed" from true to true the handler would get cancelled.

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ class NativeViewGestureHandler : GestureHandler() {
6868
delaysChildPressedState = DEFAULT_DELAYS_CHILD_PRESSED_STATE
6969
}
7070

71+
fun updateConfig(config: Config) {
72+
isEnabled = config.enabled
73+
shouldCancelWhenOutside = config.shouldCancelWhenOutside
74+
testID = config.testID
75+
val hitSlop = config.hitSlop
76+
if (hitSlop != null) {
77+
setHitSlop(hitSlop.left, hitSlop.top, hitSlop.right, hitSlop.bottom, HIT_SLOP_NONE, HIT_SLOP_NONE)
78+
} else {
79+
setHitSlop(null)
80+
}
81+
shouldActivateOnStart = config.shouldActivateOnStart
82+
disallowInterruption = config.disallowInterruption
83+
yieldsToContinuousGestures = config.yieldsToContinuousGestures
84+
delaysChildPressedState = config.delaysChildPressedState
85+
}
86+
7187
override fun shouldRecognizeSimultaneously(handler: GestureHandler): Boolean {
7288
// if the gesture is marked by user as simultaneous with other or the hook return true
7389
hook.shouldRecognizeSimultaneously(handler)?.let {
@@ -225,6 +241,24 @@ class NativeViewGestureHandler : GestureHandler() {
225241

226242
override fun wantsToAttachDirectlyToView() = true
227243

244+
data class HitSlop(
245+
val left: Float = HIT_SLOP_NONE,
246+
val top: Float = HIT_SLOP_NONE,
247+
val right: Float = HIT_SLOP_NONE,
248+
val bottom: Float = HIT_SLOP_NONE,
249+
)
250+
251+
data class Config(
252+
val enabled: Boolean = true,
253+
val shouldCancelWhenOutside: Boolean = DEFAULT_SHOULD_CANCEL_WHEN_OUTSIDE,
254+
val hitSlop: HitSlop? = null,
255+
val testID: String? = null,
256+
val shouldActivateOnStart: Boolean = DEFAULT_SHOULD_ACTIVATE_ON_START,
257+
val disallowInterruption: Boolean = DEFAULT_DISALLOW_INTERRUPTION,
258+
val yieldsToContinuousGestures: Boolean = DEFAULT_YIELDS_TO_CONTINUOUS_GESTURES,
259+
val delaysChildPressedState: Boolean? = DEFAULT_DELAYS_CHILD_PRESSED_STATE,
260+
)
261+
228262
class Factory : GestureHandler.Factory<NativeViewGestureHandler>() {
229263
override val type = NativeViewGestureHandler::class.java
230264
override val name = "NativeViewGestureHandler"

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

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,22 @@ class RNGestureHandlerButtonViewManager :
102102

103103
@ReactProp(name = "gestureHitSlop")
104104
override fun setGestureHitSlop(view: ButtonViewGroup, gestureHitSlop: ReadableMap?) {
105-
view.managedHandlerHitSlop = gestureHitSlop
105+
view.managedHandlerHitSlop = gestureHitSlop?.let { parseHitSlop(it) }
106+
}
107+
108+
private fun parseHitSlop(hitSlop: ReadableMap): NativeViewGestureHandler.HitSlop {
109+
fun edge(key: String) = if (hitSlop.hasKey(key)) {
110+
PixelUtil.toPixelFromDIP(hitSlop.getDouble(key))
111+
} else {
112+
GestureHandler.HIT_SLOP_NONE
113+
}
114+
115+
return NativeViewGestureHandler.HitSlop(
116+
left = edge("left"),
117+
top = edge("top"),
118+
right = edge("right"),
119+
bottom = edge("bottom"),
120+
)
106121
}
107122

108123
@ReactProp(name = "hasLongPressHandler")
@@ -439,26 +454,36 @@ class RNGestureHandlerButtonViewManager :
439454

440455
// Handler tags start at 1, so anything below that means the prop was either never set or reset
441456
// to its default because it got removed.
442-
val handlerTag = view.pendingHandlerTag?.takeIf { it > 0 }
457+
val handlerTag = view.pendingHandlerTag?.toInt()?.takeIf { it > 0 }
443458

444459
if (handlerTag == null) {
445460
dropManagedHandler(view)
446461
return
447462
}
448463

449464
val module = RNGestureHandlerModule.getModule(moduleId) ?: return
465+
val isNewHandler = handlerTag != view.managedHandlerTag
450466

451-
if (handlerTag != view.managedHandlerTag) {
467+
if (isNewHandler) {
452468
dropManagedHandler(view)
453469

454-
module.createGestureHandler("NativeViewGestureHandler", handlerTag, buildManagedHandlerConfig(view))
455-
module.attachGestureHandler(handlerTag, view.id.toDouble(), GestureHandler.ACTION_TYPE_NONE.toDouble())
470+
// Created with an empty config — the full configuration is applied below, directly on the
471+
// handler, so it never has to be packed into a map.
472+
module.createGestureHandler("NativeViewGestureHandler", handlerTag.toDouble(), Arguments.createMap())
473+
}
474+
475+
val handler = RNGestureHandlerModule.registries[moduleId]?.getHandler(handlerTag)
476+
477+
(handler as? NativeViewGestureHandler)?.updateConfig(buildManagedHandlerConfig(view))
478+
479+
if (isNewHandler) {
480+
// Attached only after the handler is configured — setting `enabled` to `false` on an already
481+
// attached handler cancels it, which for a button mounted as disabled would dispatch a
482+
// pointless cancel event right after mount.
483+
module.attachGestureHandler(handlerTag.toDouble(), view.id.toDouble(), GestureHandler.ACTION_TYPE_NONE.toDouble())
456484

457485
view.managedHandlerTag = handlerTag
458-
return
459486
}
460-
461-
module.updateGestureHandlerConfig(handlerTag, buildManagedHandlerConfig(view))
462487
}
463488

464489
private fun dropManagedHandler(view: ButtonViewGroup) {
@@ -467,20 +492,20 @@ class RNGestureHandlerButtonViewManager :
467492
view.managedHandlerTag = null
468493

469494
val moduleId = view.moduleId ?: return
470-
RNGestureHandlerModule.getModule(moduleId)?.dropGestureHandler(tag)
495+
RNGestureHandlerModule.getModule(moduleId)?.dropGestureHandler(tag.toDouble())
471496
}
472497

473-
private fun buildManagedHandlerConfig(view: ButtonViewGroup): ReadableMap = Arguments.createMap().apply {
474-
putBoolean("shouldActivateOnStart", false)
475-
putBoolean("disallowInterruption", true)
476-
putBoolean("yieldsToContinuousGestures", true)
477-
putBoolean("enabled", view.isEnabled)
478-
// Written even when null so that a removed prop clears the one on the handler.
479-
putString("testID", view.managedHandlerTestID)
480-
putMap("hitSlop", view.managedHandlerHitSlop)
481-
482-
putBoolean("shouldCancelWhenOutside", view.managedHandlerCancelOnLeave ?: true)
483-
}
498+
private fun buildManagedHandlerConfig(view: ButtonViewGroup) = NativeViewGestureHandler.Config(
499+
enabled = view.isEnabled,
500+
shouldCancelWhenOutside = view.managedHandlerCancelOnLeave ?: true,
501+
// The config is absolute, so passing the cached values through applies removed props
502+
// (null here) as a reset on the handler.
503+
hitSlop = view.managedHandlerHitSlop,
504+
testID = view.managedHandlerTestID,
505+
shouldActivateOnStart = false,
506+
disallowInterruption = true,
507+
yieldsToContinuousGestures = true,
508+
)
484509

485510
override fun onAfterUpdateTransaction(view: ButtonViewGroup) {
486511
super.onAfterUpdateTransaction(view)
@@ -512,7 +537,7 @@ class RNGestureHandlerButtonViewManager :
512537
}
513538
var useBorderlessDrawable = false
514539

515-
var managedHandlerTag: Double? = null
540+
var managedHandlerTag: Int? = null
516541

517542
var pendingHandlerTag: Double? = null
518543
set(tag) = withManagedHandlerUpdate {
@@ -526,7 +551,7 @@ class RNGestureHandlerButtonViewManager :
526551
set(testID) = withManagedHandlerUpdate {
527552
field = testID
528553
}
529-
var managedHandlerHitSlop: ReadableMap? = null
554+
var managedHandlerHitSlop: NativeViewGestureHandler.HitSlop? = null
530555
set(hitSlop) = withManagedHandlerUpdate {
531556
field = hitSlop
532557
}

0 commit comments

Comments
 (0)