Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions android/capacitor/src/main/assets/native-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@ var nativeBridge = (function (exports) {
}
};
const platform = getPlatformId(win);
if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
// add DOM ready listener for System Bars
document.addEventListener('DOMContentLoaded', function () {
win.CapacitorSystemBarsAndroidInterface.onDOMReady();
});
}
if (platform == 'android' || platform == 'ios') {
// patch document.cookie on Android/iOS
win.CapacitorCookiesDescriptor =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,16 @@ public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail)

return result;
}

@Override
public void onPageCommitVisible(WebView view, String url) {
super.onPageCommitVisible(view, url);

List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
if (webViewListeners != null) {
for (WebViewListener listener : bridge.getWebViewListeners()) {
listener.onPageCommitVisible(view, url);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,14 @@ public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail deta
// Override me to add behavior to the web view render process gone event
return false;
}

/**
* Callback for page start event.
*
* @param view The WebView for which the navigation occurred.
* @param url The URL corresponding to the page navigation that triggered this callback.
*/
public void onPageCommitVisible(WebView view, String url) {
// Override me to add behavior to handle the onPageCommitVisible event
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
package com.getcapacitor.plugin;

import android.content.pm.PackageInfo;
import android.content.res.Configuration;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.webkit.WebViewCompat;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.WebViewListener;
import com.getcapacitor.annotation.CapacitorPlugin;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@CapacitorPlugin
public class SystemBars extends Plugin {
Expand All @@ -27,6 +28,9 @@ public class SystemBars extends Plugin {
static final String BAR_STATUS_BAR = "StatusBar";
static final String BAR_GESTURE_BAR = "NavigationBar";

static final String INSETS_HANDLING_CSS = "css";
static final String INSETS_HANDLING_DISABLE = "disable";

static final String viewportMetaJSFunction = """
function capacitorSystemBarsCheckMetaViewport() {
const meta = document.querySelectorAll("meta[name=viewport]");
Expand All @@ -37,42 +41,52 @@ function capacitorSystemBarsCheckMetaViewport() {
const metaContent = meta[meta.length - 1].content;
return metaContent.includes("viewport-fit=cover");
}

capacitorSystemBarsCheckMetaViewport();
""";

private boolean insetHandlingEnabled = true;
private boolean hasViewportCover = false;

@Override
public void load() {
getBridge().getWebView().addJavascriptInterface(this, "CapacitorSystemBarsAndroidInterface");
super.load();

initSystemBars();
}

private boolean hasFixedWebView() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this check not needed anymore? Not clear to me.

PackageInfo packageInfo = WebViewCompat.getCurrentWebViewPackage(bridge.getContext());
Pattern pattern = Pattern.compile("(\\d+)");
Matcher matcher = pattern.matcher(packageInfo.versionName);

if (!matcher.find()) {
return false;
}

String majorVersionStr = matcher.group(0);
int majorVersion = Integer.parseInt(majorVersionStr);
@Override
protected void handleOnStart() {
super.handleOnStart();

this.getBridge().addWebViewListener(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason this listener is being set in the handleOnStart instead of the load callback?

new WebViewListener() {
@Override
public void onPageCommitVisible(WebView view, String url) {
super.onPageCommitVisible(view, url);
getBridge().getWebView().requestApplyInsets();
Comment on lines +66 to +67
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this relate to the onDOMReady code? With onDOMReady already in place, this code doesn't seem to be necessary? Especially since viewportMetaJSFunction is not executed here (again)

}
}
);
}

return majorVersion >= 140;
@Override
protected void handleOnConfigurationChanged(Configuration newConfig) {
super.handleOnConfigurationChanged(newConfig);
setStyle(STYLE_DEFAULT, "");
}

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

this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
boolean hasMetaViewportCover = res.equals("true");
if (!disableCSSInsets) {
setupSafeAreaInsets(this.hasFixedWebView(), hasMetaViewportCover);
}
});
String insetsHandling = getConfig().getString("insetsHandling", "css");
if (insetsHandling.equals(INSETS_HANDLING_DISABLE)) {
insetHandlingEnabled = false;
}

initWindowInsetsListener();
initSafeAreaInsets();

getBridge().executeOnMainThread(() -> {
setStyle(style, "");
Expand Down Expand Up @@ -116,28 +130,63 @@ public void setAnimation(final PluginCall call) {
call.resolve();
}

private void setupSafeAreaInsets(boolean hasFixedWebView, boolean hasMetaViewportCover) {
ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> {
if (hasFixedWebView && hasMetaViewportCover) {
return insets;
}
@JavascriptInterface
public void onDOMReady() {
getActivity().runOnUiThread(() -> {
this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
hasViewportCover = res.equals("true");
Comment on lines +134 to +137
Copy link

@tafelnl tafelnl Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're going with the strategy where the webview is responsible for telling the plugin if it has a viewport-fit=cover value, wouldn't it make more sense to skip the evaluateJavascript part and directly let the native code know what that value is?


Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
getBridge().getWebView().requestApplyInsets();
});
});
}

int bottomInsets = safeArea.bottom;
private Insets calcSafeAreaInsets(WindowInsetsCompat insets) {
Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
return Insets.of(safeArea.left, safeArea.top, safeArea.right, safeArea.bottom);
}

if (keyboardVisible) {
// When https://issues.chromium.org/issues/457682720 is fixed and released,
// add behind a WebView version check
bottomInsets = imeInsets.bottom - bottomInsets;
private void initSafeAreaInsets() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && insetHandlingEnabled) {
View v = (View) this.getBridge().getWebView().getParent();
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(v);
if (insets != null) {
Insets safeAreaInsets = calcSafeAreaInsets(insets);
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
}
}
}

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

return WindowInsetsCompat.CONSUMED;
});
if (keyboardVisible) {
Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
setViewMargins(v, Insets.of(0, 0, 0, imeInsets.bottom));
} else {
setViewMargins(v, Insets.NONE);
}

injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
return WindowInsetsCompat.CONSUMED;
}

return insets;
});
}
}

