Skip to content

fix: ignore a system bar hidden through the system UI visibility on Android 6-10 - #785

Open
YevheniiKotyrlo wants to merge 2 commits into
appandflow:mainfrom
YevheniiKotyrlo:fix/android-hidden-bar-insets
Open

YevheniiKotyrlo wants to merge 2 commits into
appandflow:mainfrom
YevheniiKotyrlo:fix/android-hidden-bar-insets

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 14, 2026 •

Copy link
Copy Markdown

Summary

On Android 6–10, a status bar hidden through View.SYSTEM_UI_FLAG_FULLSCREEN still counts as the top inset. That is what WindowInsetsControllerCompat.hide(statusBars()) sets below API 30, and so what React Native's StatusBar.setHidden(true) does in an edge-to-edge app. The edge-to-edge layout sets SYSTEM_UI_FLAG_LAYOUT_STABLE, under which the system window insets keep a hidden bar's size, and getRootWindowInsetsCompatM reads systemWindowInsetTop. So SafeAreaView and useSafeAreaInsets() reserve a band no bar covers. From API 30, getInsets(statusBars() | displayCutout() | …) already excludes a hidden bar. A navigation bar hidden through SYSTEM_UI_FLAG_HIDE_NAVIGATION has the same defect at the bottom (#63).

The API 23–29 path now reads rootView.windowSystemUiVisibility: a hidden status bar leaves the top to the display cutout's safe inset, and a hidden navigation bar leaves the sides and the bottom to it, matching the API 30 path. In split-screen and freeform mode the system keeps the bars visible whatever the app requested, so while a flag hides a bar the activity is asked whether it is in multi-window mode, and if it is the bars keep their insets. That question is a binder call, so an app hiding no bar never asks it. getSafeAreaInsets gains an overload taking the activity's context, which SafeAreaContextModule uses for the initial metrics, because a decor view's own context does not lead to the activity.

androidx core's WindowInsetsCompat reads the same flags below API 30 from 1.16.0 (androidx/androidx@deb2c56), but this library declares no androidx core version, so calling it would tie the result to whichever version an app resolves.

Test Plan

  • SafeAreaUtilsTest (Robolectric, API 29) covers a hidden status bar with and without a display cutout, a visible status bar, a hidden navigation bar, both bars hidden in multi-window mode, a decor view given its activity, and an app hiding no bar never asking about multi-window mode. On main the three hidden-bar cases fail, e.g. expected:<EdgeInsets(top=0.0, right=0.0, bottom=126.0, left=0.0)> but was:<EdgeInsets(top=96.0, right=0.0, bottom=126.0, left=0.0)>. It runs from the example app: cd example/android && ./gradlew :react-native-safe-area-context:testDebugUnitTest. The example's Jetifier skips bcprov-jdk18on, which Robolectric pulls and whose Java 25 classes Jetifier cannot read, and the package's files and scripts/check-package.mjs keep android/src/test out of the npm package.
  • On a stock Android 9 (API 28) emulator, a SafeAreaView under a hidden status bar reserves a 72 px band on 5.10.1 and none with this change; in split-screen, where the system keeps the status bar visible, the 72 px inset stays as on 5.10.1 (device evidence).
  • The JS job passes on Ubuntu (Node 22.13.0, npm 10.9.2), and the Android job's assembleDebug with -PnewArchEnabled=false and true passes on Ubuntu (JDK 17) and Windows.

…ndroid 6-10

On API 23-29 a bar hidden through SYSTEM_UI_FLAG_FULLSCREEN or
SYSTEM_UI_FLAG_HIDE_NAVIGATION keeps its system window inset under
SYSTEM_UI_FLAG_LAYOUT_STABLE, so a hidden status bar still counted as the
top inset and a hidden navigation bar as the bottom one. A hidden bar now
leaves only the display cutout, as getInsets does from API 30.
@vercel

vercel Bot commented Oct 14, 2026

Copy link
Copy Markdown

@YevheniiKotyrlo is attempting to deploy a commit to the appandflow Team on Vercel.

A member of the Team first needs to authorize it.

@YevheniiKotyrlo

YevheniiKotyrlo commented Oct 14, 2026 •

Copy link
Copy Markdown
Author

Device evidence — before / after

UNFIXED — With the status bar hidden, SafeAreaView still reserves the bar's 72 px at the top, and useSafeAreaInsets().top is 24.

FIXED — The content starts at the top of the screen, and top is 0. In split-screen, where the system keeps the status bar visible, top stays 24 as on 5.10.1.

before — stock 5.10.1 after — with this PR
full screen
split-screen

Stock Android 9 (API 28) emulator, 1140x2400 at density 480. The scene is <StatusBar hidden /> over a red view holding a transparent SafeAreaView edges={['top']} with a blue view inside it, and it hides the status bar again whenever its window gains focus, since entering split-screen clears the flag. The example's host app turns on React Native's edge-to-edge mode in both builds (setEdgeToEdgeFeatureFlagOn() and WindowCompat.setDecorFitsSystemWindows(window, false)), as an edge-to-edge app does. In full screen the red band is 72 rows on 5.10.1 and 0 with this PR; in split-screen it is 72 rows under the status bar on both.

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 14, 2026 11:36
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as draft October 15, 2026 16:54
In split-screen and freeform mode Android 7-10 keeps the system bars
visible whatever the app requested, and Android 7-9 lays the app out under
them, so a status bar hidden through SYSTEM_UI_FLAG_FULLSCREEN still covers
the top of the window there. While a flag hides a bar, ask the activity
whether it is in multi-window mode, and keep the bars' insets when it is.
The question is a binder call, so an app hiding no bar never asks it.
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 15, 2026 18:27

This branch has not been deployed

No deployments
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.

1 participant