Skip to content

Commit 81ae30a

Browse files
authored
feat(android): Improving SystemBars inset handling (#8268)
1 parent e6f50b8 commit 81ae30a

File tree

9 files changed

+144
-48
lines changed

9 files changed

+144
-48
lines changed

android/capacitor/src/main/assets/native-bridge.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,12 @@ var nativeBridge = (function (exports) {
367367
}
368368
};
369369
const platform = getPlatformId(win);
370+
if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
371+
// add DOM ready listener for System Bars
372+
document.addEventListener('DOMContentLoaded', function () {
373+
win.CapacitorSystemBarsAndroidInterface.onDOMReady();
374+
});
375+
}
370376
if (platform == 'android' || platform == 'ios') {
371377
// patch document.cookie on Android/iOS
372378
win.CapacitorCookiesDescriptor =

android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,16 @@ public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail)
102102

103103
return result;
104104
}
105+
106+
@Override
107+
public void onPageCommitVisible(WebView view, String url) {
108+
super.onPageCommitVisible(view, url);
109+
110+
List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
111+
if (webViewListeners != null) {
112+
for (WebViewListener listener : bridge.getWebViewListeners()) {
113+
listener.onPageCommitVisible(view, url);
114+
}
115+
}
116+
}
105117
}

android/capacitor/src/main/java/com/getcapacitor/WebViewListener.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,14 @@ public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail deta
5454
// Override me to add behavior to the web view render process gone event
5555
return false;
5656
}
57+
58+
/**
59+
* Callback for page start event.
60+
*
61+
* @param view The WebView for which the navigation occurred.
62+
* @param url The URL corresponding to the page navigation that triggered this callback.
63+
*/
64+
public void onPageCommitVisible(WebView view, String url) {
65+
// Override me to add behavior to handle the onPageCommitVisible event
66+
}
5767
}

android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java

Lines changed: 89 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
package com.getcapacitor.plugin;
22

3-
import android.content.pm.PackageInfo;
43
import android.content.res.Configuration;
4+
import android.os.Build;
55
import android.view.View;
6+
import android.view.ViewGroup;
67
import android.view.Window;
8+
import android.webkit.JavascriptInterface;
9+
import android.webkit.WebView;
710
import androidx.core.graphics.Insets;
811
import androidx.core.view.ViewCompat;
912
import androidx.core.view.WindowCompat;
1013
import androidx.core.view.WindowInsetsCompat;
1114
import androidx.core.view.WindowInsetsControllerCompat;
12-
import androidx.webkit.WebViewCompat;
1315
import com.getcapacitor.Plugin;
1416
import com.getcapacitor.PluginCall;
1517
import com.getcapacitor.PluginMethod;
18+
import com.getcapacitor.WebViewListener;
1619
import com.getcapacitor.annotation.CapacitorPlugin;
1720
import java.util.Locale;
18-
import java.util.regex.Matcher;
19-
import java.util.regex.Pattern;
2021

