Description
On Android, ButtonViewGroup.animatePressOut() can crash with a NullPointerException when it dereferences the view's Handler, which is null while the view is detached from the window.
handler here is android.view.View.getHandler(), which returns null when the view is not attached to a window (mAttachInfo == null). A cancel touch event can be delivered to a button that has just been detached: the orchestrator calls NativeViewGestureHandler.onCancel → dispatchCancelEventToView → button.onTouchEvent(CANCEL) → setPressed(false) → animatePressOut(), where handler.removeCallbacks(...) throws on the null handler.
The handler receiver is dereferenced unguarded at four sites (animatePressIn, animatePressOut, and onDetachedFromWindow). On main these are lines 710, 769, 797 and 914 of RNGestureHandlerButtonViewManager.kt. Guarding them with handler?. fixes the crash (a detached view has nothing meaningful to schedule/cancel and shows no visible animation, so a no-op is correct).
NullPointerException: Attempt to invoke virtual method 'void android.os.Handler.removeCallbacks(java.lang.Runnable)' on a null object reference
at com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager$ButtonViewGroup.animatePressOut(RNGestureHandlerButtonViewManager.kt:641)
at com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager$ButtonViewGroup.setPressed(RNGestureHandlerButtonViewManager.kt:935)
at android.view.View.onTouchEvent(View.java:19360)
at com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager$ButtonViewGroup.onTouchEvent(RNGestureHandlerButtonViewManager.kt:523)
at com.swmansion.gesturehandler.core.NativeViewGestureHandler$NativeViewGestureHandlerHook$DefaultImpls.sendTouchEvent(NativeViewGestureHandler.kt:303)
at com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager$ButtonViewGroup.access$getDummyLongClickListener$cp(RNGestureHandlerButtonViewManager.kt:356)
at com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager$ButtonViewGroup.sendTouchEvent(RNGestureHandlerButtonViewManager.kt:356)
at com.swmansion.gesturehandler.core.NativeViewGestureHandler.dispatchCancelEventToView(NativeViewGestureHandler.kt:171)
at com.swmansion.gesturehandler.core.NativeViewGestureHandler.onCancel(NativeViewGestureHandler.kt:175)
at com.swmansion.gesturehandler.core.GestureHandler.cancel(GestureHandler.kt:749)
at com.swmansion.gesturehandler.core.GestureHandlerOrchestrator.deliverEventToGestureHandler(GestureHandlerOrchestrator.kt:299)
at com.swmansion.gesturehandler.core.GestureHandlerOrchestrator.deliverEventToGestureHandlers(GestureHandlerOrchestrator.kt:276)
at com.swmansion.gesturehandler.core.GestureHandlerOrchestrator.onTouchEvent(GestureHandlerOrchestrator.kt:60)
at com.swmansion.gesturehandler.react.RNGestureHandlerRootHelper.dispatchTouchEvent(RNGestureHandlerRootHelper.kt:130)
at com.swmansion.gesturehandler.react.RNGestureHandlerRootView.dispatchTouchEvent(RNGestureHandlerRootView.kt:58)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3371)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:3047)
... (framework frames)
at android.app.Activity.dispatchTouchEvent(Activity.java:4776)
at android.view.View.dispatchPointerEvent(View.java:17527)
at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:9847)
... (ViewRootImpl input dispatch frames)
at android.os.Handler.handleCallback(Handler.java:995)
at android.os.Looper.loop(Looper.java:363)
at android.app.ActivityThread.main(ActivityThread.java:10060)
(Line numbers in the trace are from v3.0.2; the same unguarded calls are present on main.)
Steps to reproduce
This is a race between window detachment and touch cancellation, caught in production via Sentry (single occurrence, not deterministically reproducible). It occurs when:
- A button (
Pressable/BaseButton/RectButton) is pressed.
- The button is unmounted / detached from the window (e.g. a navigation transition or list re-render) while still in the pressed state.
- A cancel event is delivered to the now-detached button, so
setPressed(false) → animatePressOut() runs with getHandler() == null.
Gesture Handler version
3.0.2 (also present on main)
React Native version
0.86.0
Platforms
Android
JavaScript runtime
Hermes
Workflow
Using Expo Prebuild or an Expo development build
Architecture
New Architecture (Fabric)
Build type
Release mode
Device
Real device
Device model
Samsung Galaxy (SM-M346B1), Android 16
Additional device info
- Device: samsung SM-M346B1, device class: medium
- OS: Android 16, build BP2A.250605.031.A3.M346B1DXSCEZD8, kernel 5.10.237
- Chipset: Samsung s5e8825, 8 cores, arm64-v8a
- Screen: 1080x2340, density 2.625 (420 dpi)
- Not rooted, not an emulator, installed from Google Play
Acknowledgements
Yes
Description
On Android,
ButtonViewGroup.animatePressOut()can crash with aNullPointerExceptionwhen it dereferences the view'sHandler, which isnullwhile the view is detached from the window.handlerhere isandroid.view.View.getHandler(), which returnsnullwhen the view is not attached to a window (mAttachInfo == null). A cancel touch event can be delivered to a button that has just been detached: the orchestrator callsNativeViewGestureHandler.onCancel→dispatchCancelEventToView→button.onTouchEvent(CANCEL)→setPressed(false)→animatePressOut(), wherehandler.removeCallbacks(...)throws on the null handler.The
handlerreceiver is dereferenced unguarded at four sites (animatePressIn,animatePressOut, andonDetachedFromWindow). Onmainthese are lines 710, 769, 797 and 914 ofRNGestureHandlerButtonViewManager.kt. Guarding them withhandler?.fixes the crash (a detached view has nothing meaningful to schedule/cancel and shows no visible animation, so a no-op is correct).(Line numbers in the trace are from v3.0.2; the same unguarded calls are present on
main.)Steps to reproduce
This is a race between window detachment and touch cancellation, caught in production via Sentry (single occurrence, not deterministically reproducible). It occurs when:
Pressable/BaseButton/RectButton) is pressed.setPressed(false)→animatePressOut()runs withgetHandler() == null.Gesture Handler version
3.0.2 (also present on
main)React Native version
0.86.0
Platforms
Android
JavaScript runtime
Hermes
Workflow
Using Expo Prebuild or an Expo development build
Architecture
New Architecture (Fabric)
Build type
Release mode
Device
Real device
Device model
Samsung Galaxy (SM-M346B1), Android 16
Additional device info
Acknowledgements
Yes