fix(Android, Tabs): Apply bottom inset immediately when the container is attached#4098
fix(Android, Tabs): Apply bottom inset immediately when the container is attached#4098t0maboro wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses an Android visual “jump” of the bottom tab bar by applying window insets synchronously at view-attachment time (using the Activity DecorView insets), and exposes a JS prop to opt out of that behavior.
Changes:
- Added
tabBarShouldApplyInsetsSynchronously(defaulttrue) to the TabsHost Android native component + JS-facing types and wiring. - Implemented synchronous insets dispatch in
TabsContainer.onAttachedToWindow()by reading root window insets fromDecorView. - Added a dedicated Single Feature Test (SFT) scenario for manual verification.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/fabric/tabs/TabsHostAndroidNativeComponent.ts | Adds the new codegen prop with a default value. |
| src/components/tabs/host/TabsHost.android.types.ts | Exposes the new Android prop to JS/TS consumers. |
| src/components/tabs/host/TabsHost.android.tsx | Wires the new prop from props.android into the native component. |
| android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt | Adds the generated setter to forward the prop to the view. |
| android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHost.kt | Proxies the new prop down to the container. |
| android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt | Implements synchronous insets application during attachment and tracks dispatch state. |
| apps/src/tests/single-feature-tests/tabs/test-tabs-synchronous-insets-android/scenario.md | Documents the manual test steps/expectations. |
| apps/src/tests/single-feature-tests/tabs/test-tabs-synchronous-insets-android/scenario-description.ts | Registers metadata for the new SFT scenario. |
| apps/src/tests/single-feature-tests/tabs/test-tabs-synchronous-insets-android/index.tsx | Implements the new SFT scenario UI/flow. |
| apps/src/tests/single-feature-tests/tabs/index.ts | Registers the new scenario in the tabs scenario group. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
LKuchno
left a comment
There was a problem hiding this comment.
I left few comments and suggestions :)
|
|
||
| ## E2E test | ||
|
|
||
| No. Visual layout jumps during screen transitions are difficult to capture reliably in automated UI tests like Detox without precise frame-by-frame visual regression tools. Manual visual verification is required. |
There was a problem hiding this comment.
Following new naming (described in RFC), if we are not going to implement e2e test I would change this line to:
| No. Visual layout jumps during screen transitions are difficult to capture reliably in automated UI tests like Detox without precise frame-by-frame visual regression tools. Manual visual verification is required. | |
| Incomplete: Not automated. Visual layout jumps during screen transitions are difficult to capture reliably in automated UI tests like Detox without precise frame-by-frame visual regression tools. Manual visual verification is required. |
| key: 'test-tabs-synchronous-insets-android', | ||
| details: 'Test synchronous application of window insets on Android', | ||
| platforms: ['android'], | ||
| e2eCoverage: 'tbd', |
There was a problem hiding this comment.
Looking at the scenario E2E test section it's already decided that this test won't be automated so e2eCoverage value update should be made:
| e2eCoverage: 'tbd', | |
| e2eCoverage: 'incomplete', |
| 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'; |
There was a problem hiding this comment.
If there is no corresponding iOS screen with the same name, we can omit the platform from the scenario name (similar to how it is done for TestTabsTabBarMinimizeBehavior). The fact that a scenario is Android-only is already indicated by the icon on the scenario list.
| import TestTabsSynchronousInsetsAndroid from './test-tabs-synchronous-insets-android'; | |
| import TestTabsSynchronousInsets from './test-tabs-synchronous-insets-android'; |
| TestTabsTabBarMinimizeBehavior, | ||
| TestTabsTabBarControllerMode, | ||
| TestTabsSpecialEffectsScrollToTop, | ||
| TestTabsSynchronousInsetsAndroid, |
There was a problem hiding this comment.
| TestTabsSynchronousInsetsAndroid, | |
| TestTabsSynchronousInsets, |
|
|
||
| - [ ] Expected: A transition to the tabs screen begins. The bottom tab bar renders with the correct height and bottom padding respecting system navigation bars. There is no visible vertical layout jump or resize of the tab bar after the transition completes. | ||
|
|
||
| 3. Press the back button to return to the **Setup** screen. |
| return insets | ||
| } | ||
|
|
||
| insetsAppliedBySystem = true |
There was a problem hiding this comment.
First call to this method comes from us in onAttachedToWindow -> can we call it "by system" then?
| * @supported API 30 or higher | ||
| */ | ||
| tabBarRespectsIMEInsets?: boolean | undefined; | ||
|
|
There was a problem hiding this comment.
I think we've been using no new line between props. Recently in FormSheet we started using them but it would be nice to keep one convention. Not sure which one.
| * | ||
| * @platform android | ||
| */ | ||
| tabBarShouldApplyInsetsSynchronously?: boolean; |
There was a problem hiding this comment.
I'm thinking about how we want to expose controlling the inset in tabs here.
Now we're adding a switch between reading from decor vs not reading from decor and relying on native dispatch.
But should we also expose an option to disable the inset completely? E.g. is somebody uses a footer of some kind that handles the bottom inset visually but doesn't consume it natively (e.g. a react native view)?
If so, do we need an option to control synchronous read? I guess it won't hurt even if we add a prop to control inset application later.

Description
On Android, applying window insets asynchronously causes a visual jump of the bottom tab bar. This happens because React Native calculates the initial layout and starts the screen transition before the system dispatches the window insets. By the time the insets arrive, the
BottomNavigationViewrecalculates its height/padding mid-transition or right after it, resulting in a tab bar content jitter.Note
Visual jumps inside the TabBar's content will be resolved in a followup PR.
Changes
DecorViewduringonAttachedToWindowand manually dispatch them immediately.tabBarShouldApplyInsetsSynchronouslyprop to the JS layer, allowing developers to opt-out of this behavior if needed.Before & after - visual documentation
before.mov
after.mov
Test plan
Added dedicated SFT
Checklist