2122
@CapacitorPlugin
2223
public class SystemBars extends Plugin {
@@ -27,6 +28,9 @@ public class SystemBars extends Plugin {
2728
static final String BAR_STATUS_BAR = "StatusBar";
2829
static final String BAR_GESTURE_BAR = "NavigationBar";
2930

31+
static final String INSETS_HANDLING_CSS = "css";
32+
static final String INSETS_HANDLING_DISABLE = "disable";
33+
3034
static final String viewportMetaJSFunction = """
3135
function capacitorSystemBarsCheckMetaViewport() {
3236
const meta = document.querySelectorAll("meta[name=viewport]");
@@ -37,42 +41,52 @@ function capacitorSystemBarsCheckMetaViewport() {
3741
const metaContent = meta[meta.length - 1].content;
3842
return metaContent.includes("viewport-fit=cover");
3943
}
40-
4144
capacitorSystemBarsCheckMetaViewport();
4245
""";
4346

47+
private boolean insetHandlingEnabled = true;
48+
private boolean hasViewportCover = false;
49+
4450
@Override
4551
public void load() {
52+
getBridge().getWebView().addJavascriptInterface(this, "CapacitorSystemBarsAndroidInterface");
4653
super.load();
54+
4755
initSystemBars();
4856
}
4957

50-
private boolean hasFixedWebView() {
51-
PackageInfo packageInfo = WebViewCompat.getCurrentWebViewPackage(bridge.getContext());
52-
Pattern pattern = Pattern.compile("(\\d+)");
53-
Matcher matcher = pattern.matcher(packageInfo.versionName);
54-
55-
if (!matcher.find()) {
56-
return false;
57-
}
58-
59-
String majorVersionStr = matcher.group(0);
60-
int majorVersion = Integer.parseInt(majorVersionStr);
58+
@Override
59+
protected void handleOnStart() {
60+
super.handleOnStart();
61+
62+
this.getBridge().addWebViewListener(
63+
new WebViewListener() {
64+
@Override
65+
public void onPageCommitVisible(WebView view, String url) {
66+
super.onPageCommitVisible(view, url);
67+
getBridge().getWebView().requestApplyInsets();
68+
}
69+
}
70+
);
71+
}
6172

62-
return majorVersion >= 140;
73+
@Override
74+
protected void handleOnConfigurationChanged(Configuration newConfig) {
75+
super.handleOnConfigurationChanged(newConfig);
76+
setStyle(STYLE_DEFAULT, "");
6377
}
6478

6579
private void initSystemBars() {
6680
String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase(Locale.US);
6781
boolean hidden = getConfig().getBoolean("hidden", false);
68-
boolean disableCSSInsets = getConfig().getBoolean("disableInsets", false);
6982

70-
this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
71-
boolean hasMetaViewportCover = res.equals("true");
72-
if (!disableCSSInsets) {
73-
setupSafeAreaInsets(this.hasFixedWebView(), hasMetaViewportCover);
74-
}
75-
});
83+
String insetsHandling = getConfig().getString("insetsHandling", "css");
84+
if (insetsHandling.equals(INSETS_HANDLING_DISABLE)) {
85+
insetHandlingEnabled = false;
86+
}
87+
88+
initWindowInsetsListener();
89+
initSafeAreaInsets();
7690

7791
getBridge().executeOnMainThread(() -> {
7892
setStyle(style, "");
@@ -116,28 +130,63 @@ public void setAnimation(final PluginCall call) {
116130
call.resolve();
117131
}
118132

119-
private void setupSafeAreaInsets(boolean hasFixedWebView, boolean hasMetaViewportCover) {
120-
ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> {
121-
if (hasFixedWebView && hasMetaViewportCover) {
122-
return insets;
123-
}
133+
@JavascriptInterface
134+
public void onDOMReady() {
135+
getActivity().runOnUiThread(() -> {
136+
this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
137+
hasViewportCover = res.equals("true");
124138

125-
Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
126-
Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
127-
boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
139+
getBridge().getWebView().requestApplyInsets();
140+
});
141+
});
142+
}
128143

129-
int bottomInsets = safeArea.bottom;
144+
private Insets calcSafeAreaInsets(WindowInsetsCompat insets) {
145+
Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
146+
return Insets.of(safeArea.left, safeArea.top, safeArea.right, safeArea.bottom);
147+
}
130148

131-
if (keyboardVisible) {
132-
// When https://issues.chromium.org/issues/457682720 is fixed and released,
133-
// add behind a WebView version check
134-
bottomInsets = imeInsets.bottom - bottomInsets;
149+
private void initSafeAreaInsets() {
150+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && insetHandlingEnabled) {
151+
View v = (View) this.getBridge().getWebView().getParent();
152+
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(v);
153+
if (insets != null) {
154+
Insets safeAreaInsets = calcSafeAreaInsets(insets);
155+
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
135156
}
157+
}
158+
}
136159

137-
injectSafeAreaCSS(safeArea.top, safeArea.right, bottomInsets, safeArea.left);
160+
private void initWindowInsetsListener() {
161+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && insetHandlingEnabled) {
162+
ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> {
163+
if (hasViewportCover) {
164+
Insets safeAreaInsets = calcSafeAreaInsets(insets);
165+
boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
138166

139-
return WindowInsetsCompat.CONSUMED;
140-
});
167+
if (keyboardVisible) {
168+
Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
169+
setViewMargins(v, Insets.of(0, 0, 0, imeInsets.bottom));
170+
} else {
171+
setViewMargins(v, Insets.NONE);
172+
}
173+
174+
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
175+
return WindowInsetsCompat.CONSUMED;
176+
}
177+
178+
return insets;
179+
});
180+
}
181+
}
182+
183+
private void setViewMargins(View v, Insets insets) {
184+
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
185+
mlp.leftMargin = insets.left;
186+
mlp.bottomMargin = insets.bottom;
187+
mlp.rightMargin = insets.right;
188+
mlp.topMargin = insets.top;
189+
v.setLayoutParams(mlp);
141190
}
142191

