From 0da9f92c1e3862c9486416c21024050f9e183459 Mon Sep 17 00:00:00 2001 From: stachbial Date: Wed, 10 Jun 2026 18:43:08 +0200 Subject: [PATCH] fix(Android): don't block window insets animation dispatch from ScreenFooter ScreenFooter installed a WindowInsetsAnimationCompat.Callback with DISPATCH_MODE_STOP on the activity decor view and never removed it. Since DISPATCH_MODE_STOP halts dispatch to the whole subtree - and the decor view is the root of the window - mounting a single formSheet footer permanently disabled keyboard insets animations for every other consumer in the app (reanimated useAnimatedKeyboard, react-native-keyboard-controller, ...). Switch the callback to DISPATCH_MODE_CONTINUE_ON_SUBTREE (it never consumes insets, so propagation is safe) and tie registration to the view's window attachment so the decor view callback slot is restored on detach. --- .../com/swmansion/rnscreens/ScreenFooter.kt | 46 +++++-- .../TestScreenFooterKeyboardInsets.tsx | 120 ++++++++++++++++++ apps/src/tests/issue-tests/index.ts | 1 + 3 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 apps/src/tests/issue-tests/TestScreenFooterKeyboardInsets.tsx diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenFooter.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenFooter.kt index 37fe386e43..b59fe87cc8 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenFooter.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenFooter.kt @@ -15,6 +15,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EX import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN import com.google.android.material.math.MathUtils import com.swmansion.rnscreens.bottomsheet.SheetUtils +import java.lang.ref.WeakReference import kotlin.math.max @SuppressLint("ViewConstructor") @@ -48,9 +49,13 @@ class ScreenFooter( // Main goal of this callback implementation is to handle keyboard appearance. We use it to make sure // that the footer respects keyboard during layout. - // Note `DISPATCH_MODE_STOP` is used here to avoid propagation of insets callback to footer subtree. + // Note `DISPATCH_MODE_CONTINUE_ON_SUBTREE` is required here: this callback is installed on the + // window's decor view, so `DISPATCH_MODE_STOP` would halt WindowInsetsAnimation dispatch for the + // entire window, silencing every other insets-animation consumer in the app (e.g. reanimated's + // useAnimatedKeyboard or react-native-keyboard-controller). We do not consume insets in onProgress, + // so continuing dispatch is safe. private val insetsAnimation = - object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { + object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) { override fun onStart( animation: WindowInsetsAnimationCompat, bounds: WindowInsetsAnimationCompat.BoundsCompat, @@ -92,17 +97,6 @@ class ScreenFooter( } } - init { - val rootView = - checkNotNull(reactContext.currentActivity) { - "[RNScreens] Context detached from activity while creating ScreenFooter" - }.window.decorView - - // Note that we do override insets animation on given view. I can see it interfering e.g. - // with reanimated keyboard or even other places in our code. Need to test this. - ViewCompat.setWindowInsetsAnimationCallback(rootView, insetsAnimation) - } - private fun requireScreenParent(): Screen = requireNotNull(screenParent) private fun requireSheetBehavior(): BottomSheetBehavior = requireNotNull(sheetBehavior) @@ -192,11 +186,33 @@ class ScreenFooter( override fun onAttachedToWindow() { super.onAttachedToWindow() sheetBehavior?.let { registerWithSheetBehavior(it) } + registerInsetsAnimationCallback() } override fun onDetachedFromWindow() { super.onDetachedFromWindow() sheetBehavior?.let { unregisterWithSheetBehavior(it) } + unregisterInsetsAnimationCallback() + } + + private val decorView: View? + get() = reactContext.currentActivity?.window?.decorView + + // Note that we do override insets animation callback on the decor view - a view we do not own. + // Android keeps a single callback slot per view, so we restore it to null when the footer + // detaches, guarded against clearing a callback installed by a more recently attached footer. + private fun registerInsetsAnimationCallback() { + val rootView = decorView ?: return + ViewCompat.setWindowInsetsAnimationCallback(rootView, insetsAnimation) + lastRegisteredFooter = WeakReference(this) + } + + private fun unregisterInsetsAnimationCallback() { + if (lastRegisteredFooter?.get() !== this) { + return + } + lastRegisteredFooter = null + decorView?.let { ViewCompat.setWindowInsetsAnimationCallback(it, null) } } /** @@ -295,5 +311,9 @@ class ScreenFooter( companion object { const val TAG = "ScreenFooter" + + // Tracks which footer's callback currently occupies the decor view's single + // insets-animation callback slot. + private var lastRegisteredFooter: WeakReference? = null } } diff --git a/apps/src/tests/issue-tests/TestScreenFooterKeyboardInsets.tsx b/apps/src/tests/issue-tests/TestScreenFooterKeyboardInsets.tsx new file mode 100644 index 0000000000..5000e5ef9e --- /dev/null +++ b/apps/src/tests/issue-tests/TestScreenFooterKeyboardInsets.tsx @@ -0,0 +1,120 @@ +import { NavigationContainer, RouteProp } from '@react-navigation/native'; +import { + NativeStackNavigationProp, + createNativeStackNavigator, +} from '@react-navigation/native-stack'; +import React from 'react'; +import { Button, StyleSheet, Text, TextInput, View } from 'react-native'; +import Animated, { + useAnimatedKeyboard, + useAnimatedStyle, +} from 'react-native-reanimated'; + +type RouteParamList = { + Home: undefined; + FormSheetWithFooter: undefined; +}; + +type RouteProps = { + navigation: NativeStackNavigationProp; + route: RouteProp; +}; + +const Stack = createNativeStackNavigator(); + +function Home({ navigation }: RouteProps<'Home'>) { + const keyboard = useAnimatedKeyboard(); + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: -keyboard.height.value }], + })); + + return ( + + + 1. Focus the input — it should translate up with the keyboard.{'\n'} + 2. Dismiss the keyboard, open the form sheet (its footer mounts), + close the sheet.{'\n'} + 3. Focus the input again — before the fix the keyboard animation no + longer runs and never recovers for the lifetime of the process. + +