private void setViewMargins(View v, Insets insets) {
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
mlp.leftMargin = insets.left;
mlp.bottomMargin = insets.bottom;
mlp.rightMargin = insets.right;
mlp.topMargin = insets.top;
v.setLayoutParams(mlp);
}

private void injectSafeAreaCSS(int top, int right, int bottom, int left) {
Expand Down
12 changes: 9 additions & 3 deletions cli/src/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,11 +707,17 @@ export interface PluginsConfig {
*/
SystemBars?: {
/**
* Disables the injection of device css insets into the web view.
* Specifies how to handle problematic insets on Android.
*
* @default false
* This option is only supported on Android.
*
* `css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
*
* `disable` = Disable all inset handling.
*
* @default "css"
Copy link

@tafelnl tafelnl Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the default being css, this will break everyones app when migrating from v7 to v8. Also when encapsulating an existing webapp into Capacitor, it will be broken initially. Only after a developer moves from using envs to vars the app will appear visually correct again. That should at least be documented somewhere. I think a more sensible default would be disable (and add some padding in that case) or a variant where you fallback to using envs (although you've stated you are opposing that approach)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even the Ionic framework itself uses envs. So Ionic users would also be impacted by this

*/
disableInsets?: boolean;
insetsHandling?: 'css' | 'disable';
/**
* The style of the text and icons of the system bars.
*
Expand Down
7 changes: 7 additions & 0 deletions core/native-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,13 @@ const initBridge = (w: any): void => {

const platform = getPlatformId(win);

if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
// add DOM ready listener for System Bars
document.addEventListener('DOMContentLoaded', function () {
win.CapacitorSystemBarsAndroidInterface.onDOMReady();
});
}

if (platform == 'android' || platform == 'ios') {
// patch document.cookie on Android/iOS
win.CapacitorCookiesDescriptor =
Expand Down
1 change: 1 addition & 0 deletions core/src/definitions-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export interface CapacitorCustomPlatformInstance {

export interface WindowCapacitor {
Capacitor?: CapacitorInstance;
CapacitorSystemBarsAndroidInterface?: any;
CapacitorCookiesAndroidInterface?: any;
CapacitorCookiesDescriptor?: PropertyDescriptor;
CapacitorHttpAndroidInterface?: any;
Expand Down
9 changes: 4 additions & 5 deletions core/system-bars.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ html {
padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
}
```

To disable the inset variable injections, set the configuration setting `disableInsets` to `true`.
To control this behavior, use the `insetsHandling` configuration setting.

## Example

Expand Down Expand Up @@ -72,7 +71,7 @@ const setStatusBarAnimation = async () => {
## Configuration
| Prop | Type | Description | Default |
| ------------- | -------------------- | ------------------------------------------------------------------------- | ------------------ |
| **`disableInsets`** | <code>boolean</code> | Disables the injection of device css insets into the webview. This option is only supported on Android. | <code>false</code> |
| **`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> |
| **`style`** | <code>string</code> | The style of the text and icons of the system bars. | <code>DEFAULT</code> |
| **`hidden`** | <code>boolean</code> | Hide the system bars on start. | <code>false</code> |
| **`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> |
Expand All @@ -86,7 +85,7 @@ In `capacitor.config.json`:
{
"plugins": {
"SystemBars": {
"disableInsets": true,
"insetsHandling": "css",
"style": "DARK",
"hidden": false,
"animation": "NONE"
Expand All @@ -103,7 +102,7 @@ import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
plugins: {
SystemBars: {
disableInsets: true,
insetsHandling: "css",
style: "DARK",
hidden: false,
animation: "NONE"
Expand Down
6 changes: 6 additions & 0 deletions ios/Capacitor/Capacitor/assets/native-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@ var nativeBridge = (function (exports) {
}
};
const platform = getPlatformId(win);
if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
// add DOM ready listener for System Bars
document.addEventListener('DOMContentLoaded', function () {
win.CapacitorSystemBarsAndroidInterface.onDOMReady();
});
}
if (platform == 'android' || platform == 'ios') {
// patch document.cookie on Android/iOS
win.CapacitorCookiesDescriptor =
Expand Down