mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
fix(android): inset WebView for IME under enforced edge-to-edge (draft)
Durable follow-up to the #8295 revert. On targetSdk 36 (Android 16) edge-to-edge is mandatory and adjustResize is a no-op for the IME, so the WebView only stays above the soft keyboard if @capawesome/capacitor-android-edge-to-edge-support insets it. The plugin instead zeroes the WebView bottom margin while the keyboard is visible (assuming the system resized the window), leaving fixed content behind the IME. patch-package the plugin's EdgeToEdge.applyInsetsInternal so the WebView bottom margin is always max(imeInsets.bottom, systemBarsInsets.bottom). The WebView then shrinks above the keyboard, content resizes, and --keyboard-height stays 0 with the add-task bar above the IME on the existing JS path. Wire patch-package via a postinstall script; run 'npm install' to apply (also refreshes the lockfile). DRAFT: verified the patch applies cleanly, but the gradle build and on-device behavior are UNVERIFIED here. Needs the Android 10/14/15/16 matrix in docs/android-edge-to-edge-keyboard.md before merge; upstream so the patch can be dropped.
This commit is contained in:
parent
43444e6b53
commit
5497212b99
3 changed files with 119 additions and 0 deletions
94
docs/android-edge-to-edge-keyboard.md
Normal file
94
docs/android-edge-to-edge-keyboard.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Android edge-to-edge + soft keyboard (IME)
|
||||
|
||||
Why the global add-task bar sits above the keyboard, and how the pieces fit.
|
||||
Read this before touching anything keyboard/IME-related on Android — this area
|
||||
has regressed twice.
|
||||
|
||||
## How the bar is positioned
|
||||
|
||||
The global add-task bar is `position: fixed` and lifted off the bottom by a CSS
|
||||
variable only:
|
||||
|
||||
```scss
|
||||
// add-task-bar.component.scss
|
||||
:host-context(.isTouchOnly).global {
|
||||
bottom: calc(var(--keyboard-height) + var(--s2));
|
||||
}
|
||||
```
|
||||
|
||||
`--keyboard-height` defaults to `0px`. On Android/web it is set by
|
||||
`GlobalThemeService._initVisualViewportKeyboardTracking()` from
|
||||
`obscured = window.innerHeight - visualViewport.height`. On iOS the Capacitor
|
||||
Keyboard plugin sets it.
|
||||
|
||||
So the bar only floats above the keyboard if **either** the WebView shrinks
|
||||
(then `bottom: 0` is already above the IME) **or** the visual viewport shrinks
|
||||
(then `--keyboard-height` lifts the bar). If neither happens, the bar sits
|
||||
behind the keyboard.
|
||||
|
||||
## The trap: targetSdk 35+ / edge-to-edge
|
||||
|
||||
On `targetSdkVersion >= 35` (we are on 36, Android 16) edge-to-edge is
|
||||
**mandatory** and `android:windowSoftInputMode="adjustResize"` is a **no-op for
|
||||
the IME** — the system no longer resizes the window when the keyboard opens.
|
||||
`android:windowOptOutEdgeToEdgeEnforcement` only exists on API 35 (see
|
||||
`res/values-v35/styles.xml`) and is ignored on API 36, so there is no opt-out.
|
||||
|
||||
Under enforced edge-to-edge the **only** thing that can shrink the WebView for
|
||||
the IME is `@capawesome/capacitor-android-edge-to-edge-support`, which applies
|
||||
the IME inset as the WebView's bottom margin via a single
|
||||
`OnApplyWindowInsetsListener`.
|
||||
|
||||
## The plugin bug (fixed via patch-package)
|
||||
|
||||
`EdgeToEdge.applyInsetsInternal()` ships with:
|
||||
|
||||
```java
|
||||
// When keyboard is visible, don't apply bottom margin to avoid double-counting
|
||||
// (the system already resizes the window for the keyboard)
|
||||
int bottomMargin = keyboardVisible ? 0 : Math.max(imeInsets.bottom, systemBarsInsets.bottom);
|
||||
```
|
||||
|
||||
That comment's premise — _"the system already resizes the window"_ — is false on
|
||||
Android 16. When the keyboard opens the plugin sets `bottomMargin = 0`, the
|
||||
WebView stays full height, nothing resizes, and the fixed add-task bar ends up
|
||||
behind the IME.
|
||||
|
||||
`patches/@capawesome+capacitor-android-edge-to-edge-support+8.0.8.patch` drops
|
||||
the `keyboardVisible ? 0` special case:
|
||||
|
||||
```java
|
||||
int bottomMargin = Math.max(imeInsets.bottom, systemBarsInsets.bottom);
|
||||
```
|
||||
|
||||
`imeInsets.bottom` already spans the navigation-bar area while the keyboard is
|
||||
up, and an edge-to-edge window never resizes for the IME, so a single `max()` is
|
||||
correct in both states with no double-counting. With the WebView inset above the
|
||||
keyboard, the content genuinely resizes and `--keyboard-height` stays `0` — the
|
||||
bar lands at `bottom: var(--s2)` just above the IME, on the existing JS path with
|
||||
no client changes.
|
||||
|
||||
Apply via `npm install` (runs `patch-package` in `postinstall`). Upstream this
|
||||
to the plugin so the patch can be dropped.
|
||||
|
||||
## What NOT to do
|
||||
|
||||
Do not stack a second/third keyboard-height source on top of the VisualViewport
|
||||
signal (native physical-px height + a `baseInnerHeight`-tracking path combined as
|
||||
`max(obscured, nativeKeyboardHeight - layoutShrink)`). That was #8295; the
|
||||
sources race on separate async events, the baseline gets reset to the shrunk
|
||||
`innerHeight` mid-animation, the double-count guard collapses, and the bar is
|
||||
mispositioned. It was reverted. Fix the inset at the source (the WebView size),
|
||||
not with JS heuristics layered on top.
|
||||
|
||||
## Device test matrix (required before merging IME changes)
|
||||
|
||||
Heuristic stacking looks fine on one device and breaks on another — test the
|
||||
add-task bar opening the keyboard, and content resizing, on:
|
||||
|
||||
- Android 10 (API 29) — pre-edge-to-edge; `Type.ime()` insets are unreliable here
|
||||
- Android 14 (API 34) — edge-to-edge opt-out still possible
|
||||
- Android 15 (API 35) — edge-to-edge enforced, `windowOptOutEdgeToEdgeEnforcement` honored
|
||||
- Android 16 (API 36) — edge-to-edge mandatory, no opt-out (our target)
|
||||
|
||||
Both gesture-nav and 3-button-nav, light and dark.
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
"assemble:android:prod": "cd android && ./gradlew assembleRelease && cd ..",
|
||||
"assemble:android:stage": "cd android && ./gradlew assembleDebug && cd ..",
|
||||
"prebuild": "npm run env && node ./tools/git-version.js && npm run build:packages",
|
||||
"postinstall": "patch-package",
|
||||
"prepare": "husky && ts-patch install && npm run env && npm run sync-core:build && npm run sync-providers:build && npm run shared-schema:build && npm run plugin-api:build",
|
||||
"build": "npm run buildAllElectron:noTests:prod",
|
||||
"build:packages": "node ./packages/build-packages.js",
|
||||
|
|
@ -298,6 +299,7 @@
|
|||
"new-github-issue-url": "^1.1.0",
|
||||
"ng2-charts": "^10.0.0",
|
||||
"ngx-markdown": "^21.2.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"playwright": "^1.59.1",
|
||||
"prettier": "^3.8.3",
|
||||
"pretty-quick": "^4.2.2",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
diff --git a/node_modules/@capawesome/capacitor-android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdge.java b/node_modules/@capawesome/capacitor-android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdge.java
|
||||
index 0000001..0000002 100644
|
||||
--- a/node_modules/@capawesome/capacitor-android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdge.java
|
||||
+++ b/node_modules/@capawesome/capacitor-android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdge.java
|
||||
@@ -91,10 +91,14 @@ public class EdgeToEdge {
|
||||
private void applyInsetsInternal(View view, WindowInsetsCompat currentInsets) {
|
||||
Insets systemBarsInsets = currentInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
Insets imeInsets = currentInsets.getInsets(WindowInsetsCompat.Type.ime());
|
||||
- boolean keyboardVisible = currentInsets.isVisible(WindowInsetsCompat.Type.ime());
|
||||
- // When keyboard is visible, don't apply bottom margin to avoid double-counting
|
||||
- // (the system already resizes the window for the keyboard)
|
||||
- int bottomMargin = keyboardVisible ? 0 : Math.max(imeInsets.bottom, systemBarsInsets.bottom);
|
||||
+ // Edge-to-edge is enforced on targetSdk 35+ (Android 15/16), where the
|
||||
+ // system no longer resizes the window for the IME (adjustResize is a
|
||||
+ // no-op). The WebView must therefore be inset by the keyboard height
|
||||
+ // itself, or position:fixed content (e.g. the add-task bar) sits behind
|
||||
+ // the keyboard. imeInsets.bottom already spans the nav-bar area while the
|
||||
+ // keyboard is up, so one max() is correct in both states; an edge-to-edge
|
||||
+ // window never resizes for the IME, so nothing is double-counted.
|
||||
+ int bottomMargin = Math.max(imeInsets.bottom, systemBarsInsets.bottom);
|
||||
|
||||
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
|
||||
mlp.bottomMargin = bottomMargin;
|
||||
Loading…
Add table
Add a link
Reference in a new issue