Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
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 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.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 +25,11 @@ public class SystemBars extends Plugin {
static final String BAR_STATUS_BAR = "StatusBar";
static final String BAR_GESTURE_BAR = "NavigationBar";

static final String INSETS_HANDLING_BOTH = "both";
static final String INSETS_HANDLING_CSS = "css";
static final String INSETS_HANDLING_MARGINS = "margins";
static final String INSETS_HANDLING_DISABLE = "disable";

static final String viewportMetaJSFunction = """
function capacitorSystemBarsCheckMetaViewport() {
const meta = document.querySelectorAll("meta[name=viewport]");
Expand All @@ -41,38 +44,51 @@ function capacitorSystemBarsCheckMetaViewport() {
capacitorSystemBarsCheckMetaViewport();
""";

private boolean useCSSVariables = true;
private boolean useViewMargins = true;

@Override
public void load() {
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);

return majorVersion >= 140;
}

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", "both");
switch (insetsHandling) {
case INSETS_HANDLING_BOTH -> {
useViewMargins = true;
useCSSVariables = true;
}
});
case INSETS_HANDLING_CSS -> {
useViewMargins = false;
useCSSVariables = true;
}
case INSETS_HANDLING_MARGINS -> {
useViewMargins = true;
useCSSVariables = false;
}
case INSETS_HANDLING_DISABLE -> {
useViewMargins = false;
useCSSVariables = false;
}
}

getBridge()
.getWebView()
.post(() -> {
this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
boolean hasMetaViewportCover = res.equals("true");

useViewMargins = !hasMetaViewportCover && useViewMargins;

initWindowInsetsListener();
initSafeAreaInsets();
});
});

getBridge().executeOnMainThread(() -> {
setStyle(style, "");
Expand Down Expand Up @@ -116,30 +132,62 @@ 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;
}
private Insets calcSafeAreaInsets(WindowInsetsCompat insets) {
Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());

int bottomInsets = 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;
}

return Insets.of(safeArea.left, safeArea.top, safeArea.right, bottomInsets);
}

private void initSafeAreaInsets() {
View v = (View) this.getBridge().getWebView().getParent();
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(v);
Insets safeAreaInsets = calcSafeAreaInsets(insets);

Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && useCSSVariables) {
Copy link
Contributor

@OS-pedrogustavobilro OS-pedrogustavobilro Dec 4, 2025

Choose a reason for hiding this comment

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

Is the Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM check in place to only apply this for apps that are edge-to-edge? But there could be Capacitor apps with the edge-to-edge optout (as it was suggested for status-bar plugin around Cap 7 time), in which case it would enter this if condition, when perhaps it shouldn't?

Also what about if a developer enables edge-to-edge on the activity explicitly, in which case they'd have edge-to-edge on versions lower than 15?

Or maybe I'm missing some context here on why this version check is in place.

Copy link

Choose a reason for hiding this comment

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

I agree. This check doesn't make sense at all. This actually breaks the inset behavior for users on older Android versions, because they won't have access to the injected vars

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

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && useViewMargins) {
setSafeAreaMargins(v, safeAreaInsets);
}
}

int bottomInsets = safeArea.bottom;
private void initWindowInsetsListener() {
ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> {
Insets safeAreaInsets = calcSafeAreaInsets(insets);

if (keyboardVisible) {
// When https://issues.chromium.org/issues/457682720 is fixed and released,
// add behind a WebView version check
bottomInsets = imeInsets.bottom - bottomInsets;
if (useCSSVariables) {
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
}

injectSafeAreaCSS(safeArea.top, safeArea.right, bottomInsets, safeArea.left);
if (useViewMargins) {
setSafeAreaMargins(v, safeAreaInsets);
return WindowInsetsCompat.CONSUMED;
}

return WindowInsetsCompat.CONSUMED;
return insets;
});
}

private void setSafeAreaMargins(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) {
// Convert pixels to density-independent pixels
float density = getActivity().getResources().getDisplayMetrics().density;
Expand Down
16 changes: 13 additions & 3 deletions cli/src/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,11 +707,21 @@ 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.
*
* `both` = Injects CSS variables (`--safe-area-inset-*`) into the document in addition to shrinking the webview into the device safe areas. The margin shrinking is ignored if the document has a meta viewport tag with `viewport-fit=cover`.
*
* `css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
*
* `margins` = Shrinks the webview into the device safe area using view margins. This is ignored if the document has a meta viewport tag with `viewport-fit=cover`.
*
* `disable` = Disable all inset handling.
*
* @default "both"
*/
disableInsets?: boolean;
insetsHandling?: 'both' | 'css' | 'margins' | 'disable';
/**
* The style of the text and icons of the system bars.
*
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`.
In Android 15+'s default edge-to-edge environment, if your web application does not opt-in to safe area handling via the meta viewport tag (`viewport-fit=cover`), this plugin will automatically apply native padding to the WebView. This ensures your application fits within the safe areas without being obscured by system bars. 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>`both` = Injects CSS variables (`--safe-area-inset-*`) into the document in addition to shrinking the webview into the device safe areas. The margin shrinking is ignored if the document has a meta viewport tag with viewport-fit=cover.<br>`css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.<br>`margins` = Shrinks the webview into the device safe area using view margins. This is ignored if the document has a meta viewport tag with viewport-fit=cover.<br>`disable` = Disable all inset handling. | <code>both</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": "margins",
"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: "margins",
style: "DARK",
hidden: false,
animation: "NONE"
Expand Down