From 53491d577ba955cb472a59a21e5d5d25ec6c879a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Boro=C5=84?= Date: Mon, 25 May 2026 16:27:36 +0200 Subject: [PATCH 1/6] Add logic for applying insets from DecorView --- .../gamma/tabs/container/TabsContainer.kt | 22 +++++++++++++++++++ .../rnscreens/gamma/tabs/host/TabsHost.kt | 2 ++ .../gamma/tabs/host/TabsHostViewManager.kt | 4 ++++ src/components/tabs/host/TabsHost.android.tsx | 5 ++++- .../tabs/host/TabsHost.android.types.ts | 6 +++++ .../tabs/TabsHostAndroidNativeComponent.ts | 1 + 6 files changed, 39 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt index 3916fdc2ac..4d003a61cb 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt @@ -10,10 +10,12 @@ import android.view.WindowInsets import android.widget.FrameLayout import androidx.appcompat.view.ContextThemeWrapper import androidx.core.graphics.Insets +import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.children import androidx.core.view.size import androidx.fragment.app.FragmentManager +import com.facebook.react.uimanager.ThemedReactContext import com.google.android.material.R import com.google.android.material.bottomnavigation.BottomNavigationView import com.swmansion.rnscreens.gamma.common.colorscheme.ColorScheme @@ -127,6 +129,9 @@ class TabsContainer internal constructor( internal var colorScheme: ColorScheme by colorSchemeCoordinator::colorScheme internal var tabBarRespectsIMEInsets: Boolean = false + internal var tabBarShouldApplyInsetsSynchronously: Boolean = true + private var insetsAppliedBySystem = false + private val contentView: FrameLayout = FrameLayout(context).apply { layoutParams = @@ -274,6 +279,8 @@ class TabsContainer internal constructor( RNSLog.d(TAG, "TabsContainer [$id] attached to window") super.onAttachedToWindow() + + maybeApplyDecorViewInsetsSynchronously() setupFragmentManager() // When TabsContainer is reattached to window, it might find new fragment manager (other @@ -306,6 +313,19 @@ class TabsContainer internal constructor( colorSchemeCoordinator.onConfigurationChanged(newConfig) } + private fun maybeApplyDecorViewInsetsSynchronously() { + if (insetsAppliedBySystem || !tabBarShouldApplyInsetsSynchronously) return + + val activity = (context as? ThemedReactContext)?.currentActivity ?: return + val decorView = activity.window.decorView + val insetsCompat = ViewCompat.getRootWindowInsets(decorView) ?: return + + val windowInsets = insetsCompat.toWindowInsets() + if (windowInsets != null) { + dispatchApplyWindowInsets(windowInsets) + } + } + override fun dispatchApplyWindowInsets(insets: WindowInsets?): WindowInsets? { // On Android versions prior to R, insets dispatch is broken. // In order to mitigate this, we override dispatchApplyWindowInsets with @@ -316,6 +336,8 @@ class TabsContainer internal constructor( return insets } + insetsAppliedBySystem = true + for (child in children) { if (child === bottomNavigationView) { val insetsForBottomNavigationView = getInsetsForBottomNavigationView(insets) diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHost.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHost.kt index 8da0489f1b..f165a4b3d2 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHost.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHost.kt @@ -58,6 +58,8 @@ class TabsHost( internal var colorScheme: ColorScheme by container::colorScheme var tabBarRespectsIMEInsets: Boolean by container::tabBarRespectsIMEInsets + internal var tabBarShouldApplyInsetsSynchronously: Boolean by container::tabBarShouldApplyInsetsSynchronously + init { addView(container) check(container.addNavigationStateObserver(this)) { diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt index c640543d2f..f51885ba59 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt @@ -134,6 +134,10 @@ class TabsHostViewManager : } } + override fun setTabBarShouldApplyInsetsSynchronously(view: TabsHost, shouldApply: Boolean) { + view.tabBarShouldApplyInsetsSynchronously = shouldApply + } + companion object { const val REACT_CLASS = "RNSTabsHostAndroid" } diff --git a/src/components/tabs/host/TabsHost.android.tsx b/src/components/tabs/host/TabsHost.android.tsx index 110998379e..a201de5c95 100644 --- a/src/components/tabs/host/TabsHost.android.tsx +++ b/src/components/tabs/host/TabsHost.android.tsx @@ -48,7 +48,10 @@ function TabsHost(props: TabsHostProps) { ref={componentNodeRef} {...filteredBaseProps} // Android-specific - tabBarRespectsIMEInsets={android?.tabBarRespectsIMEInsets}> + tabBarRespectsIMEInsets={android?.tabBarRespectsIMEInsets} + tabBarShouldApplyInsetsSynchronously={ + android?.tabBarShouldApplyInsetsSynchronously + }> {children} ); diff --git a/src/components/tabs/host/TabsHost.android.types.ts b/src/components/tabs/host/TabsHost.android.types.ts index 4f86e43cef..3f3b6001e9 100644 --- a/src/components/tabs/host/TabsHost.android.types.ts +++ b/src/components/tabs/host/TabsHost.android.types.ts @@ -15,4 +15,10 @@ export interface TabsHostPropsAndroid { * @supported API 30 or higher */ tabBarRespectsIMEInsets?: boolean | undefined; + + /** + * Defines whether window insets should be applied synchronously + * from the DecorView during view attachment. + */ + tabBarShouldApplyInsetsSynchronously?: boolean; } diff --git a/src/fabric/tabs/TabsHostAndroidNativeComponent.ts b/src/fabric/tabs/TabsHostAndroidNativeComponent.ts index 221ea81dd2..d78cf3df00 100644 --- a/src/fabric/tabs/TabsHostAndroidNativeComponent.ts +++ b/src/fabric/tabs/TabsHostAndroidNativeComponent.ts @@ -61,6 +61,7 @@ export interface NativeProps extends ViewProps { // Android-specific props tabBarRespectsIMEInsets?: CT.WithDefault; + tabBarShouldApplyInsetsSynchronously?: CT.WithDefault; } export default codegenNativeComponent('RNSTabsHostAndroid', { From 02a4c8bda092a8085c88ecd09a26b85c3b4577ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Boro=C5=84?= Date: Mon, 25 May 2026 16:40:22 +0200 Subject: [PATCH 2/6] Add scenario --- .../tests/single-feature-tests/tabs/index.ts | 2 + .../index.tsx | 153 ++++++++++++++++++ .../scenario-description.ts | 10 ++ .../scenario.md | 42 +++++ 4 files changed, 207 insertions(+) create mode 100644 apps/src/tests/single-feature-tests/tabs/test-tabs-synchronous-insets-android/index.tsx create mode 100644 apps/src/tests/single-feature-tests/tabs/test-tabs-synchronous-insets-android/scenario-description.ts create mode 100644 apps/src/tests/single-feature-tests/tabs/test-tabs-synchronous-insets-android/scenario.md diff --git a/apps/src/tests/single-feature-tests/tabs/index.ts b/apps/src/tests/single-feature-tests/tabs/index.ts index 39584ef9dc..031210d657 100644 --- a/apps/src/tests/single-feature-tests/tabs/index.ts +++ b/apps/src/tests/single-feature-tests/tabs/index.ts @@ -15,6 +15,7 @@ import TestTabsStaleStateUpdateRejection from './test-tabs-stale-update-rejectio import TestTabsTabBarMinimizeBehavior from './test-tabs-tab-bar-minimize-behavior-ios'; import TestTabsTabBarControllerMode from './test-tabs-tab-bar-controller-mode-ios'; import TestTabsSpecialEffectsScrollToTop from './test-tabs-special-effects-scroll-to-top'; +import TestTabsSynchronousInsetsAndroid from './test-tabs-synchronous-insets-android'; import TestTabsTabBarExperimentalUserInterfaceStyle from './test-tabs-tab-bar-experimental-user-interface-style-ios'; import TestTabsLifecycleEvents from './test-tabs-lifecycle-events'; import TestTabsItemTitle from './test-tabs-item-title-ios'; @@ -35,6 +36,7 @@ const scenarios = { TestTabsTabBarMinimizeBehavior, TestTabsTabBarControllerMode, TestTabsSpecialEffectsScrollToTop, + TestTabsSynchronousInsetsAndroid, TestTabsTabBarExperimentalUserInterfaceStyle, TestTabsLifecycleEvents, TestTabsItemTitle, diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-synchronous-insets-android/index.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-synchronous-insets-android/index.tsx new file mode 100644 index 0000000000..6373700898 --- /dev/null +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-synchronous-insets-android/index.tsx @@ -0,0 +1,153 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { Button, StyleSheet, Text, View } from 'react-native'; +import { NavigationContainer } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; + +import { scenarioDescription } from './scenario-description'; +import { createScenario } from '@apps/tests/shared/helpers'; +import { SettingsSwitch } from '@apps/shared'; +import { + TabsContainerWithHostConfigContext, + type TabRouteConfig, + useTabsHostConfig, + DEFAULT_TAB_ROUTE_OPTIONS, +} from '@apps/shared/gamma/containers/tabs'; +import { Colors } from '@apps/shared/styling'; + +const TestConfigContext = createContext({ + syncInsets: true, + setSyncInsets: (_: boolean) => {}, +}); + +function SetupScreen({ navigation }: any) { + const { syncInsets, setSyncInsets } = useContext(TestConfigContext); + + return ( + + Test Configuration + + + + + +