Skip to content

Commit 5e6ff91

Browse files
tomekzawclaude
andauthored
fix(android): guard null Handler in ButtonViewGroup press animations (#4294)
## Description On Android, `ButtonViewGroup` uses the view's Handler (via `View.getHandler()`) to schedule and cancel the delayed press-out animation. That getter returns null while the view is detached from the window, so when a cancel event is delivered to a button that has just been detached, `animatePressOut` crashes with a NullPointerException. The crash path is: the orchestrator cancels the native handler, which dispatches a cancel event back to the button, which runs `setPressed(false)` → `animatePressOut()` and dereferences the now-null handler. This guards the four handler accesses (in `animatePressIn`, `animatePressOut` and `onDetachedFromWindow`) with a null check. When the view is detached there is nothing meaningful to schedule or cancel and no visible animation to play, so skipping the call is the correct behavior; while attached the getter is non-null and behavior is unchanged. Additionally, `onDetachedFromWindow` now runs its cleanup (cancelling `pendingPressOut`, resetting press/hover state) before calling `super.onDetachedFromWindow()`, so the pending runnable is removed while the view is still fully attached and the handler is guaranteed to be available (addresses the review note about reading the handler after `super` during detach). Closes #4293 ## Test plan The crash is a detach/cancel race captured in production via Sentry, so it has no deterministic repro. The change only adds null-safety around `View.getHandler()`, which is null exactly while the view is detached: - Attached (normal) case: the getter is non-null, so press-in and press-out animation scheduling behaves exactly as before. - Detached case: the affected `removeCallbacks`/`postDelayed` calls are skipped instead of throwing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 28a4c99 commit 5e6ff91

1 file changed

Lines changed: 6 additions & 5 deletions

File tree

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ class RNGestureHandlerButtonViewManager :
707707

708708
private fun animatePressIn() {
709709
pendingPressOut?.let {
710-
handler.removeCallbacks(it)
710+
handler?.removeCallbacks(it)
711711
pendingPressOut = null
712712
}
713713
pressInTimestamp = SystemClock.uptimeMillis()
@@ -766,7 +766,7 @@ class RNGestureHandlerButtonViewManager :
766766
event.x >= 0 && event.y >= 0 && event.x < width && event.y < height
767767

768768
private fun animatePressOut() {
769-
pendingPressOut?.let { handler.removeCallbacks(it) }
769+
pendingPressOut?.let { handler?.removeCallbacks(it) }
770770
val tapInMs = tapAnimationInDuration.toLong()
771771
val tapOutMs = tapAnimationOutDuration.toLong()
772772
val longPressMs = longPressDuration.toLong()
@@ -794,7 +794,7 @@ class RNGestureHandlerButtonViewManager :
794794
// The animator scales `remaining` by ANIMATOR_DURATION_SCALE internally,
795795
// so the press-in actually completes after `remaining * scale` ms. We need
796796
// to match that.
797-
handler.postDelayed(runnable, (remaining * getAnimatorDurationScale()).toLong())
797+
handler?.postDelayed(runnable, (remaining * getAnimatorDurationScale()).toLong())
798798
}
799799
}
800800

@@ -910,8 +910,7 @@ class RNGestureHandlerButtonViewManager :
910910
}
911911

912912
override fun onDetachedFromWindow() {
913-
super.onDetachedFromWindow()
914-
pendingPressOut?.let { handler.removeCallbacks(it) }
913+
pendingPressOut?.let { handler?.removeCallbacks(it) }
915914
pendingPressOut = null
916915
cancelPendingHoverOut()
917916
currentAnimator?.cancel()
@@ -925,6 +924,8 @@ class RNGestureHandlerButtonViewManager :
925924
if (soundResponder === this) {
926925
soundResponder = null
927926
}
927+
928+
super.onDetachedFromWindow()
928929
}
929930

930931
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {

0 commit comments

Comments
 (0)