-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: System Bars Plugin #8180
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
feat: System Bars Plugin #8180
Changes from all commits
2b49827
b78f69f
edb5288
3bb5784
6ddb7b9
9e6b41a
2c93ccd
c4be18d
11c5590
07d6b3f
0698ca7
8bcd46d
15fab3a
bf2fe33
bbfb49b
c6664d1
55717fd
0f7d843
ba72a20
e6bd10b
929aa33
114367d
e8df7ae
7eb6aed
6fb8d70
a104cea
7ca256c
21f473b
be27917
cd2fb6c
cf0b1f9
ec05a18
984a254
0978ba3
d5504fa
39872aa
b90b657
0a51081
188e5ba
6a43397
8be67e9
dd019b9
0334463
ca6607c
b69f466
5dda6ae
4136185
44d4ab5
06ec8b3
79e319e
28018be
dc1ef57
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 |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| package com.getcapacitor.plugin; | ||
|
|
||
| import android.content.pm.PackageInfo; | ||
| import android.content.res.Configuration; | ||
| import android.view.View; | ||
| 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 { | ||
|
|
||
| static final String STYLE_LIGHT = "LIGHT"; | ||
| static final String STYLE_DARK = "DARK"; | ||
| static final String STYLE_DEFAULT = "DEFAULT"; | ||
| static final String BAR_STATUS_BAR = "StatusBar"; | ||
| static final String BAR_GESTURE_BAR = "NavigationBar"; | ||
|
|
||
| static final String viewportMetaJSFunction = """ | ||
| function capacitorSystemBarsCheckMetaViewport() { | ||
| const meta = document.querySelectorAll("meta[name=viewport]"); | ||
| if (meta.length == 0) { | ||
| return false; | ||
| } | ||
| // get the last found meta viewport tag | ||
| const metaContent = meta[meta.length - 1].content; | ||
| return metaContent.includes("viewport-fit=cover"); | ||
| } | ||
| capacitorSystemBarsCheckMetaViewport(); | ||
| """; | ||
|
|
||
| @Override | ||
| public void load() { | ||
| 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); | ||
|
|
||
| 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); | ||
|
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 a naming like
Right now this PR makes it quite hard to override this behavior if a developer is - for whatever reason - looking to change the core behavior (for example by installing a plugin that has a different philosophy about this). On another note: the |
||
|
|
||
| this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> { | ||
| boolean hasMetaViewportCover = res.equals("true"); | ||
| if (!disableCSSInsets) { | ||
| setupSafeAreaInsets(this.hasFixedWebView(), hasMetaViewportCover); | ||
| } | ||
| }); | ||
|
|
||
| getBridge().executeOnMainThread(() -> { | ||
| setStyle(style, ""); | ||
| setHidden(hidden, ""); | ||
| }); | ||
| } | ||
|
|
||
| @PluginMethod | ||
|
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. The swift equivalents of the plugin methods here are annotated with a |
||
| public void setStyle(final PluginCall call) { | ||
| String bar = call.getString("bar", ""); | ||
| String style = call.getString("style", STYLE_DEFAULT); | ||
|
|
||
| getBridge().executeOnMainThread(() -> { | ||
| setStyle(style, bar); | ||
| call.resolve(); | ||
| }); | ||
| } | ||
|
|
||
| @PluginMethod | ||
| public void show(final PluginCall call) { | ||
| String bar = call.getString("bar", ""); | ||
|
|
||
| getBridge().executeOnMainThread(() -> { | ||
| setHidden(false, bar); | ||
| call.resolve(); | ||
| }); | ||
| } | ||
|
|
||
| @PluginMethod | ||
| public void hide(final PluginCall call) { | ||
| String bar = call.getString("bar", ""); | ||
|
|
||
| getBridge().executeOnMainThread(() -> { | ||
| setHidden(true, bar); | ||
| call.resolve(); | ||
| }); | ||
| } | ||
|
|
||
| @PluginMethod | ||
| 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; | ||
| } | ||
|
|
||
| Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); | ||
| injectSafeAreaCSS(safeArea.top, safeArea.right, safeArea.bottom, safeArea.left); | ||
|
Comment on lines
+125
to
+126
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. I think in this case we should fallback to logic similar as to what Also injecting the safe area insets as custom vars should be a toggleable option I think. And only respected if
Contributor
Author
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.
You are referring to adding margins directly to the native view? That was meant to be a quick and dirty fix - we generally try to avoid manipulating the native views style-wise as much as possible to leave the power and final decision making in the hands of the web developer. Using CSS env vars (albeit a bit convoluted because of the Android bug:
This is something we can do, I'll work on adding a config option for disabling the inset injections altogether. 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.
Yes that's what I meant. It would align with iOS behavior. If a user does not enable edge-to-edge support through the Ideally, of course, this is something that the Android team will just natively support out of the box. But iirc a few years/months ago one of the devs mentioned that it's not something they were looking into. I cannot find the comment anymore. I'll link it if I come across it 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. Found it: https://issues.chromium.org/issues/396827865#comment12 They do mention:
So I guess if we properly file a FR, they might even be willing to support this out of the box. If we then also file a FR for the bug I would say this is definitely worth a try |
||
|
|
||
| return WindowInsetsCompat.CONSUMED; | ||
| }); | ||
| } | ||
|
|
||
| private void injectSafeAreaCSS(int top, int right, int bottom, int left) { | ||
| // 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; | ||
|
|
||
| // Execute JavaScript to inject the CSS | ||
| getBridge().executeOnMainThread(() -> { | ||
| if (bridge != null && bridge.getWebView() != null) { | ||
| String script = String.format( | ||
| Locale.US, | ||
| """ | ||
| try { | ||
| document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx"); | ||
| document.documentElement.style.setProperty("--safe-area-inset-right", "%dpx"); | ||
| document.documentElement.style.setProperty("--safe-area-inset-bottom", "%dpx"); | ||
| document.documentElement.style.setProperty("--safe-area-inset-left", "%dpx"); | ||
| } catch(e) { console.error('Error injecting safe area CSS:', e); } | ||
| """, | ||
| (int) topPx, | ||
| (int) rightPx, | ||
| (int) bottomPx, | ||
| (int) leftPx | ||
| ); | ||
|
|
||
| bridge.getWebView().evaluateJavascript(script, null); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| private void setStyle(String style, String bar) { | ||
| if (style.equals(STYLE_DEFAULT)) { | ||
| style = getStyleForTheme(); | ||
| } | ||
|
|
||
| Window window = getActivity().getWindow(); | ||
| WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat.getInsetsController(window, window.getDecorView()); | ||
| if (bar.isEmpty() || bar.equals(BAR_STATUS_BAR)) { | ||
| windowInsetsControllerCompat.setAppearanceLightStatusBars(!style.equals(STYLE_DARK)); | ||
| } | ||
|
|
||
| if (bar.isEmpty() || bar.equals(BAR_GESTURE_BAR)) { | ||
| windowInsetsControllerCompat.setAppearanceLightNavigationBars(!style.equals(STYLE_DARK)); | ||
| } | ||
| } | ||
|
|
||
| private void setHidden(boolean hide, String bar) { | ||
| Window window = getActivity().getWindow(); | ||
| WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat.getInsetsController(window, window.getDecorView()); | ||
|
|
||
| if (hide) { | ||
| if (bar.isEmpty() || bar.equals(BAR_STATUS_BAR)) { | ||
| windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.statusBars()); | ||
| } | ||
| if (bar.isEmpty() || bar.equals(BAR_GESTURE_BAR)) { | ||
| windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.navigationBars()); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (bar.isEmpty() || bar.equals(BAR_STATUS_BAR)) { | ||
| windowInsetsControllerCompat.show(WindowInsetsCompat.Type.systemBars()); | ||
| } | ||
| if (bar.isEmpty() || bar.equals(BAR_GESTURE_BAR)) { | ||
| windowInsetsControllerCompat.show(WindowInsetsCompat.Type.navigationBars()); | ||
| } | ||
| } | ||
|
|
||
| private String getStyleForTheme() { | ||
| int currentNightMode = getActivity().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; | ||
| if (currentNightMode != Configuration.UI_MODE_NIGHT_YES) { | ||
| return STYLE_LIGHT; | ||
| } | ||
| return STYLE_DARK; | ||
| } | ||
| } | ||
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.
I think
adjustMarginsForEdgeToEdgecan be removed?