mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
feat(android): migrate edge-to-edge to built-in SystemBars (#8543)
* feat(android): migrate edge-to-edge to built-in SystemBars Replace @capawesome/capacitor-android-edge-to-edge-support with Capacitor 8's built-in SystemBars (insetsHandling: 'css'). SystemBars handles edge-to-edge insets + IME padding on WebView >= 140 / API >= 35; the WebView < 140 / API < 35 tail is covered by env() fallback plus a native keyboard shim (adjustWebViewHeightForKeyboardBelowApi30), gated to WebView < 140 so it does not fight SystemBars' own IME padding. - config: drop EdgeToEdge plugin config + includePlugins entry + dep; remove the plugin from the generated gradle registration - theme: stop JS-writing --safe-area-inset-* on Android (SystemBars owns them; the SCSS env() fallback preserves the #8283 top fix); bars are now transparent with the theme color painted behind them via setWebViewBackgroundColor, since SystemBars has no bar-color API - StartupOverlayManager: derive the overlay inset from the system-bar insets instead of the removed plugin's WebView margin Spike: TypeScript verified (tsc clean, lint clean). Kotlin and on-device behavior NOT yet validated (gradle unavailable in this environment). Requires `npm install` + `npx cap sync android` and the device matrix in docs/plans/2026-06-22-android-systembars-migration-corrected.md before merge. * build(deps): drop @capawesome edge-to-edge from lockfile Reconcile package-lock.json with the package.json change in the edge-to-edge -> SystemBars migration (removes only the @capawesome/capacitor-android-edge-to-edge- support entry). Generated by `npm install`. * docs(android): clarify SystemBars band model from implementation review Comment-only + plan-doc follow-up to the multi-agent review of the migration: - Correct the band model in the inset comments: SystemBars *injects* --safe-area-inset-* only on API >= 35; WebView >= 140 is native env() passthrough (no injection below API 35). The SCSS var(..., env()) fallback covers every band. (capacitor.config.ts, global-theme.service.ts) - Fix a stale comment in StartupOverlayManager.show() that still described the removed @capawesome plugin insetting the WebView. - Record the device-validation-only findings in the plan doc (API>=35/WebView<140 double-count corner, env vs var consumer split, API 30-34/WebView<140 IME owner, CDK overlay top shift) so they are checked on the matrix, not blind-fixed (a blind shim extension would risk re-creating #8508). No behavior change.
This commit is contained in:
parent
20e85a2a19
commit
bb3ed74480
10 changed files with 364 additions and 168 deletions
|
|
@ -17,7 +17,6 @@ dependencies {
|
||||||
implementation project(':capacitor-local-notifications')
|
implementation project(':capacitor-local-notifications')
|
||||||
implementation project(':capacitor-share')
|
implementation project(':capacitor-share')
|
||||||
implementation project(':capawesome-capacitor-android-dark-mode-support')
|
implementation project(':capawesome-capacitor-android-dark-mode-support')
|
||||||
implementation project(':capawesome-capacitor-android-edge-to-edge-support')
|
|
||||||
implementation project(':capawesome-capacitor-background-task')
|
implementation project(':capawesome-capacitor-background-task')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -452,45 +452,40 @@ class CapacitorMainActivity : BridgeActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SDK < 30 soft-keyboard workaround for the add-task bar sitting behind the
|
* SDK < 30 soft-keyboard fallback for the add-task bar sitting behind the
|
||||||
* keyboard (#8508 follow-up, Android 9 / API 28).
|
* keyboard (#8508 follow-up, Android 9 / API 28).
|
||||||
*
|
*
|
||||||
* Root cause: the `@capawesome` edge-to-edge plugin sets the WebView
|
* Context: Android edge-to-edge inset handling is owned by Capacitor's
|
||||||
* `bottomMargin = 0` whenever the IME is visible (`EdgeToEdge.applyInsetsInternal`:
|
* built-in SystemBars now (the `@capawesome` edge-to-edge plugin was removed).
|
||||||
* "the system already resizes the window for the keyboard"), but under enforced
|
* SystemBars only pads the WebView for the IME on **WebView >= 140**
|
||||||
* edge-to-edge (targetSdk 36) the window does NOT resize on API < 30, so the
|
* (passthrough) or **API >= 35**; below that band it is a no-op, and under
|
||||||
* WebView keeps its full height and the `position: fixed` bar sits behind the
|
* enforced edge-to-edge the window does NOT resize for the IME on API < 30,
|
||||||
* keyboard.
|
* so the `position: fixed` add-task bar sits behind the keyboard. This shim
|
||||||
|
* covers exactly that WebView < 140 / API < 30 tail and is **gated to
|
||||||
|
* WebView < 140** so it never double-counts against SystemBars' own padding.
|
||||||
*
|
*
|
||||||
* We must NOT correct this via `bottomMargin`: the plugin owns that property and
|
* We must NOT correct this via `bottomMargin` or `padding` (a margin writer
|
||||||
* rewrites it on every inset dispatch, so a second writer just flickers
|
* fights whatever owns the insets, and WebView padding does not move the web
|
||||||
* (confirmed on an API 28 device — the margin alternated `0 ↔ lift`). WebView
|
* layout viewport). Instead, while the keyboard is up we set an explicit
|
||||||
* *padding* does not move the web layout viewport either.
|
* WebView **layout height** (to the keyboard top) and restore the resting
|
||||||
*
|
* height ([webViewLayoutHeightDefault], e.g. MATCH_PARENT) on hide. Shrinking
|
||||||
* Instead, while the keyboard is up we set an explicit WebView **layout height**
|
* the view shrinks the web layout viewport, so the existing CSS resolves the
|
||||||
* (to the keyboard top), and restore the resting height ([webViewLayoutHeightDefault],
|
* bar above the keyboard with no web-side keyboard-height math (avoiding the
|
||||||
* e.g. MATCH_PARENT) on hide. Height is a different property than the margin the
|
* reverted #8295 fallback). The target (`rect.bottom − webViewTop`) is read
|
||||||
* plugin manages, and for an explicit-height view the bottom margin does not
|
* from `getWindowVisibleDisplayFrame` (reliable on API 28) and does not
|
||||||
* change the view's size — so the two never fight, and the plugin keeps doing
|
|
||||||
* everything else (system-bar insets AND its color overlays, so no white navbar
|
|
||||||
* gap). Shrinking the view shrinks the web layout viewport, so the existing CSS
|
|
||||||
* resolves the bar above the keyboard with no web-side keyboard-height math
|
|
||||||
* (avoiding the reverted #8295 fallback). The target (`rect.bottom − webViewTop`)
|
|
||||||
* is read from `getWindowVisibleDisplayFrame` (reliable on API 28) and does not
|
|
||||||
* depend on the WebView's own height, so it is stable across passes — no
|
* depend on the WebView's own height, so it is stable across passes — no
|
||||||
* feedback loop. See docs/android-edge-to-edge-keyboard.md.
|
* feedback loop. See docs/android-edge-to-edge-keyboard.md and
|
||||||
|
* docs/plans/2026-06-22-android-systembars-migration-corrected.md.
|
||||||
*
|
*
|
||||||
* API >= 30 is a strict no-op — the plugin stays fully in charge, so the
|
* API >= 30 and WebView >= 140 are strict no-ops.
|
||||||
* behavior verified in 18.12.0 is unchanged.
|
|
||||||
*
|
|
||||||
* NOTE: [StartupOverlayManager.updateOverlayInsets] also reads the WebView's
|
|
||||||
* geometry (`webView.height`) to align the native startup overlay. It only
|
|
||||||
* stays correct because it early-returns once its input bar is visible (the
|
|
||||||
* keyboard phase) — i.e. it never reads the height while this method is
|
|
||||||
* shrinking it. Keep that guard if you touch either side.
|
|
||||||
*/
|
*/
|
||||||
private fun adjustWebViewHeightForKeyboardBelowApi30(rect: Rect, isKeyboardOpen: Boolean) {
|
private fun adjustWebViewHeightForKeyboardBelowApi30(rect: Rect, isKeyboardOpen: Boolean) {
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 30) return
|
if (android.os.Build.VERSION.SDK_INT >= 30) return
|
||||||
|
// Skip when SystemBars already handles the IME inset (WebView >= 140
|
||||||
|
// passthrough pads the WebView parent itself). Unknown version (null) ->
|
||||||
|
// run the shim, the safe default on API < 30.
|
||||||
|
val wvMajor = webViewCompatibility?.majorVersion
|
||||||
|
if (wvMajor != null && wvMajor >= 140) return
|
||||||
val webView = bridge?.webView ?: return
|
val webView = bridge?.webView ?: return
|
||||||
val params = webView.layoutParams ?: return
|
val params = webView.layoutParams ?: return
|
||||||
// Ignore stale/pre-layout geometry so the height is not set from a bad frame.
|
// Ignore stale/pre-layout geometry so the height is not set from a bad frame.
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
|
||||||
import com.getcapacitor.BridgeActivity
|
|
||||||
import com.superproductivity.superproductivity.R
|
import com.superproductivity.superproductivity.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -38,11 +39,12 @@ class StartupOverlayManager(private val activity: android.app.Activity) {
|
||||||
private var insetLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
|
private var insetLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
|
||||||
private var taskCount = 0
|
private var taskCount = 0
|
||||||
private var isBarVisible = false
|
private var isBarVisible = false
|
||||||
// How far the WebView's bottom sits above the overlay's bottom (px) = the
|
// The bottom system-bar inset (nav/gesture bar height, px) that the web
|
||||||
// bottom inset the edge-to-edge plugin applied to the WebView. This is what
|
// add-task bar pads itself above via the safe-area inset. The FAB/input bar
|
||||||
// the web UI's safe area actually is on this device — measured directly from
|
// are lifted by the same amount so they line up with the web bar we hand off
|
||||||
// the WebView's geometry rather than guessed from navigationBars(), which
|
// to. Under Capacitor's built-in SystemBars the WebView draws edge-to-edge
|
||||||
// does not match the applied inset on gesture-nav devices. -1 = not measured.
|
// (no bottom margin), so this is read from the root window insets rather than
|
||||||
|
// measured from the WebView's geometry. -1 = not measured.
|
||||||
private var webViewBottomInset = -1
|
private var webViewBottomInset = -1
|
||||||
|
|
||||||
private val isDarkMode: Boolean
|
private val isDarkMode: Boolean
|
||||||
|
|
@ -65,10 +67,11 @@ class StartupOverlayManager(private val activity: android.app.Activity) {
|
||||||
|
|
||||||
// Edge-to-edge (Capacitor 8, targetSdk 36): this overlay lives on the
|
// Edge-to-edge (Capacitor 8, targetSdk 36): this overlay lives on the
|
||||||
// full-window android.R.id.content, behind the system navigation bar,
|
// full-window android.R.id.content, behind the system navigation bar,
|
||||||
// while the web UI lives in a WebView that the edge-to-edge plugin insets
|
// while the web UI draws edge-to-edge and pads its add-task bar above the
|
||||||
// above the nav bar. Lift the FAB/input bar by that same inset so they
|
// nav bar by the bottom system-bar (safe-area) inset. Lift the FAB/input
|
||||||
// line up with the web add-task button/bar we hand off to. Re-measured on
|
// bar by that same inset so they line up with the web bar we hand off to.
|
||||||
// each layout so it catches the inset being applied late and on rotation.
|
// Re-measured on each layout so it catches the inset being applied late
|
||||||
|
// and on rotation. See updateOverlayInsets.
|
||||||
insetLayoutListener = ViewTreeObserver.OnGlobalLayoutListener { updateOverlayInsets() }
|
insetLayoutListener = ViewTreeObserver.OnGlobalLayoutListener { updateOverlayInsets() }
|
||||||
rootView.viewTreeObserver.addOnGlobalLayoutListener(insetLayoutListener)
|
rootView.viewTreeObserver.addOnGlobalLayoutListener(insetLayoutListener)
|
||||||
updateOverlayInsets()
|
updateOverlayInsets()
|
||||||
|
|
@ -96,35 +99,30 @@ class StartupOverlayManager(private val activity: android.app.Activity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Measure how far the WebView's bottom sits above the overlay's bottom (the
|
* Lift the FAB / input bar above the navigation bar by the bottom system-bar
|
||||||
* inset the edge-to-edge plugin applied) and lift the FAB / input bar by it
|
* inset, so they align with the web add-task button/bar (which pads itself by
|
||||||
* so they align with the web add-task button/bar. Mirroring the WebView's
|
* the same safe-area inset under SystemBars). Reads the root window insets
|
||||||
* real geometry is device-independent: it works regardless of gesture vs
|
* directly — device-independent across gesture vs 3-button navigation. No-op
|
||||||
* 3-button navigation, where navigationBars() would not match the applied
|
* until the insets are available and when the inset is unchanged.
|
||||||
* inset. No-op until the views are laid out and when the inset is unchanged.
|
|
||||||
*/
|
*/
|
||||||
private fun updateOverlayInsets() {
|
private fun updateOverlayInsets() {
|
||||||
val overlay = overlayView ?: return
|
val overlay = overlayView ?: return
|
||||||
// Freeze the inset once the bar is expanded. The edge-to-edge plugin sets
|
// Freeze the inset once the bar is expanded: from then on the keyboard
|
||||||
// the WebView's bottom margin to 0 while the IME is visible (EdgeToEdge.java:
|
// listener in expandToInputBar owns the bar's margin (it adds the IME
|
||||||
// "system already resizes the window for the keyboard"), so re-measuring
|
// height on top of this resting inset). This also stops dead work once
|
||||||
// during the keyboard phase would read ~0 and drop the bar behind the
|
// the FAB is gone.
|
||||||
// keyboard. The FAB-phase value (keyboard down) is the one the keyboard
|
|
||||||
// listener needs. This also stops dead work once the FAB is gone.
|
|
||||||
//
|
|
||||||
// This early-return is also what keeps us off CapacitorMainActivity's
|
|
||||||
// adjustWebViewHeightForKeyboardBelowApi30, which shrinks `webView.height`
|
|
||||||
// while the IME is up (API < 30): we only read the height in the keyboard-
|
|
||||||
// down FAB phase, never while it is being shrunk. Keep this guard.
|
|
||||||
if (isBarVisible) return
|
if (isBarVisible) return
|
||||||
val webView = (activity as? BridgeActivity)?.bridge?.webView ?: return
|
if (overlay.height == 0) return
|
||||||
if (overlay.height == 0 || webView.height == 0) return
|
|
||||||
|
|
||||||
val overlayLoc = IntArray(2)
|
// Read the bottom system-bar inset (nav/gesture bar) directly. Under
|
||||||
val webViewLoc = IntArray(2)
|
// SystemBars the WebView draws edge-to-edge with no bottom margin, so the
|
||||||
overlay.getLocationInWindow(overlayLoc)
|
// old (overlayBottom − webViewBottom) measurement reads ~0; the web bar
|
||||||
webView.getLocationInWindow(webViewLoc)
|
// pads itself by this same safe-area inset, so mirroring it here keeps the
|
||||||
val inset = ((overlayLoc[1] + overlay.height) - (webViewLoc[1] + webView.height))
|
// native overlay aligned with the web bar across gesture and 3-button nav.
|
||||||
|
val rootInsets = ViewCompat.getRootWindowInsets(overlay) ?: return
|
||||||
|
val inset = rootInsets
|
||||||
|
.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
.bottom
|
||||||
.coerceAtLeast(0)
|
.coerceAtLeast(0)
|
||||||
if (inset == webViewBottomInset) return
|
if (inset == webViewBottomInset) return
|
||||||
webViewBottomInset = inset
|
webViewBottomInset = inset
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,5 @@ project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/sh
|
||||||
include ':capawesome-capacitor-android-dark-mode-support'
|
include ':capawesome-capacitor-android-dark-mode-support'
|
||||||
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/@capawesome/capacitor-android-dark-mode-support/android')
|
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/@capawesome/capacitor-android-dark-mode-support/android')
|
||||||
|
|
||||||
include ':capawesome-capacitor-android-edge-to-edge-support'
|
|
||||||
project(':capawesome-capacitor-android-edge-to-edge-support').projectDir = new File('../node_modules/@capawesome/capacitor-android-edge-to-edge-support/android')
|
|
||||||
|
|
||||||
include ':capawesome-capacitor-background-task'
|
include ':capawesome-capacitor-background-task'
|
||||||
project(':capawesome-capacitor-background-task').projectDir = new File('../node_modules/@capawesome/capacitor-background-task/android')
|
project(':capawesome-capacitor-background-task').projectDir = new File('../node_modules/@capawesome/capacitor-background-task/android')
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ const config: CapacitorConfig = {
|
||||||
// and uses JavaScriptInterface for keyboard visibility instead.
|
// and uses JavaScriptInterface for keyboard visibility instead.
|
||||||
// 'native' resizes the WKWebView so 100vh fits above the keyboard.
|
// 'native' resizes the WKWebView so 100vh fits above the keyboard.
|
||||||
resize: 'native',
|
resize: 'native',
|
||||||
// false is required when paired with @capawesome/capacitor-android-edge-
|
// iOS-only; ignored on Android (plugin excluded). Kept false so the
|
||||||
// to-edge-support; ignored on iOS where this key has no effect.
|
// WKWebView is not force-resized in fullscreen.
|
||||||
resizeOnFullScreen: false,
|
resizeOnFullScreen: false,
|
||||||
},
|
},
|
||||||
StatusBar: {
|
StatusBar: {
|
||||||
|
|
@ -27,20 +27,17 @@ const config: CapacitorConfig = {
|
||||||
overlaysWebView: true,
|
overlaysWebView: true,
|
||||||
},
|
},
|
||||||
SystemBars: {
|
SystemBars: {
|
||||||
// Disable Capacitor's built-in inset handling so the edge-to-edge plugin
|
// Let Capacitor's built-in SystemBars own edge-to-edge inset handling on
|
||||||
// can own it. With targetSdk 36 (Android 16) edge-to-edge is mandatory,
|
// Android (replaces the @capawesome edge-to-edge plugin). 'css' enables
|
||||||
// and the two layers both applying insets fight each other — visible
|
// SystemBars' Android inset handling: it *injects* --safe-area-inset-* CSS
|
||||||
// as fixed-position elements scrolling with content when the IME is up.
|
// vars on API >= 35, and passes native env(safe-area-inset-*) through on
|
||||||
insetsHandling: 'disable',
|
// WebView >= 140. The WebView <140 / API <35 tail gets neither and falls
|
||||||
},
|
// back to env() (plus the native keyboard shim in CapacitorMainActivity).
|
||||||
EdgeToEdge: {
|
// See docs/plans/2026-06-22-android-systembars-migration-corrected.md.
|
||||||
// Initial status/navigation bar background color, shown before the theme
|
insetsHandling: 'css',
|
||||||
// service boots and calls EdgeToEdge.set{Status,Navigation}BarColor.
|
// Initial bar icon style before the theme service boots; runtime updates
|
||||||
// Without this the plugin's overlay views default to transparent, so the
|
// go through StatusBar.setStyle / NavigationBar (global-theme.service.ts).
|
||||||
// bottom navigation/gesture area shows the bare window background. Dark to
|
style: 'DARK',
|
||||||
// match the most common mobile theme (cf. the ios backgroundColor below).
|
|
||||||
statusBarColor: '#131314',
|
|
||||||
navigationBarColor: '#131314',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
android: {
|
android: {
|
||||||
|
|
@ -56,7 +53,6 @@ const config: CapacitorConfig = {
|
||||||
'@capacitor/local-notifications',
|
'@capacitor/local-notifications',
|
||||||
'@capacitor/share',
|
'@capacitor/share',
|
||||||
'@capawesome/capacitor-android-dark-mode-support',
|
'@capawesome/capacitor-android-dark-mode-support',
|
||||||
'@capawesome/capacitor-android-edge-to-edge-support',
|
|
||||||
'@capawesome/capacitor-background-task',
|
'@capawesome/capacitor-background-task',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,18 @@ How the global add-task bar is positioned over the keyboard, and the full #8508
|
||||||
saga. **Read this before touching anything keyboard/IME-related on Android — this
|
saga. **Read this before touching anything keyboard/IME-related on Android — this
|
||||||
area has regressed repeatedly (#8295, then #8508).**
|
area has regressed repeatedly (#8295, then #8508).**
|
||||||
|
|
||||||
|
> **Update (2026-06-22): migrated off `@capawesome/...edge-to-edge-support` to
|
||||||
|
> Capacitor's built-in `SystemBars`** (`insetsHandling: 'css'`). Edge-to-edge
|
||||||
|
> insets + IME padding are now handled by SystemBars on **WebView ≥ 140** (or
|
||||||
|
> API ≥ 35); the **WebView < 140 / API < 35** tail is covered by env() + a native
|
||||||
|
> keyboard shim (`adjustWebViewHeightForKeyboardBelowApi30`, now gated to
|
||||||
|
> WebView < 140 so it never fights SystemBars). Bar backgrounds are no longer
|
||||||
|
> painted by a plugin (SystemBars has no color API) — the bars are transparent
|
||||||
|
> and the theme color shows through via `NavigationBarPlugin.setWebViewBackgroundColor`
|
||||||
|
> (window decor + WebView surface). The #8508 sections below describe the *former*
|
||||||
|
> `@capawesome` mechanics and are kept as history. Full rationale + device matrix:
|
||||||
|
> [`docs/plans/2026-06-22-android-systembars-migration-corrected.md`](plans/2026-06-22-android-systembars-migration-corrected.md).
|
||||||
|
|
||||||
> **⚠️ Do NOT inset the WebView for the IME based on an assumption that the
|
> **⚠️ Do NOT inset the WebView for the IME based on an assumption that the
|
||||||
> system "doesn't resize on Android 15/16."** Real devices (incl. a Pixel-class
|
> system "doesn't resize on Android 15/16."** Real devices (incl. a Pixel-class
|
||||||
> Android 16 phone) still resize the window for the keyboard. Insetting on top of
|
> Android 16 phone) still resize the window for the keyboard. Insetting on top of
|
||||||
|
|
|
||||||
247
docs/plans/2026-06-22-android-systembars-migration-corrected.md
Normal file
247
docs/plans/2026-06-22-android-systembars-migration-corrected.md
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
# Android edge-to-edge: migrate `@capawesome` → Capacitor `SystemBars` (corrected plan)
|
||||||
|
|
||||||
|
**Date:** 2026-06-22
|
||||||
|
**Status:** PLAN ONLY — not started. Supersedes the migration section (§5) of the
|
||||||
|
PR-8528 handover, which was written before the `SystemBars` WebView/API gating
|
||||||
|
(below) was known.
|
||||||
|
**Prereq reading:** [`docs/android-edge-to-edge-keyboard.md`](../android-edge-to-edge-keyboard.md)
|
||||||
|
(the #8508 saga and the "never blindly inset the WebView for the IME" rule).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
- Dropping the thrice-regressed `@capawesome/capacitor-android-edge-to-edge-support`
|
||||||
|
plugin for Capacitor 8's built-in `SystemBars` is the right **long-term**
|
||||||
|
direction (one fewer fragile dependency in the area that regressed at #8295 and
|
||||||
|
twice in #8508).
|
||||||
|
- **It is a device-validated spike, not a config swap.** A naive "remove the plugin,
|
||||||
|
flip `insetsHandling` to `'css'`" **regresses a supported slice of the fleet** —
|
||||||
|
see the gating model below.
|
||||||
|
- **The reported API<30 keyboard bug should ship independently** via the native
|
||||||
|
height workaround (`adjustWebViewHeightForKeyboardBelowApi30`, PR #8528). It is
|
||||||
|
WebView-version-independent; the migration's keyboard handling is **not**. Keep
|
||||||
|
that workaround inside the migration too — do not bank on Capacitor core #8481.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why the naive plan is wrong: the `SystemBars` inset model
|
||||||
|
|
||||||
|
Verified by reading the bundled source
|
||||||
|
`node_modules/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java`
|
||||||
|
(constants `WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 140`,
|
||||||
|
`WEBVIEW_VERSION_WITH_SAFE_AREA_KEYBOARD_FIX = 144`; logic in
|
||||||
|
`initWindowInsetsListener()` lines 177–231).
|
||||||
|
|
||||||
|
`SystemBars` does **not** handle insets unconditionally. Its window-insets listener
|
||||||
|
branches on `shouldPassthroughInsets = (WebView major ≥ 140) && viewport-fit=cover`
|
||||||
|
(the app has `viewport-fit=cover`, `src/index.html:8`):
|
||||||
|
|
||||||
|
| Device state | Static bar insets | Keyboard / IME inset |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **WebView ≥ 140** (passthrough) | native `env(safe-area-inset-*)` passed through, all API levels | `setPadding(…, imeInsets.bottom)` on the WebView parent — **works on all API levels** (incl. API 28) |
|
||||||
|
| **WebView < 140**, **API ≥ 35** | `setPadding` + injects `--safe-area-inset-*` CSS vars | handled |
|
||||||
|
| **WebView < 140**, **API < 35** | **nothing** (insets zeroed & consumed) | **nothing** |
|
||||||
|
|
||||||
|
The killer mismatch: this app **officially supports WebView 107+** and only *warns*
|
||||||
|
below 110 (`WebViewCompatibilityChecker.kt`: `MIN_CHROMIUM_VERSION = 107`,
|
||||||
|
`RECOMMENDED_CHROMIUM_VERSION = 110`). `SystemBars` edge-to-edge only engages at
|
||||||
|
WebView **140**. So **WebView 107–139 on API < 35** is a *supported* band where
|
||||||
|
`SystemBars` is a no-op.
|
||||||
|
|
||||||
|
The `@capawesome` plugin currently insets the WebView on **every** API/WebView
|
||||||
|
combination. Therefore **removing it without a replacement for the no-op band
|
||||||
|
regresses exactly the bug's device class** (Android 9 / API 28, `minSdkVersion 24`):
|
||||||
|
content slides under the bars and behind the keyboard, with zero inset
|
||||||
|
compensation. Note the app also force-enables edge-to-edge on API < 35 via
|
||||||
|
`@capacitor/status-bar`'s legacy `overlaysWebView: true` (see
|
||||||
|
`global-theme.service.ts:776–784`), so the OS will *not* fall back to insetting the
|
||||||
|
window for us there.
|
||||||
|
|
||||||
|
**Corollary for the keyboard bug:** moving to `SystemBars` only fixes the API<30
|
||||||
|
add-task-bar-behind-keyboard bug *if* the device has WebView ≥ 140. The reporter's
|
||||||
|
API 28 device may not. The native height workaround is pure geometry → reliable
|
||||||
|
regardless of WebView version. **Keep it.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision gates — do NOT start the spike until all hold
|
||||||
|
|
||||||
|
1. A **device/emulator matrix is actually runnable** for validation (it is not in
|
||||||
|
the current Claude sandbox: gradle + emulator are unavailable, so any migration
|
||||||
|
would land unvalidated on real devices — the precise failure mode of #8508).
|
||||||
|
2. A view on the **WebView-version distribution** of the Android user base (esp.
|
||||||
|
API < 35), or an accepted decision to regress the WebView 107–139 / API<35 band.
|
||||||
|
3. **Design sign-off** on the end-state bar appearance: `SystemBars` has **no color
|
||||||
|
API** (`setBackgroundColor()` = *Unsupported*, confirmed in
|
||||||
|
`node_modules/@capacitor/core/system-bars.md`), so the end state is transparent
|
||||||
|
bars with the web background showing through — a visible change from today's
|
||||||
|
opaque `#131314` / `#f8f8f7` overlays, across light/dark, cutout, gesture vs
|
||||||
|
3-button nav.
|
||||||
|
4. The reported keyboard bug is **already shipped** independently (PR #8528), so it
|
||||||
|
does not ride on this risky rewrite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Corrected migration steps (each device-validated before merge)
|
||||||
|
|
||||||
|
1. **Spike on a fresh branch off master.** Keep the keyboard fix independent.
|
||||||
|
|
||||||
|
2. **Config (`capacitor.config.ts`):** remove the `EdgeToEdge` config block, drop
|
||||||
|
`@capawesome/capacitor-android-edge-to-edge-support` from `android.includePlugins`
|
||||||
|
and from `package.json`, then `npx cap sync` (regenerates
|
||||||
|
`android/capacitor.settings.gradle` + `android/app/capacitor.build.gradle`; a
|
||||||
|
stale module reference is a hard build break, so don't hand-edit).
|
||||||
|
- Set `SystemBars.insetsHandling: 'css'`.
|
||||||
|
- **KEEP `windowOptOutEdgeToEdgeEnforcement=true`** (`values-v35/styles.xml`).
|
||||||
|
Removing it (the original plan's step 3) is **gratuitous extra risk**: on API
|
||||||
|
35+ it turns `window.{status,navigation}BarColor` into hard no-ops (so even the
|
||||||
|
custom `NavigationBarPlugin.setColor` stops coloring) and forces transparent
|
||||||
|
bars — a separate behavioral change from the plugin swap. Do enforcement
|
||||||
|
removal, if ever, as its own follow-up with design sign-off.
|
||||||
|
- The `Keyboard.resizeOnFullScreen: false` comment ("required when paired with
|
||||||
|
@capawesome edge-to-edge") becomes stale — fix the comment; the key is
|
||||||
|
effectively iOS-only (Android excludes `@capacitor/keyboard`).
|
||||||
|
|
||||||
|
3. **Resolve the `--safe-area-inset-*` writer collision (HIGH).** `insetsHandling:
|
||||||
|
'css'` injects `--safe-area-inset-{top,right,bottom,left}` onto
|
||||||
|
`document.documentElement` — the **exact** vars the app already writes in
|
||||||
|
`GlobalThemeService._initSafeAreaInsets()` and reads in `_css-variables.scss:51`.
|
||||||
|
Two writers on the same inline style = last-writer-wins, OS/timing-dependent.
|
||||||
|
Pick **one owner per platform**:
|
||||||
|
- Android, API 35+ / WebView ≥ 140: let `SystemBars` own them → **stop the JS
|
||||||
|
writes on Android**.
|
||||||
|
- Android, the no-op band (WebView 107–139 / API<35): `SystemBars` injects
|
||||||
|
nothing, so an `env()` fallback source is still required (the existing
|
||||||
|
`var(--safe-area-inset-*, env(...))` chain in `_css-variables.scss` already
|
||||||
|
provides it; the app's current Android top-deferral to `env(safe-area-inset-top)`
|
||||||
|
for #8283 must be preserved).
|
||||||
|
- Re-check `_patchCdkViewportForSafeArea()`: it `parseInt`s these vars via
|
||||||
|
`getComputedStyle`. Today the top resolves to the literal `"env(...)"` string →
|
||||||
|
`parseInt` yields 0 by design. Under `'css'`, `SystemBars` writes numeric px →
|
||||||
|
overlay (menu/select/autocomplete) top positioning **changes** on Android.
|
||||||
|
Re-test the full overlay matrix. Same for `task-context-menu` / `context-menu`
|
||||||
|
readers of `--safe-area-inset-top`.
|
||||||
|
|
||||||
|
4. **Cover the no-op band (the hard problem).** Decide explicitly — do not leave
|
||||||
|
implicit:
|
||||||
|
- (a) Accept the regression for WebView 107–139 / API<35 (only if that
|
||||||
|
population is negligible — needs data from gate #2). Simplest; risky.
|
||||||
|
- (b) Keep a **minimal native inset shim** for that band (partially defeats the
|
||||||
|
"drop the plugin" win, but bounded and self-owned).
|
||||||
|
- (c) Turn `overlaysWebView` **off on Android < 15** so the OS insets the window
|
||||||
|
normally there (opaque OS bars) — but this changes the look and re-enters #8283
|
||||||
|
territory; verify the top-inset fallback.
|
||||||
|
|
||||||
|
5. **Bar color.** Transparent bars + paint behind them:
|
||||||
|
`NavigationBarPlugin.setWebViewBackgroundColor` already sets the **window decor**
|
||||||
|
(`window.setBackgroundDrawable`) *and* the WebView surface — independent of
|
||||||
|
`@capawesome`, survives the migration, and is the lever for the color behind
|
||||||
|
transparent bars. Combined with the web `<body>` background filling the
|
||||||
|
safe-area zones and `SystemBars.setStyle` for light/dark icon content. Verify no
|
||||||
|
white gap (the #8508 / capawesome-#725 failure) on every API/theme.
|
||||||
|
|
||||||
|
6. **Keyboard.** Keep `adjustWebViewHeightForKeyboardBelowApi30` (PR #8528) — it
|
||||||
|
reads geometry only, so it is plugin-agnostic and WebView-version-independent.
|
||||||
|
Its height target (`rect.bottom − webViewTopOnScreen`) is independent of the
|
||||||
|
plugin, but `webViewTopOnScreen` shifts once the WebView is no longer natively
|
||||||
|
inset → re-validate on API 28/29. Do **not** assume core #8481 retires it.
|
||||||
|
|
||||||
|
7. **`StartupOverlayManager.kt` (HIGH, native, not hotfixable).** It derives
|
||||||
|
`webViewBottomInset` by *measuring the @capawesome-applied WebView margin*
|
||||||
|
(lines 106–138). With the WebView no longer inset, that measures ~0 and the
|
||||||
|
native startup quick-add bar drops behind the nav bar. Re-derive from system
|
||||||
|
insets — but note the current code comment (lines 43–46) explicitly **rejected**
|
||||||
|
`navigationBars()` because it mismatches the applied inset on gesture-nav
|
||||||
|
devices; design the new source carefully. Update the now-wrong freeze-during-IME
|
||||||
|
comment (lines 108–114).
|
||||||
|
|
||||||
|
8. **Keep for iOS:** `@capacitor/status-bar` (`StatusBar.setStyle` +
|
||||||
|
`overlaysWebView` + `contentInset:'never'`) and `capacitor-plugin-safe-area`
|
||||||
|
(the only iOS safe-area source). `SystemBars` `insetsHandling` is Android-only.
|
||||||
|
Don't let a cleanup pass remove the `StatusBar` import from
|
||||||
|
`global-theme.service.ts` (used on both platforms). Verify `SystemBars` does not
|
||||||
|
double-write CSS vars on iOS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## iOS invariants (must not break)
|
||||||
|
|
||||||
|
The migration is Android-only but `global-theme.service.ts` + `_css-variables.scss`
|
||||||
|
are shared. Keep: `StatusBar.overlaysWebView:true` + `ios.contentInset:'never'` +
|
||||||
|
`ios.backgroundColor` (content-under-notch on iOS), the iOS branch of
|
||||||
|
`_initSafeAreaInsets()` (`SafeArea.getSafeAreaInsets()` + `safeAreaChanged`), and
|
||||||
|
the iOS keyboard path in `_patchCdkViewportForSafeArea()`
|
||||||
|
(`--keyboard-overlay-offset`, gated on `body.isIOS`). Add iOS keyboard + notch +
|
||||||
|
overlays to the test matrix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation matrix (minimum, all UNVALIDATED in the dev sandbox)
|
||||||
|
|
||||||
|
- API 28/29 **split by WebView <140 vs ≥140**, API 30–34, API 35, API 36.
|
||||||
|
- Light + dark theme; gesture + 3-button nav; a device with a display cutout.
|
||||||
|
- Per cell: status/nav bar color (no white gap), content not under bars, add-task
|
||||||
|
bar vs keyboard (open/close), native startup-overlay alignment, rotation,
|
||||||
|
typed characters appear in order (the #8508 reversal check).
|
||||||
|
- iOS: notch, home indicator, keyboard, connected overlays.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-review findings to confirm on device (implementation, 2026-06-22)
|
||||||
|
|
||||||
|
A 3-agent review of the implementation diff found the merge resolution correct and
|
||||||
|
the migration mechanically clean (no leftovers, deps/gradle consistent, iOS
|
||||||
|
untouched). The residual risks are all device-matrix items — listed here so they
|
||||||
|
are explicitly checked, NOT blind-fixed (a blind fix risks re-creating #8508):
|
||||||
|
|
||||||
|
1. **API ≥ 35 + WebView < 140 double-count (narrow band).** In SystemBars'
|
||||||
|
non-passthrough branch (API ≥ 35) it `setPadding`s the WebView parent *and*
|
||||||
|
injects `--safe-area-inset-*`; if the web also pads via `var(--safe-area-*)`
|
||||||
|
that double-counts. The common API 36 case is WebView ≥ 140 = passthrough (no
|
||||||
|
static parent padding → no double-count), so this is the stale-WebView corner.
|
||||||
|
Verify on an API 35/36 device with an old WebView; if real, gate the web
|
||||||
|
padding off on that band rather than removing it globally.
|
||||||
|
2. **`env(safe-area-inset-bottom)` vs `var(--safe-area-bottom)` consumers diverge
|
||||||
|
on API ≥ 35.** Some SCSS (e.g. `mobile-bottom-nav`, `app.component`) reads raw
|
||||||
|
`env()`; others read `var(--safe-area-*)`. On API ≥ 35 SystemBars can zero the
|
||||||
|
passed-through insets while injecting real px into the vars, so the two
|
||||||
|
families disagree. Confirm the bottom-nav / add-task-bar spacing on API 35/36;
|
||||||
|
reconcile to one source per band if it's wrong.
|
||||||
|
3. **API 30–34 + WebView < 140 IME owner.** The native shim is gated `SDK_INT < 30`
|
||||||
|
(deliberate — newer APIs were observed to resize the window for the IME, and
|
||||||
|
insetting on top of that re-creates the #8508 squash). Under SystemBars,
|
||||||
|
WebView < 140 gets no IME padding below API 35. Verify whether the window still
|
||||||
|
resizes on API 30–34: if it does, no gap; if it does NOT, extend the shim to
|
||||||
|
`< 35 && WebView < 140` — but only after confirming on a device, never blind.
|
||||||
|
4. **CDK overlay / context-menu top position shifts on API ≥ 35.** `--safe-area-
|
||||||
|
inset-top` now resolves to real px there (was 0 on Android), so connected
|
||||||
|
overlays clamp below the status bar. Likely more correct; re-test the overlay
|
||||||
|
matrix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blast radius / rollback
|
||||||
|
|
||||||
|
- **Native (NOT remotely hotfixable):** plugin removal, `StartupOverlayManager.kt`,
|
||||||
|
`NavigationBarPlugin.kt`, `styles.xml`. A wrong inset or white-bar regression
|
||||||
|
needs a full Play Store release + staged rollout — the slow loop that made
|
||||||
|
#8295/#8508 painful (the git log shows #8508 regressed, was "fixed", then
|
||||||
|
reverted: `2a0cc73507` → `c247bc541a`).
|
||||||
|
- **Web (hotfixable):** the CSS-var/SCSS changes — but only if the symptom is
|
||||||
|
genuinely web-side; many are coupled to the native inset model.
|
||||||
|
- **Rollback:** re-adding `@capawesome` is a multi-file native revert (re-pin dep,
|
||||||
|
re-sync gradle, restore color calls + `StartupOverlayManager` coupling), not a
|
||||||
|
one-line flip. Open a tracking issue and keep the revert documented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Capacitor core: `ionic-team/capacitor#8466` (insetsHandling breaks WebView on
|
||||||
|
API 29 + keyboard), fixed for built-in `SystemBars` by core PR **#8481** (merged).
|
||||||
|
- Plugin: `capawesome-team/capacitor-plugins` #845/#490/#596/#725/#819/#812,
|
||||||
|
**#847 open**; plugin PR **#848** (open/unreleased; its "API 29 resizes via
|
||||||
|
adjustResize" premise did not match this app's on-device logcat).
|
||||||
|
- App history: #8295, #8508 (`docs/android-edge-to-edge-keyboard.md`); the keyboard
|
||||||
|
fix is PR **#8528**.
|
||||||
20
package-lock.json
generated
20
package-lock.json
generated
|
|
@ -17,7 +17,6 @@
|
||||||
"@capacitor/ios": "^8.3.4",
|
"@capacitor/ios": "^8.3.4",
|
||||||
"@capacitor/keyboard": "8.0.1",
|
"@capacitor/keyboard": "8.0.1",
|
||||||
"@capacitor/status-bar": "^8.0.2",
|
"@capacitor/status-bar": "^8.0.2",
|
||||||
"@capawesome/capacitor-android-edge-to-edge-support": "^8.0.8",
|
|
||||||
"@material-symbols/font-400": "^0.44.10",
|
"@material-symbols/font-400": "^0.44.10",
|
||||||
"@noble/ciphers": "^2.2.0",
|
"@noble/ciphers": "^2.2.0",
|
||||||
"capacitor-plugin-safe-area": "^5.0.0",
|
"capacitor-plugin-safe-area": "^5.0.0",
|
||||||
|
|
@ -3045,25 +3044,6 @@
|
||||||
"@capacitor/core": ">=8.0.0"
|
"@capacitor/core": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@capawesome/capacitor-android-edge-to-edge-support": {
|
|
||||||
"version": "8.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@capawesome/capacitor-android-edge-to-edge-support/-/capacitor-android-edge-to-edge-support-8.0.8.tgz",
|
|
||||||
"integrity": "sha512-+eKz15uhfOwft5IRZFL1QQZT/FHeeE5scNnlP9CIMrhW0Ctc9cJM13Ec2vOTd9oYGz3mCF3kf39C2spDDuSUVw==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/capawesome-team/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/capawesome"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@capacitor/core": ">=8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@capawesome/capacitor-background-task": {
|
"node_modules/@capawesome/capacitor-background-task": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@capawesome/capacitor-background-task/-/capacitor-background-task-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@capawesome/capacitor-background-task/-/capacitor-background-task-8.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,6 @@
|
||||||
"@capacitor/ios": "^8.3.4",
|
"@capacitor/ios": "^8.3.4",
|
||||||
"@capacitor/keyboard": "8.0.1",
|
"@capacitor/keyboard": "8.0.1",
|
||||||
"@capacitor/status-bar": "^8.0.2",
|
"@capacitor/status-bar": "^8.0.2",
|
||||||
"@capawesome/capacitor-android-edge-to-edge-support": "^8.0.8",
|
|
||||||
"@material-symbols/font-400": "^0.44.10",
|
"@material-symbols/font-400": "^0.44.10",
|
||||||
"@noble/ciphers": "^2.2.0",
|
"@noble/ciphers": "^2.2.0",
|
||||||
"capacitor-plugin-safe-area": "^5.0.0",
|
"capacitor-plugin-safe-area": "^5.0.0",
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ import { CapacitorPlatformService } from '../platform/capacitor-platform.service
|
||||||
import { Keyboard, KeyboardInfo } from '@capacitor/keyboard';
|
import { Keyboard, KeyboardInfo } from '@capacitor/keyboard';
|
||||||
import { PluginListenerHandle, registerPlugin } from '@capacitor/core';
|
import { PluginListenerHandle, registerPlugin } from '@capacitor/core';
|
||||||
import { StatusBar, Style } from '@capacitor/status-bar';
|
import { StatusBar, Style } from '@capacitor/status-bar';
|
||||||
import { EdgeToEdge } from '@capawesome/capacitor-android-edge-to-edge-support';
|
|
||||||
import { SafeArea } from 'capacitor-plugin-safe-area';
|
import { SafeArea } from 'capacitor-plugin-safe-area';
|
||||||
import { FlexibleConnectedPositionStrategy } from '@angular/cdk/overlay';
|
import { FlexibleConnectedPositionStrategy } from '@angular/cdk/overlay';
|
||||||
import { LS } from '../persistence/storage-keys.const';
|
import { LS } from '../persistence/storage-keys.const';
|
||||||
|
|
@ -759,49 +758,25 @@ export class GlobalThemeService {
|
||||||
root.style.setProperty(CSS_VAR_SAFE_AREA_RIGHT, `${insets.right}px`);
|
root.style.setProperty(CSS_VAR_SAFE_AREA_RIGHT, `${insets.right}px`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// On Android (targetSdk 35+, edge-to-edge enforced) the
|
// On Android the WebView now draws edge-to-edge (the @capawesome plugin that
|
||||||
// @capawesome/capacitor-android-edge-to-edge-support plugin already insets
|
// used to inset it via native margins was removed in favour of Capacitor's
|
||||||
// the WebView below the status bar and above the navigation bar via native
|
// built-in SystemBars). The --safe-area-inset-* vars are no longer written
|
||||||
// margins. capacitor-plugin-safe-area reports the decorView's full
|
// from JS on Android — that would race SystemBars on the same documentElement
|
||||||
// system-bar insets regardless, so applying them as CSS padding on top of
|
// inline style (last-writer-wins, OS/timing dependent). Each band resolves
|
||||||
// the native margin double-counts the inset (visible as excessive padding
|
// them on its own (verified against the bundled SystemBars.java):
|
||||||
// above the top bar). The WebView interior is fully safe there, so keep the
|
// - API >= 35: SystemBars *injects* the real px into --safe-area-inset-*.
|
||||||
// bottom/side safe-area CSS vars at 0; only iOS (contentInset: 'never')
|
// - WebView >= 140 (any API): SystemBars passes the native insets through,
|
||||||
// needs the WebView to pad itself. A few styles read env(safe-area-inset-
|
// so the WebView's own env(safe-area-inset-*) is correct (no injection
|
||||||
// bottom) directly (e.g. mobile-bottom-nav) rather than these vars; inside
|
// below API 35).
|
||||||
// the natively-inset WebView that env value is expected to be ~0, keeping
|
// - WebView < 140 / API < 35 tail: SystemBars does nothing here.
|
||||||
// them consistent with the pinned vars here.
|
// In every case the SCSS fallback `var(--safe-area-inset-*, env(...))` in
|
||||||
//
|
// _css-variables.scss resolves to the injected px when present, else to
|
||||||
// The TOP is the exception. On Android < 15 the WebView is forced
|
// env(). With viewport-fit=cover env(safe-area-inset-top) equals the
|
||||||
// edge-to-edge by @capacitor/status-bar's legacy `overlaysWebView`
|
// status-bar height when the WebView extends under it — exactly the #8283 top
|
||||||
// fullscreen flag (a no-op on Android 15+), and the plugin's native top
|
// fallback, preserved automatically by not pinning the var here.
|
||||||
// margin is not reliably applied on every OS/ROM — the header then draws
|
// Only iOS (contentInset: 'never') still needs JS-fed insets from
|
||||||
// behind the status bar (#8283, seen on Android 14). Pinning the top var to
|
// capacitor-plugin-safe-area; SystemBars insetsHandling is Android-only.
|
||||||
// 0 leaves no fallback. Instead defer the top to the WebView's own
|
if (!this._platformService.isAndroid()) {
|
||||||
// env(safe-area-inset-top): with viewport-fit=cover it equals the status-bar
|
|
||||||
// height exactly when the WebView extends under it, and resolves to 0 once
|
|
||||||
// the WebView is already inset (Android 15+ / native margin applied) — so it
|
|
||||||
// self-corrects across OS versions without ever double-counting.
|
|
||||||
if (this._platformService.isAndroid()) {
|
|
||||||
applyInsets({ top: 0, right: 0, bottom: 0, left: 0 });
|
|
||||||
// Override only the top with the env() fallback (see above). NOTE: this
|
|
||||||
// self-corrects only for *CSS* consumers — `padding-top: var(--safe-area-top)`
|
|
||||||
// resolves the nested env() at use-time, so the header padding is right. JS
|
|
||||||
// readers that parse this custom property via getComputedStyle (e.g.
|
|
||||||
// _patchCdkViewportForSafeArea, the context menus) get the *unresolved*
|
|
||||||
// "env(...)" token string back — env()/var() are only substituted when the
|
|
||||||
// property is actually used, not when a custom property's own value is read —
|
|
||||||
// so parseInt() yields 0, the same top inset those readers already used on
|
|
||||||
// Android before this change. Connected overlays are therefore not pushed
|
|
||||||
// below the status bar; only the header padding is. That is the scope of
|
|
||||||
// #8283 (the header was the reported regression). If overlay top-insets ever
|
|
||||||
// matter on Android, register the var via @property or read a probe element's
|
|
||||||
// resolved padding-top instead of the raw custom property.
|
|
||||||
this.document.documentElement.style.setProperty(
|
|
||||||
CSS_VAR_SAFE_AREA_TOP,
|
|
||||||
'env(safe-area-inset-top, 0px)',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
SafeArea.getSafeAreaInsets().then(({ insets }) => applyInsets(insets));
|
SafeArea.getSafeAreaInsets().then(({ insets }) => applyInsets(insets));
|
||||||
SafeArea.addListener('safeAreaChanged', ({ insets }) => applyInsets(insets));
|
SafeArea.addListener('safeAreaChanged', ({ insets }) => applyInsets(insets));
|
||||||
}
|
}
|
||||||
|
|
@ -853,29 +828,27 @@ export class GlobalThemeService {
|
||||||
});
|
});
|
||||||
if (this._platformService.isAndroid()) {
|
if (this._platformService.isAndroid()) {
|
||||||
const bgColor = isDark ? '#131314' : '#f8f8f7';
|
const bgColor = isDark ? '#131314' : '#f8f8f7';
|
||||||
// Under enforced edge-to-edge (targetSdk 35+) Window.setStatusBarColor /
|
// The @capawesome edge-to-edge plugin (which painted opaque bar overlays
|
||||||
// setNavigationBarColor are no-ops; the edge-to-edge support plugin owns
|
// via EdgeToEdge.set{Status,Navigation}BarColor) was removed in favour of
|
||||||
// the bar backgrounds via its own overlay views. Color them through it
|
// Capacitor's built-in SystemBars. SystemBars has NO bar-color API — the
|
||||||
// so the status bar and the bottom navigation/gesture area match the
|
// edge-to-edge model is transparent bars with the web content drawn
|
||||||
// theme background.
|
// behind them. The bar backgrounds are therefore painted by:
|
||||||
EdgeToEdge.setStatusBarColor({ color: bgColor }).catch((err) => {
|
// - setWebViewBackgroundColor below (window decor + WebView surface),
|
||||||
Log.warn('Failed to set status bar color', err);
|
// which shows through the transparent bars (the color backstop on
|
||||||
});
|
// API 35+ where window.*BarColor is a no-op), and
|
||||||
EdgeToEdge.setNavigationBarColor({ color: bgColor }).catch((err) => {
|
// - NavigationBar.setColor's window.navigationBarColor, still effective
|
||||||
Log.warn('Failed to set navigation bar color', err);
|
// on API < 35, plus its setSystemBarsAppearance which drives the nav
|
||||||
});
|
// bar icon light/dark on all versions.
|
||||||
// The custom NavigationBar plugin still drives the nav bar icon/pill
|
// Status-bar icon light/dark is set via StatusBar.setStyle above.
|
||||||
// appearance (light vs dark) via setSystemBarsAppearance, which remains
|
|
||||||
// effective on Android 15+; the window.navigationBarColor it also sets
|
|
||||||
// is a harmless no-op there.
|
|
||||||
NavigationBar.setColor({
|
NavigationBar.setColor({
|
||||||
color: bgColor,
|
color: bgColor,
|
||||||
style: isDark ? 'DARK' : 'LIGHT',
|
style: isDark ? 'DARK' : 'LIGHT',
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
Log.warn('Failed to set navigation bar appearance', err);
|
Log.warn('Failed to set navigation bar appearance', err);
|
||||||
});
|
});
|
||||||
// Keep the native WebView surface matched to the theme so the
|
// Paint the WebView surface and window decor with the theme background so
|
||||||
// adjustResize keyboard animation can't flash white between frames.
|
// the transparent system bars show the theme color behind them and the
|
||||||
|
// keyboard animation can't flash white between frames.
|
||||||
NavigationBar.setWebViewBackgroundColor({ color: bgColor }).catch((err) => {
|
NavigationBar.setWebViewBackgroundColor({ color: bgColor }).catch((err) => {
|
||||||
Log.warn('Failed to set web view background color', err);
|
Log.warn('Failed to set web view background color', err);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue