Skip to content

[Bug]: SystemBars reports a zero bottom inset on API < 30 when the app uses EdgeToEdge.enable() (content renders under the navigation bar) #8603

Description

@seravifer

Capacitor Version

@capacitor/android 8.5.2
@capacitor/core 8.5.2
@capacitor/keyboard 8.0.5
@capacitor/splash-screen 8.0.2

Other API Details

Android: compileSdk/targetSdk 36, minSdk 26
Affected device: PAX A920Pro, Android 10 (API 29), Android System WebView 147.0.7727.137
Also reported by our users: Saturn1000F2, Android 9 (API 28), WebView 135
Not affected: same app on Android 11+ (API 30+), any WebView
The activity calls androidx `EdgeToEdge.enable(this)` in onCreate
Page declares <meta name="viewport" content="... viewport-fit=cover">
capacitor.config: "SystemBars": { "initialViewportFitValueHint": "cover" } (insetsHandling default "css")

Platforms Affected

  • iOS
  • Android
  • Web

Current Behavior

On API < 30, when the activity enables edge-to-edge with androidx EdgeToEdge.enable(), SystemBars reports a bottom inset of 0 while the navigation bar is visible and drawn over the window. Web content therefore renders underneath the navigation bar. The top inset is unaffected.

Measured on a PAX A920Pro (Android 10, 720x1440 @ 320dpi, dpr 2), page read through remote debugging:

--safe-area-inset-top: 24px      env(safe-area-inset-top): 24px
--safe-area-inset-bottom: 0px    env(safe-area-inset-bottom): 0px
innerHeight: 720  (= full screen height, window is edge-to-edge)

while dumpsys window windows shows the navigation bar is present and 48dp tall:

Window{... NavigationBar0}: mFrame=[0,1344][720,1440]     # 96px = 48dp
Window{... StatusBar}:      mFrame=[0,0][720,48]          # 48px = 24dp
Window{... MainActivity}:   mFrame=[0,0][720,1440]        # window covers both bars
  vsysui=LAYOUT_STABLE LAYOUT_HIDE_NAVIGATION LAYOUT_FULLSCREEN LIGHT_STATUS_BAR LIGHT_NAVIGATION_BAR
  mSystemUiVisibility=0x2710

Other apps on the same device get mContentInsets=[0,48][0,96], so the navigation bar is a normal inset provider on this ROM.

The flags above are what EdgeToEdge.enable() sets on API < 30 (SYSTEM_UI_FLAG_LAYOUT_STABLE | LAYOUT_HIDE_NAVIGATION | LAYOUT_FULLSCREEN). LAYOUT_HIDE_NAVIGATION tells the platform the window lays itself out under the navigation bar, so the navigation bar is dropped from the window's system-window insets. initWindowInsetsListener reads exactly those:

View view = getActivity().getWindow().getDecorView();

ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
    Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());

On API < 30 WindowInsetsCompat.getInsets() resolves to the system-window insets, which is where the zero comes from; on API 30+ it maps to the real WindowInsets.getInsets(Type) API, which the legacy layout flags do not affect — hence the version split.

Both branches of the listener are affected, so the symptom does not depend on the WebView version: with WebView >= 140 the zero is passed through to env(safe-area-inset-bottom), and with WebView < 140 the decor view is padded by 0 and --safe-area-inset-bottom: 0px is injected.

We started seeing this after upgrading 8.4.2 -> 8.5.2. In 8.4.x the listener was attached to the WebView's parent rather than the decor view (changed in #8535), which we believe is why the app was not exposed to the window-level layout flags before.

Expected Behavior

SystemBars reports the navigation bar inset whenever the navigation bar is visible, independent of the legacy SYSTEM_UI_FLAG_LAYOUT_* flags that EdgeToEdge.enable() sets on API < 30 — the same values the app gets on API 30+.

WindowInsetsCompat.getInsetsIgnoringVisibility() returns the correct value here: for NAVIGATION_BARS on API < 30 it resolves to the stable insets, which the layout flags do not zero. Guarding it with isVisible() keeps apps that genuinely hide the bars (immersive/fullscreen) unpadded:

Insets systemBarsInsets = Build.VERSION.SDK_INT < Build.VERSION_CODES.R && insets.isVisible(WindowInsetsCompat.Type.navigationBars())
    ? insets.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout())
    : insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());

As a workaround we now call EdgeToEdge.enable() only on API 30+, which removes the flags and fixes the overlap, at the cost of edge-to-edge rendering (and therefore the system bar colours) on older devices.

Additional Information

Possibly related to #8601, which is a different symptom in the same listener on API < 30.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions