fix: bottom sheet not appearing with android reduce motion#2652
Conversation
ddd8af3 to
9294d5d
Compare
9294d5d to
e5927ff
Compare
|
I've got a minimal reproduction that demonstrates the bad interaction between BottomSheet and Reanimated v3: https://github.com/coolsoftwaretyler/motionreproducer If you use the With reduced motion (bug)reanimated-v3-repro.webmWithout reduced motion (expected behavior)with-animations-on.webmI'd expect the reduced motion to go to the same snap point as the non-reduced motion video. It makes sense that the animation is gone itself, but it seems the position of the sheet is broken. This issue does not seem present with Reanimated v4, which behaves uniformly across reduced motion/not reduced motion from what I can see. |
|
that's a very interesting deep dive you did @coolsoftwaretyler , thanks for that! I am trying to repro the issue, but i do not have access to https://github.com/coolsoftwaretyler/motionreproducer |
|
Whoops, sorry about that @gorhom. I just updated it to be public. |
|
@coolsoftwaretyler is it possible to setup a simplified repro example on the official template: https://snack.expo.dev/@gorhom/bottom-sheet---issue-reproduction-template |
|
@gorhom I made one here, but I'm not sure if the Snack reproducer will work. The reduce motion setting in the provided Android emulator seems to be behaving differently than when I run Video of Snack: https://www.dropbox.com/scl/fi/4hnf7vzqsnsue6acb3iml/CleanShot-2026-04-21-at-14.00.19.mp4?rlkey=eieqe85mig666kwifppa9y4hm&st=86xzhpyp&dl=0 |
Motivation
This fixes bottom sheets not appearing when Android has reduce motion on, without disabling the reduced motion for users who have opted in.
As opposed to the approach in #1743, which forces the setting off in certain circumstances, this PR would trigger one more rerender on the
BottomSheetcomponent to force the screen to commit the changes.I traced through the source code of
@gorhom/bottom-sheetandreact-native-reanimated(I'm on verison 3.19, but I imagine the core mechanism is the same across v4). I believe I've pieced together the root cause of the issue, and a simple (albeit kind of clunky) fix.With reduce motion enabled and
animateOnMount=true(the default), theBottomSheetcomponent callsanimateToPosition. SourceanimateToPositioncallsanimatefrom utilities:Which in turn calls
withTimingorwithSpringhere:withSpring/withTimingreturn anAnimationObject— an object with anonFramemethod. Reanimated's shared value has a custom setter that intercepts every assignment and branches on this:Inside
initializeAnimation, reanimated callsanimation.onStart. Reanimated wraps every animation'sonStartwith a reduce-motion check inanimation/util.ts:When reduce motion is active,
onStartreplacesonFramewith() => trueand pre-setsanimation.currentto the target value beforestepever runs. Back invalueSetter,step(currentTimestamp)is then called synchronously:animateToPositionunder reduce motion ends up with norequestAnimationFramecall. From here, the Choreographer chain that would flush native view updates is never entered.Why This Breaks Android
requestAnimationFramein a Reanimated worklet is an override installed on the UI thread by Reanimated during initialization:nativeRequestAnimationFrameis a JSI binding that calls into Reanimated's C++ layer:requestRender_is a function pointer bound to the Java side:postOnAnimationqueues the callback and callsstartUpdatingOnAnimationFrame, which registers a callback with Android'sReactChoreographer:The Android
Choreographeris the system class responsible for coordinating all drawing on the main thread. It fires registered callbacks in sync with the display's vsync signal. Only work that runs inside a Choreographer callback is guaranteed to be committed to the screen in the next frame.When
mReactChoreographer.postFrameCallbackis called, Android schedulesonAnimationFrameto run at the next vsync. That callback evaluates alluseAnimatedStylehooks that depend on changed shared values and commits the resulting style updates to native views.Because reduce motion causes
step()to returnfinished=trueon its first synchronous call,requestAnimationFrameis never invoked. The entire chain ofnativeRequestAnimationFrame→maybeRequestRender→postOnAnimation→postFrameCallbackis never entered. The Choreographer is never notified,useAnimatedStyleis never re-evaluated, and the native view is never updated, even thoughBottomSheetbelieves it has completed its mount animation.Perhaps we should submit this to reanimated itself, but the bottom sheet library can workaround the problem by directly setting the value and flushing the animation.