Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
2b49827
Setting on core definitions and docs for SystemBars plugin
theproducer Sep 15, 2025
b78f69f
Renaming, adding basic docs
theproducer Sep 17, 2025
edb5288
Adding Android SystemBars basic functionality
theproducer Sep 17, 2025
3bb5784
Adding Android safe area injection
theproducer Sep 17, 2025
6ddb7b9
fmt
theproducer Sep 17, 2025
9e6b41a
Adding insets support to setting style and hiding
theproducer Sep 22, 2025
2c93ccd
Starting on overlay webview support
theproducer Sep 24, 2025
c4be18d
Cleanup, adding support for configuration
theproducer Sep 26, 2025
11c5590
Fixes to type declarations
theproducer Sep 26, 2025
07d6b3f
Updating meaning of LIGHT vs DARK
theproducer Sep 30, 2025
0698ca7
Updating style to match documentation on iOS
theproducer Sep 30, 2025
8bcd46d
Rename `enable` to `enableInsets`
theproducer Oct 3, 2025
15fab3a
Merge branch 'main' into RDMR-902
theproducer Oct 3, 2025
bf2fe33
fmt
theproducer Oct 3, 2025
bbfb49b
Removing overlay support
theproducer Oct 22, 2025
c6664d1
Adding webview version check, optimizing insets injection
theproducer Oct 22, 2025
55717fd
fmt
theproducer Oct 22, 2025
0f7d843
Merge branch 'main' into RDMR-902
theproducer Oct 22, 2025
ba72a20
Adding config option for hiding system bars
theproducer Oct 22, 2025
e6bd10b
Adding support for configurations in iOS system bars
theproducer Oct 22, 2025
929aa33
Adding support for setting iOS status bar animations
theproducer Oct 22, 2025
114367d
Making showing / hiding status bars more friendly
theproducer Oct 22, 2025
e8df7ae
naming and consistency
theproducer Oct 22, 2025
7eb6aed
documentation
theproducer Oct 23, 2025
6fb8d70
docs, fmt
theproducer Oct 23, 2025
a104cea
fmt
theproducer Oct 23, 2025
7ca256c
Removing enableInsets setting in exchange for meta viewport checking
theproducer Oct 27, 2025
21f473b
Removing Cap 7 edgeToEdgeHandler
theproducer Oct 28, 2025
be27917
Merge branch 'main' into RDMR-902
theproducer Nov 3, 2025
cd2fb6c
Adding ability to control iOS home indicator visibility
theproducer Nov 3, 2025
cf0b1f9
fmt
theproducer Nov 3, 2025
ec05a18
Readding `adjustMarginsForEdgeToEdge`, defaulted to “auto”
theproducer Nov 4, 2025
984a254
webview and viewport check fix
theproducer Nov 7, 2025
0978ba3
Adding more checks for edge to edge mode inside `setupSafeAreaInsets`
theproducer Nov 10, 2025
d5504fa
Using bridge webview parent view to apply insets
theproducer Nov 11, 2025
39872aa
docs
theproducer Nov 11, 2025
b90b657
Removing unneeded dispatchEvent
theproducer Nov 11, 2025
0a51081
ios tweaks
theproducer Nov 11, 2025
188e5ba
ios tweaks
theproducer Nov 11, 2025
6a43397
fmt
theproducer Nov 11, 2025
8be67e9
Merge branch 'main' into RDMR-902
markemer Nov 11, 2025
dd019b9
Merge branch 'main' into RDMR-902
theproducer Nov 11, 2025
0334463
Merge branch 'RDMR-902' of github.com:ionic-team/capacitor into RDMR-902
theproducer Nov 11, 2025
ca6607c
lint
theproducer Nov 11, 2025
b69f466
Adding document table explaining difference between SystemBars and St…
theproducer Nov 11, 2025
5dda6ae
Adding `disableInsets` options
theproducer Nov 11, 2025
4136185
Fixing lint errors
theproducer Nov 11, 2025
44d4ab5
More docs improvements
theproducer Nov 12, 2025
06ec8b3
Renaming insets to SystemBarType
theproducer Nov 12, 2025
79e319e
Updating docs
theproducer Nov 12, 2025
28018be
Merge branch 'main' into RDMR-902
theproducer Nov 12, 2025
dc1ef57
more doc tweaks
theproducer Nov 12, 2025
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
Expand Up @@ -655,6 +655,7 @@ private void registerAllPlugins() {
this.registerPlugin(com.getcapacitor.plugin.CapacitorCookies.class);
this.registerPlugin(com.getcapacitor.plugin.WebView.class);
this.registerPlugin(com.getcapacitor.plugin.CapacitorHttp.class);
this.registerPlugin(com.getcapacitor.plugin.SystemBars.class);

for (Class<? extends Plugin> pluginClass : this.initialPlugins) {
this.registerPlugin(pluginClass);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
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 INSET_TOP = "TOP";
static final String INSET_BOTTOM = "BOTTOM";

@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() {
boolean enableInsets = getConfig().getBoolean("enableInsets", true);
String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase();
boolean hidden = getConfig().getBoolean("hidden", false);

if (enableInsets) {
setupSafeAreaInsets();
}

getBridge()
.executeOnMainThread(() -> {
setStyle(style, "");
setHidden(hidden, "");
});
}

@PluginMethod
Copy link

Choose a reason for hiding this comment

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

The swift equivalents of the plugin methods here are annotated with a returnType: none. Would it make sense to change it to @PluginMethod(returnType = PluginMethod.RETURN_NONE)?

public void setStyle(final PluginCall call) {
String inset = call.getString("inset", "").toUpperCase(Locale.US);
String style = call.getString("style", STYLE_DEFAULT);

getBridge()
.executeOnMainThread(() -> {
setStyle(style, inset);
call.resolve();
});
}

@PluginMethod
public void show(final PluginCall call) {
String inset = call.getString("inset", "").toUpperCase(Locale.US);

getBridge()
.executeOnMainThread(() -> {
setHidden(false, inset);
call.resolve();
});
}

@PluginMethod
public void hide(final PluginCall call) {
String inset = call.getString("inset", "").toUpperCase(Locale.US);

getBridge()
.executeOnMainThread(() -> {
setHidden(true, inset);
call.resolve();
});
}

@PluginMethod
public void setAnimation(final PluginCall call) {
call.resolve();
}

private void setupSafeAreaInsets() {
View decorView = getActivity().getWindow().getDecorView();

ViewCompat.setOnApplyWindowInsetsListener(decorView, (v, insets) -> {
if (!this.hasFixedWebView()) {
Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
injectSafeAreaCSS(safeArea.top, safeArea.right, safeArea.bottom, safeArea.left);

return WindowInsetsCompat.CONSUMED;
}

return insets;
});
}

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");
window.dispatchEvent(new CustomEvent('safeAreaChanged'));
} 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 inset) {
if (style.equals(STYLE_DEFAULT)) {
style = getStyleForTheme();
}

Window window = getActivity().getWindow();
WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat.getInsetsController(window, window.getDecorView());
if (inset.isEmpty() || inset.equals(INSET_TOP)) {
windowInsetsControllerCompat.setAppearanceLightStatusBars(!style.equals(STYLE_DARK));
}

if (inset.isEmpty() || inset.equals(INSET_BOTTOM)) {
windowInsetsControllerCompat.setAppearanceLightNavigationBars(!style.equals(STYLE_DARK));
}
}

