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: 0 additions & 6 deletions android/capacitor/src/main/assets/native-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,6 @@ 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 @@ -4,11 +4,9 @@
import android.content.pm.PackageInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.util.TypedValue;
import android.view.View;
import android.view.Window;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
Expand All @@ -33,9 +31,9 @@ public class SystemBars extends Plugin {
static final String BAR_STATUS_BAR = "StatusBar";
static final String BAR_GESTURE_BAR = "NavigationBar";

// TODO: In Cap 9, add an additional option "full"
static final String INSETS_HANDLING_CSS = "css";
static final String INSETS_HANDLING_DISABLE = "disable";
static final String INSETS_HANDLING_NATIVE = "native";

// https://issues.chromium.org/issues/40699457
private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 140;
Expand All @@ -61,9 +59,22 @@ function capacitorSystemBarsCheckMetaViewport() {
private String currentStatusBarStyle = STYLE_DEFAULT;
private String currentGestureBarStyle = STYLE_DEFAULT;

// Declare variable at this scope to help prevent adding multiple listeners.
private WebViewListener webViewListener;

private void warnAboutUnsupportedConfigurationValues() {
boolean keyboardResizeOnFullScreen = bridge.getConfig().getPluginConfiguration("Keyboard").getBoolean("resizeOnFullScreen", false);

if (!INSETS_HANDLING_DISABLE.equals(insetsHandling) && keyboardResizeOnFullScreen) {
Logger.warn(
"SystemBars",
"You should omit `Keyboard.resizeOnFullScreen` in your `capacitor.config.json`. Other values can lead to unexpected behavior."
);
}
}

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

initSystemBars();
Expand All @@ -73,15 +84,25 @@ public void load() {
protected void handleOnStart() {
super.handleOnStart();

this.getBridge().addWebViewListener(
new WebViewListener() {
if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) {
return;
}

if (webViewListener == null) {
webViewListener = new WebViewListener() {
@Override
public void onPageCommitVisible(WebView view, String url) {
super.onPageCommitVisible(view, url);
getBridge().getWebView().requestApplyInsets();
bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
hasViewportCover = res.equals("true");

// Request new execution tree of `setOnApplyWindowInsetsListener`
bridge.getWebView().requestApplyInsets();
});
}
}
);
};
this.getBridge().addWebViewListener(webViewListener);
}
}

@Override
Expand All @@ -93,11 +114,20 @@ protected void handleOnConfigurationChanged(Configuration newConfig) {
}

private void initSystemBars() {
// If you already know what the value of the `viewport-fit=` meta tag is going to be,
// passing it here through `initialViewportFitValueHint` can help prevent layout shifting.
String configuredInitialViewportFitValueHint = getConfig().getString("initialViewportFitValueHint", "");
hasViewportCover = "cover".equals(configuredInitialViewportFitValueHint);

String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase(Locale.US);
boolean hidden = getConfig().getBoolean("hidden", false);

String configuredInsetsHandling = getConfig().getString("insetsHandling", INSETS_HANDLING_CSS);
if (INSETS_HANDLING_CSS.equals(configuredInsetsHandling) || INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling)) {
if (
INSETS_HANDLING_CSS.equals(configuredInsetsHandling) ||
INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling) ||
INSETS_HANDLING_NATIVE.equals(configuredInsetsHandling)
) {
insetsHandling = configuredInsetsHandling;
} else {
Logger.warn(
Expand All @@ -107,8 +137,9 @@ private void initSystemBars() {
insetsHandling = INSETS_HANDLING_CSS;
}

warnAboutUnsupportedConfigurationValues();

initWindowInsetsListener();
initSafeAreaCSSVariables();

getBridge().executeOnMainThread(() -> {
setStyle(style, "");
Expand Down Expand Up @@ -152,51 +183,14 @@ public void setAnimation(final PluginCall call) {
call.resolve();
}

@JavascriptInterface
public void onDOMReady() {
if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
getActivity().runOnUiThread(() -> {
this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
hasViewportCover = res.equals("true");

getBridge().getWebView().requestApplyInsets();
});
});
}
}

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

private void initSafeAreaCSSVariables() {
if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
WindowInsetsCompat insets;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
View v = (View) this.getBridge().getWebView().getParent();
insets = ViewCompat.getRootWindowInsets(v);
} else {
insets = WindowInsetsCompat.CONSUMED;
}

if (insets != null) {
Insets safeAreaInsets = calcSafeAreaInsets(insets);
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
}
}
}

private void initWindowInsetsListener() {
if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) {
return;
}

ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> {
View view = getActivity().getWindow().getDecorView();

ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
boolean shouldPassthroughInsets = getWebViewMajorVersion() >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover;

Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
Expand All @@ -207,10 +201,7 @@ private void initWindowInsetsListener() {
// We need to correct for a possible shown IME
v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0);

Insets safeAreaInsets = calcSafeAreaInsets(insets);
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);

return new WindowInsetsCompat.Builder(insets)
WindowInsetsCompat newInsets = new WindowInsetsCompat.Builder(insets)
.setInsets(
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(),
Insets.of(
Expand All @@ -221,39 +212,49 @@ private void initWindowInsetsListener() {
)
)
.build();
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
// We need to correct for a possible shown IME
v.setPadding(
systemBarsInsets.left,
systemBarsInsets.top,
systemBarsInsets.right,
keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom
);
injectSafeAreaCSS(newInsets);

return newInsets;
}

// We need to correct for a possible shown IME
v.setPadding(
systemBarsInsets.left,
systemBarsInsets.top,
systemBarsInsets.right,
keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom
);

// Returning `WindowInsetsCompat.CONSUMED` breaks recalculation of safe area insets
// So we have to explicitly set insets to `0`
// See: https://issues.chromium.org/issues/461332423
WindowInsetsCompat newInsets = new WindowInsetsCompat.Builder(insets)
.setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of(0, 0, 0, 0))
.build();

Insets safeAreaInsets = calcSafeAreaInsets(newInsets);
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
injectSafeAreaCSS(newInsets);

return newInsets;
});
}

private void injectSafeAreaCSS(int top, int right, int bottom, int left) {
private void injectSafeAreaCSS(WindowInsetsCompat insets) {
if (!INSETS_HANDLING_CSS.equals(insetsHandling)) {
return;
}

Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());

// Convert pixels to density-independent pixels
float density = getActivity().getResources().getDisplayMetrics().density;
float topPx = top / density;
float rightPx = right / density;
float bottomPx = bottom / density;
float leftPx = left / density;
float topPx = systemBarsInsets.top / density;
float rightPx = systemBarsInsets.right / density;
// For native insets the value gets automatically corrected when the IME is visible (in newer WebView versions),
// but for these injected values we have to handle that manually (for all WebView versions).
float bottomPx = (keyboardVisible ? 0 : systemBarsInsets.bottom) / density;
float leftPx = systemBarsInsets.left / density;

// Execute JavaScript to inject the CSS
getBridge().executeOnMainThread(() -> {
Expand Down
26 changes: 23 additions & 3 deletions cli/src/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,13 +772,33 @@ export interface PluginsConfig {
*
* This option is only supported on Android.
*
* `css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
* `native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= v140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example so make sure the webview is shown correctly.
*
* `disable` = Disable CSS variables injection.
* `css` = This is the same as `native`, but it also injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
*
* `disable` = (not recommended) Disable safe area insets handling completely.
* This shifts the responsibility from Capacitor to your own code to handle the insets.
* Be aware that this might result in a visually broken UI if your native app code and the content loaded into the webview do not correctly handle safe area insets.
*
* @default "css"
*/
insetsHandling?: 'css' | 'disable';
insetsHandling?: 'native' | 'css' | 'disable';

/**
* Set an initial value for the to be detected `viewport-fit=` meta tag value.
* For most apps that support edge-to-edge this value will eventually be `cover`.
* Therefore you might want to set this value to `cover` to help prevent layout jumps and glitches.
* If you know the value to be `cover` initially, you can set it here.
* The value will always end up correctly, no matter what you set here,
* as long as `insetsHandling` is set to `native` or `css`.
* It only exists to help prevent layout jumps and glitches.
*
* This option is only supported on Android.
*
* @default undefined
*/
initialViewportFitValueHint?: 'auto' | 'contain' | 'cover';

/**
* The style of the text and icons of the system bars.
*
Expand Down
7 changes: 0 additions & 7 deletions core/native-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,13 +377,6 @@ 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
17 changes: 5 additions & 12 deletions core/system-bars.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,9 @@ The status bar visibility defaults to visible and the style defaults to

## Android Note

Due to a [bug](https://issues.chromium.org/issues/40699457) in some older versions of Android WebView (< 140), correct safe area values are not available via the `safe-area-inset-x` CSS `env` variables. This plugin will inject the correct inset values into a new CSS variable(s) named `--safe-area-inset-x` that you can use as a fallback in your frontend styles:

```css
html {
padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
}
```
To control this behavior, use the `insetsHandling` configuration setting.
Due to a [bug](https://issues.chromium.org/issues/40699457) in some older versions of Android WebView (< 140), correct safe area values are not available via the `safe-area-inset-x` CSS `env` variables. This plugin has two ways to workaround this. To control this behavior, use the `insetsHandling` configuration setting.

You should also consider calling `EdgeToEdge.enable(this);` inside your application's `BridgeActivity.onCreate` if `insetsHandling` is not set to `disable`. Starting in Capacitor 9, `insetsHandling` will default to `native` and this will be called for you.

## Example

Expand Down Expand Up @@ -73,7 +65,8 @@ const setStatusBarAnimation = async () => {
## Configuration
| Prop | Type | Description | Default |
| ------------- | -------------------- | ------------------------------------------------------------------------- | ------------------ |
| **`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 CSS variables injection. | <code>css</code> |
| **`insetsHandling`** | <code>string</code> | Specifies how to handle problematic insets on Android.<br>This option is only supported on Android.<br><br>`native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= v140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example so make sure the webview is shown correctly.<br><br>`css` = This is the same as `native`, but it also injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.<br><br>`disable` = (not recommended) Disable safe area insets handling completely.<br>This shifts the responsibility from Capacitor to your own code to handle the insets.<br>Be aware that this might result in a visually broken UI if your native app code and the content loaded into the webview do not correctly handle safe area insets.<br>| <code>css</code> |
| **`initialViewportFitValueHint`** | <code>string</code> | Set an initial value for the to be detected `viewport-fit=` meta tag value.<br>For most apps that support edge-to-edge this value will eventually be `cover`.<br>Therefore you might want to set this value to `cover` to help prevent layout jumps and glitches.<br>If you know the value to be `cover` initially, you can set it here.<br>The value will always end up correctly, no matter what you set here,<br>as long as `insetsHandling` is set to `native` or `css`.<br>It only exists to help prevent layout jumps and glitches.<br><br>This option is only supported on Android. | <code>undefined</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 Down
6 changes: 0 additions & 6 deletions ios/Capacitor/Capacitor/assets/native-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,6 @@ 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
Loading