143192
private void injectSafeAreaCSS(int top, int right, int bottom, int left) {

cli/src/declarations.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -707,11 +707,17 @@ export interface PluginsConfig {
707707
*/
708708
SystemBars?: {
709709
/**
710-
* Disables the injection of device css insets into the web view.
710+
* Specifies how to handle problematic insets on Android.
711711
*
712-
* @default false
712+
* This option is only supported on Android.
713+
*
714+
* `css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
715+
*
716+
* `disable` = Disable all inset handling.
717+
*
718+
* @default "css"
713719
*/
714-
disableInsets?: boolean;
720+
insetsHandling?: 'css' | 'disable';
715721
/**
716722
* The style of the text and icons of the system bars.
717723
*

core/native-bridge.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,13 @@ const initBridge = (w: any): void => {
377377

378378
const platform = getPlatformId(win);
379379

380+
if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
381+
// add DOM ready listener for System Bars
382+
document.addEventListener('DOMContentLoaded', function () {
383+
win.CapacitorSystemBarsAndroidInterface.onDOMReady();
384+
});
385+
}
386+
380387
if (platform == 'android' || platform == 'ios') {
381388
// patch document.cookie on Android/iOS
382389
win.CapacitorCookiesDescriptor =

core/src/definitions-internal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export interface CapacitorCustomPlatformInstance {
142142

143143
export interface WindowCapacitor {
144144
Capacitor?: CapacitorInstance;
145+
CapacitorSystemBarsAndroidInterface?: any;
145146
CapacitorCookiesAndroidInterface?: any;
146147
CapacitorCookiesDescriptor?: PropertyDescriptor;
147148
CapacitorHttpAndroidInterface?: any;

core/system-bars.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ html {
3232
padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
3333
}
3434
```
35-
36-
To disable the inset variable injections, set the configuration setting `disableInsets` to `true`.
35+
To control this behavior, use the `insetsHandling` configuration setting.
3736

3837
## Example
3938

@@ -72,7 +71,7 @@ const setStatusBarAnimation = async () => {
7271
## Configuration
7372
| Prop | Type | Description | Default |
7473
| ------------- | -------------------- | ------------------------------------------------------------------------- | ------------------ |
75-
| **`disableInsets`** | <code>boolean</code> | Disables the injection of device css insets into the webview. This option is only supported on Android. | <code>false</code> |
74+
| **`insetsHandling`** | <code>string</code> | Specifies how to handle problematic insets on Android. This option is only supported on Android.<br>`css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.<br>`disable` = Disable all inset handling. | <code>css</code> |
7675
| **`style`** | <code>string</code> | The style of the text and icons of the system bars. | <code>DEFAULT</code> |
7776
| **`hidden`** | <code>boolean</code> | Hide the system bars on start. | <code>false</code> |
7877
| **`animation`** | <code>string</code> | The type of status bar animation used when showing or hiding. This option is only supported on iOS. | <code>FADE</code> |
@@ -86,7 +85,7 @@ In `capacitor.config.json`:
8685
{
8786
"plugins": {
8887
"SystemBars": {
89-
"disableInsets": true,
88+
"insetsHandling": "css",
9089
"style": "DARK",
9190
"hidden": false,
9291
"animation": "NONE"
@@ -103,7 +102,7 @@ import { CapacitorConfig } from '@capacitor/cli';
103102
const config: CapacitorConfig = {
104103
plugins: {
105104
SystemBars: {
106-
disableInsets: true,
105+
insetsHandling: "css",
107106
style: "DARK",
108107
hidden: false,
109108
animation: "NONE"

ios/Capacitor/Capacitor/assets/native-bridge.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,12 @@ var nativeBridge = (function (exports) {
367367
}
368368
};
369369
const platform = getPlatformId(win);
370+
if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
371+
// add DOM ready listener for System Bars
372+
document.addEventListener('DOMContentLoaded', function () {
373+
win.CapacitorSystemBarsAndroidInterface.onDOMReady();
374+
});
375+
}
370376
if (platform == 'android' || platform == 'ios') {
371377
// patch document.cookie on Android/iOS
372378
win.CapacitorCookiesDescriptor =

0 commit comments

Comments
 (0)