private void setHidden(boolean hide, String inset) {
Window window = getActivity().getWindow();
WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat.getInsetsController(window, window.getDecorView());

if (hide) {
if (inset.isEmpty() || inset.equals(INSET_TOP)) {
windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.statusBars());
}
if (inset.isEmpty() || inset.equals(INSET_BOTTOM)) {
windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.navigationBars());
}
return;
}

if (inset.isEmpty() || inset.equals(INSET_TOP)) {
windowInsetsControllerCompat.show(WindowInsetsCompat.Type.systemBars());
}
if (inset.isEmpty() || inset.equals(INSET_BOTTOM)) {
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;
}
}
36 changes: 36 additions & 0 deletions cli/src/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,4 +710,40 @@ export interface PluginsConfig {
*/
enabled?: boolean;
};

/**
* System Bars plugin configuration
*
* @since 8.0.0
*/
SystemBars?: {
/**
* Enables the injection of device css insets into the webview.
*
* This option is only supported on Android.
*
* @default true
*/
enableInsets?: boolean;

/**
* The style of the text and icons of the system bars.
*/
style?: string;

/**
* Hide the system bars on start.
*/
hidden?: boolean;

/**
* The type of status bar animation used when showing or hiding.
*
* This option is only supported on iOS.
*
* @default 'FADE'
*
*/
animation?: 'FADE' | 'NONE';
};
}
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"build": "npm run clean && npm run docgen && npm run transpile && npm run rollup",
"build:nativebridge": "tsc native-bridge.ts --target es2017 --moduleResolution node --outDir build && rollup --config rollup.bridge.config.js",
"clean": "rimraf dist",
"docgen": "docgen --api CapacitorCookiesPlugin --output-readme cookies.md && docgen --api CapacitorHttpPlugin --output-readme http.md",
"docgen": "docgen --api CapacitorCookiesPlugin --output-readme cookies.md && docgen --api CapacitorHttpPlugin --output-readme http.md && docgen --api SystemBarsPlugin --output-readme systembars.md",
"prepublishOnly": "npm run build",
"rollup": "rollup --config rollup.config.js",
"transpile": "tsc",
Expand Down
Loading
Loading