Skip to content

fix(Android, Tabs): Apply bottom inset immediately when the container is attached#4098

Open
t0maboro wants to merge 6 commits into
mainfrom
@t0maboro/tabs-decor-view-inset
Open

fix(Android, Tabs): Apply bottom inset immediately when the container is attached#4098
t0maboro wants to merge 6 commits into
mainfrom
@t0maboro/tabs-decor-view-inset

Conversation

@t0maboro
Copy link
Copy Markdown
Contributor

@t0maboro t0maboro commented May 25, 2026

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 BottomNavigationView recalculates 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

  • In TabsContainer we now fetch the window insets directly from the activity's DecorView during onAttachedToWindow and manually dispatch them immediately.
  • Introduced the tabBarShouldApplyInsetsSynchronously prop to the JS layer, allowing developers to opt-out of this behavior if needed.

Before & after - visual documentation

Before After
before.mov
after.mov

Test plan

Added dedicated SFT

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (default true) to the TabsHost Android native component + JS-facing types and wiring.
  • Implemented synchronous insets dispatch in TabsContainer.onAttachedToWindow() by reading root window insets from DecorView.
  • 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.

Comment thread src/components/tabs/host/TabsHost.android.types.ts Outdated
Copy link
Copy Markdown
Collaborator

@LKuchno LKuchno left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Collaborator

@LKuchno LKuchno May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following new naming (described in RFC), if we are not going to implement e2e test I would change this line to:

Suggested change
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',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
import TestTabsSynchronousInsetsAndroid from './test-tabs-synchronous-insets-android';
import TestTabsSynchronousInsets from './test-tabs-synchronous-insets-android';

TestTabsTabBarMinimizeBehavior,
TestTabsTabBarControllerMode,
TestTabsSpecialEffectsScrollToTop,
TestTabsSynchronousInsetsAndroid,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add Button to the screen to go back to previous screen?
On new Android devices (i.e. Pixel 9) system back button is hidden by default.

Image

return insets
}

insetsAppliedBySystem = true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants