-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(android): Improving SystemBars inset handling #8268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
eae736f
00e7c27
ba7a7ac
8a70e7a
d7995dd
3a5db7a
d33ea65
4aa5930
a3b4ea6
a908d6c
4f152da
440bc21
c02cf60
c61289d
e604cbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
|
|
@@ -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]"); | ||
|
|
@@ -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() { | ||
| 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( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a particular reason this listener is being set in the |
||
| new WebViewListener() { | ||
| @Override | ||
| public void onPageCommitVisible(WebView view, String url) { | ||
| super.onPageCommitVisible(view, url); | ||
| getBridge().getWebView().requestApplyInsets(); | ||
|
Comment on lines
+66
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this relate to the |
||
| } | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| 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, ""); | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| 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) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the default being There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
| * | ||
|
|
||
There was a problem hiding this comment.
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.