capacitor-plugin-status-bar 2.0.13 → 2.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -21
- package/android/src/main/java/com/cap/plugins/statusbar/CapacitorStatusBar.java +308 -457
- package/android/src/main/java/com/cap/plugins/statusbar/CapacitorStatusBarPlugin.java +1 -21
- package/dist/docs.json +0 -40
- package/dist/esm/definitions.d.ts +0 -9
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +1 -2
- package/dist/esm/web.js +0 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +0 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +0 -3
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorStatusBarPlugin/CapacitorStatusBar.swift +173 -78
- package/ios/Sources/CapacitorStatusBarPlugin/CapacitorStatusBarPlugin.swift +18 -21
- package/package.json +1 -1
|
@@ -4,13 +4,14 @@ import android.app.Activity;
|
|
|
4
4
|
import android.graphics.Color;
|
|
5
5
|
import android.os.Build;
|
|
6
6
|
import android.util.Log;
|
|
7
|
+
import android.view.Gravity;
|
|
7
8
|
import android.view.View;
|
|
8
9
|
import android.view.ViewGroup;
|
|
9
10
|
import android.view.Window;
|
|
10
11
|
import android.view.WindowInsets;
|
|
11
12
|
import android.view.WindowInsetsController;
|
|
13
|
+
import android.view.WindowManager;
|
|
12
14
|
import android.widget.FrameLayout;
|
|
13
|
-
import android.view.Gravity;
|
|
14
15
|
|
|
15
16
|
import androidx.annotation.ColorInt;
|
|
16
17
|
import androidx.annotation.Nullable;
|
|
@@ -18,49 +19,54 @@ import androidx.core.graphics.ColorUtils;
|
|
|
18
19
|
import androidx.core.view.ViewCompat;
|
|
19
20
|
import androidx.core.view.WindowCompat;
|
|
20
21
|
import androidx.core.view.WindowInsetsCompat;
|
|
22
|
+
import androidx.core.view.WindowInsetsControllerCompat;
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Android Status Bar utilities with Android 10-15+ (API 29-35+) support.
|
|
26
|
-
* Supports:
|
|
27
|
-
* - API 29 (Android 10): Uses deprecated SYSTEM_UI_FLAG for backward
|
|
28
|
-
* compatibility
|
|
29
|
-
* - API 30+ (Android 11+): Uses modern WindowInsetsController API
|
|
30
|
-
* - API 35+ (Android 15+): Fully compatible with edge-to-edge display
|
|
31
|
-
* enforcement
|
|
32
|
-
*/
|
|
33
|
-
public class CapacitorStatusBar extends Plugin {
|
|
24
|
+
public class CapacitorStatusBar {
|
|
34
25
|
private static final String TAG = "CapacitorStatusBar";
|
|
35
26
|
private static final String STATUS_BAR_OVERLAY_TAG = "capacitor_status_bar_overlay";
|
|
36
27
|
private static final String NAV_BAR_OVERLAY_TAG = "capacitor_navigation_bar_overlay";
|
|
37
28
|
|
|
38
|
-
// Store current state to preserve colors when hiding/showing
|
|
39
29
|
private String currentStyle = "LIGHT";
|
|
40
30
|
private String currentColorHex = null;
|
|
41
31
|
private int currentStatusBarColor = Color.BLACK;
|
|
42
32
|
private int currentNavBarColor = Color.BLACK;
|
|
33
|
+
private boolean statusBarHidden = false;
|
|
34
|
+
private boolean navBarHidden = false;
|
|
35
|
+
|
|
36
|
+
private boolean isAndroid15OrAbove() {
|
|
37
|
+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private boolean supportsInsetsController() {
|
|
41
|
+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
|
|
42
|
+
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
private boolean supportsContrastControl() {
|
|
45
|
+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private void applyLegacyContrastPolicy(Window window) {
|
|
49
|
+
if (!isAndroid15OrAbove() && supportsContrastControl()) {
|
|
50
|
+
window.setStatusBarContrastEnforced(false);
|
|
51
|
+
window.setNavigationBarContrastEnforced(false);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* overlay view sizing and WebView padding so content stays within
|
|
54
|
-
* the safe area.
|
|
55
|
-
*
|
|
56
|
-
* @param activity The activity to configure
|
|
57
|
-
* @param webView The Capacitor WebView to apply safe-area padding to (may be null)
|
|
56
|
+
* Configure layout behavior for the current API level. Called once from
|
|
57
|
+
* CapacitorStatusBarPlugin.load().
|
|
58
58
|
*/
|
|
59
|
-
public void ensureEdgeToEdgeConfigured(Activity activity
|
|
59
|
+
public void ensureEdgeToEdgeConfigured(Activity activity) {
|
|
60
60
|
Window window = activity.getWindow();
|
|
61
61
|
View decorView = window.getDecorView();
|
|
62
|
+
configureWebview(window, decorView);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private void configureWebview(Window window, View decorView) {
|
|
66
|
+
if (isAndroid15OrAbove()) {
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
// Android 15+: edge-to-edge. Overlay views paint the bar regions;
|
|
69
|
+
// JS gets real insets.
|
|
64
70
|
WindowCompat.setDecorFitsSystemWindows(window, false);
|
|
65
71
|
|
|
66
72
|
window.setStatusBarColor(Color.TRANSPARENT);
|
|
@@ -73,19 +79,15 @@ public class CapacitorStatusBar extends Plugin {
|
|
|
73
79
|
int navBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
|
|
74
80
|
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
|
|
75
81
|
|
|
76
|
-
// Size status bar overlay
|
|
77
82
|
View statusOverlay = ((ViewGroup) v).findViewWithTag(STATUS_BAR_OVERLAY_TAG);
|
|
78
83
|
if (statusOverlay != null) {
|
|
79
84
|
ViewGroup.LayoutParams params = statusOverlay.getLayoutParams();
|
|
80
85
|
if (params.height != top) {
|
|
81
86
|
params.height = top;
|
|
82
87
|
statusOverlay.setLayoutParams(params);
|
|
83
|
-
Log.d(TAG, "insetsListener: status bar overlay height=" + top);
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
// Size navigation bar overlay — collapse it when the keyboard is
|
|
88
|
-
// open since the nav bar is hidden behind the IME.
|
|
89
91
|
int overlayBottom = imeVisible ? 0 : navBottom;
|
|
90
92
|
View navOverlay = ((ViewGroup) v).findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
91
93
|
if (navOverlay != null) {
|
|
@@ -93,255 +95,244 @@ public class CapacitorStatusBar extends Plugin {
|
|
|
93
95
|
if (params.height != overlayBottom) {
|
|
94
96
|
params.height = overlayBottom;
|
|
95
97
|
navOverlay.setLayoutParams(params);
|
|
96
|
-
Log.d(TAG, "insetsListener: nav bar overlay height=" + overlayBottom
|
|
97
|
-
+ " (imeVisible=" + imeVisible + ")");
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Inset the WebView via layout margins so it never renders behind
|
|
102
|
-
// system bars. When the keyboard is open the nav bar is behind the
|
|
103
|
-
// IME, so drop the bottom margin to avoid a gap above the keyboard.
|
|
104
|
-
if (webView != null
|
|
105
|
-
&& webView.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
|
|
106
|
-
ViewGroup.MarginLayoutParams mlp =
|
|
107
|
-
(ViewGroup.MarginLayoutParams) webView.getLayoutParams();
|
|
108
|
-
int left = insets.getInsets(WindowInsetsCompat.Type.systemBars()).left;
|
|
109
|
-
int right = insets.getInsets(WindowInsetsCompat.Type.systemBars()).right;
|
|
110
|
-
int bottomMargin = imeVisible ? 0 : navBottom;
|
|
111
|
-
if (mlp.topMargin != top || mlp.bottomMargin != bottomMargin
|
|
112
|
-
|| mlp.leftMargin != left || mlp.rightMargin != right) {
|
|
113
|
-
mlp.topMargin = top;
|
|
114
|
-
mlp.bottomMargin = bottomMargin;
|
|
115
|
-
mlp.leftMargin = left;
|
|
116
|
-
mlp.rightMargin = right;
|
|
117
|
-
webView.setLayoutParams(mlp);
|
|
118
|
-
Log.d(TAG, "insetsListener: webView margins t=" + top
|
|
119
|
-
+ " b=" + bottomMargin + " l=" + left + " r=" + right
|
|
120
|
-
+ " (imeVisible=" + imeVisible + ")");
|
|
121
98
|
}
|
|
122
99
|
}
|
|
123
100
|
|
|
124
|
-
return
|
|
101
|
+
return insets;
|
|
125
102
|
});
|
|
126
103
|
|
|
127
104
|
decorView.requestApplyInsets();
|
|
128
|
-
Log.d(TAG, "ensureEdgeToEdgeConfigured: edge-to-edge
|
|
105
|
+
Log.d(TAG, "ensureEdgeToEdgeConfigured: edge-to-edge (API " + Build.VERSION.SDK_INT + ")");
|
|
129
106
|
} else {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
window.setNavigationBarContrastEnforced(false);
|
|
133
|
-
}
|
|
107
|
+
// API < 35: keep WebView inside the safe area. System draws bars.
|
|
108
|
+
WindowCompat.setDecorFitsSystemWindows(window, true);
|
|
134
109
|
|
|
135
|
-
|
|
110
|
+
// Ensure the system draws bar backgrounds with the colors we set
|
|
111
|
+
// (not a translucent scrim).
|
|
112
|
+
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
|
113
|
+
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
|
114
|
+
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
|
115
|
+
|
|
116
|
+
applyLegacyContrastPolicy(window);
|
|
117
|
+
Log.d(TAG, "ensureEdgeToEdgeConfigured: fitted layout (API " + Build.VERSION.SDK_INT + ")");
|
|
136
118
|
}
|
|
137
119
|
}
|
|
138
120
|
|
|
139
121
|
public void setOverlaysWebView(Activity activity, boolean overlay) {
|
|
140
|
-
// No-op on Android. Exposed for API parity with iOS.
|
|
141
122
|
Log.d(TAG, "setOverlaysWebView: no-op on Android, overlay=" + overlay);
|
|
142
123
|
}
|
|
143
124
|
|
|
144
125
|
public void showStatusBar(Activity activity, boolean animated) {
|
|
145
|
-
Log.d(TAG, "showStatusBar: animated=" + animated + ",
|
|
146
|
-
+ Build.VERSION.SDK_INT);
|
|
126
|
+
Log.d(TAG, "showStatusBar: animated=" + animated + ", API=" + Build.VERSION.SDK_INT);
|
|
147
127
|
Window window = activity.getWindow();
|
|
148
128
|
View decorView = window.getDecorView();
|
|
129
|
+
statusBarHidden = false;
|
|
130
|
+
navBarHidden = false;
|
|
149
131
|
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
WindowInsetsController controller = window.getInsetsController();
|
|
153
|
-
if (controller != null) {
|
|
154
|
-
Log.d(TAG, "showStatusBar: showing system bars (API 30+)");
|
|
155
|
-
// Show both status and navigation bars together
|
|
156
|
-
controller.show(WindowInsets.Type.systemBars());
|
|
157
|
-
// Set behavior for transient bars (user can swipe to reveal)
|
|
158
|
-
controller.setSystemBarsBehavior(
|
|
159
|
-
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
160
|
-
} else {
|
|
161
|
-
Log.w(TAG, "showStatusBar: WindowInsetsController is null");
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
// API 29 (Android 10) - Use system UI visibility flags (deprecated but
|
|
165
|
-
// necessary)
|
|
166
|
-
Log.d(TAG, "showStatusBar: showing using system UI flags (API 29)");
|
|
167
|
-
// Set to visible state - clear all immersive flags
|
|
168
|
-
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
|
|
132
|
+
if (!isAndroid15OrAbove()) {
|
|
133
|
+
WindowCompat.setDecorFitsSystemWindows(window, true);
|
|
169
134
|
}
|
|
170
135
|
|
|
171
|
-
//
|
|
172
|
-
|
|
136
|
+
// Clear fullscreen flag so bars can appear again.
|
|
137
|
+
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
138
|
+
|
|
139
|
+
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(window, decorView);
|
|
140
|
+
controller.show(WindowInsetsCompat.Type.statusBars());
|
|
141
|
+
controller.show(WindowInsetsCompat.Type.navigationBars());
|
|
173
142
|
|
|
174
|
-
// Restore
|
|
175
|
-
|
|
143
|
+
// Restore overlays so colors reappear on both legacy and edge-to-edge.
|
|
144
|
+
ViewGroup dv = (ViewGroup) decorView;
|
|
145
|
+
View statusOverlay = dv.findViewWithTag(STATUS_BAR_OVERLAY_TAG);
|
|
146
|
+
if (statusOverlay != null) {
|
|
147
|
+
statusOverlay.setVisibility(View.VISIBLE);
|
|
148
|
+
}
|
|
149
|
+
View navOverlay = dv.findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
150
|
+
if (navOverlay != null) {
|
|
151
|
+
navOverlay.setVisibility(View.VISIBLE);
|
|
152
|
+
}
|
|
176
153
|
|
|
177
|
-
|
|
178
|
-
showNavigationBar(activity, true);
|
|
154
|
+
reapplyCurrentStyle(activity);
|
|
179
155
|
}
|
|
180
156
|
|
|
181
157
|
public void hideStatusBar(Activity activity) {
|
|
158
|
+
Log.d(TAG, "hideStatusBar: API=" + Build.VERSION.SDK_INT);
|
|
182
159
|
Window window = activity.getWindow();
|
|
183
160
|
View decorView = window.getDecorView();
|
|
161
|
+
statusBarHidden = true;
|
|
162
|
+
navBarHidden = true;
|
|
184
163
|
|
|
185
|
-
//
|
|
186
|
-
|
|
164
|
+
// Edge-to-edge so the WebView extends into the freed bar regions.
|
|
165
|
+
WindowCompat.setDecorFitsSystemWindows(window, false);
|
|
187
166
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
167
|
+
// Clear flags that can block the bars from hiding.
|
|
168
|
+
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
|
169
|
+
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
|
170
|
+
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
|
171
|
+
|
|
172
|
+
ViewGroup dv = (ViewGroup) decorView;
|
|
173
|
+
View statusOverlay = dv.findViewWithTag(STATUS_BAR_OVERLAY_TAG);
|
|
174
|
+
if (statusOverlay != null) {
|
|
175
|
+
statusOverlay.setVisibility(View.GONE);
|
|
176
|
+
}
|
|
177
|
+
View navOverlay = dv.findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
178
|
+
if (navOverlay != null) {
|
|
179
|
+
navOverlay.setVisibility(View.GONE);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
hideSystemBarsNow(window, decorView);
|
|
183
|
+
|
|
184
|
+
window.setStatusBarColor(Color.TRANSPARENT);
|
|
185
|
+
window.setNavigationBarColor(Color.TRANSPARENT);
|
|
186
|
+
|
|
187
|
+
// Re-apply on next frame to beat any post-layout reapplication from
|
|
188
|
+
// the framework or other plugins that may re-show bars.
|
|
189
|
+
decorView.post(() -> hideSystemBarsNow(window, decorView));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private void hideSystemBarsNow(Window window, View decorView) {
|
|
193
|
+
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(window, decorView);
|
|
194
|
+
controller.setSystemBarsBehavior(
|
|
195
|
+
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
196
|
+
controller.hide(WindowInsetsCompat.Type.statusBars());
|
|
197
|
+
controller.hide(WindowInsetsCompat.Type.navigationBars());
|
|
198
|
+
|
|
199
|
+
// Belt-and-suspenders: on API 30–34, set FLAG_FULLSCREEN as a fallback
|
|
200
|
+
// in case the controller call is ignored by theme/manifest state.
|
|
201
|
+
if (!isAndroid15OrAbove()) {
|
|
202
|
+
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Legacy path (API < 30): also toggle system UI visibility flags.
|
|
206
|
+
if (!supportsInsetsController()) {
|
|
206
207
|
decorView.setSystemUiVisibility(
|
|
207
|
-
|
|
208
|
+
decorView.getSystemUiVisibility()
|
|
209
|
+
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
208
210
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
209
|
-
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
210
211
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
211
|
-
| View.
|
|
212
|
-
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
|
212
|
+
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
213
|
+
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
|
214
|
+
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
|
|
213
215
|
}
|
|
214
|
-
|
|
215
|
-
// Make the overlay backgrounds transparent so content shows through
|
|
216
|
-
makeStatusBarBackgroundTransparent(activity);
|
|
217
216
|
}
|
|
218
217
|
|
|
219
218
|
public void setStyle(Activity activity, String style, @Nullable String colorHex) {
|
|
220
219
|
Log.d(TAG, "setStyle: style=" + style + ", colorHex=" + colorHex);
|
|
221
220
|
Window window = activity.getWindow();
|
|
222
221
|
|
|
223
|
-
|
|
222
|
+
if (!isAndroid15OrAbove()) {
|
|
223
|
+
statusBarHidden = false;
|
|
224
|
+
navBarHidden = false;
|
|
225
|
+
ensureLegacyOpaqueSystemBars(window);
|
|
226
|
+
}
|
|
227
|
+
|
|
224
228
|
currentStyle = style;
|
|
225
229
|
currentColorHex = colorHex;
|
|
226
230
|
|
|
227
|
-
// Determine icon appearance based on style
|
|
228
231
|
boolean lightBackground;
|
|
229
|
-
|
|
232
|
+
int statusBarColor;
|
|
233
|
+
int navBarColor;
|
|
234
|
+
|
|
235
|
+
if ("CUSTOM".equalsIgnoreCase(style) && colorHex != null) {
|
|
236
|
+
int color = parseColorOrDefault(colorHex, Color.BLACK);
|
|
237
|
+
lightBackground = isEffectiveLightColor(color);
|
|
238
|
+
statusBarColor = color;
|
|
239
|
+
navBarColor = color;
|
|
240
|
+
} else if ("LIGHT".equalsIgnoreCase(style)) {
|
|
230
241
|
lightBackground = true;
|
|
242
|
+
statusBarColor = Color.WHITE;
|
|
243
|
+
navBarColor = Color.WHITE;
|
|
231
244
|
} else if ("DARK".equalsIgnoreCase(style)) {
|
|
232
245
|
lightBackground = false;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
int parsed = parseColorOrDefault(colorHex, Color.BLACK);
|
|
236
|
-
lightBackground = isEffectiveLightColor(parsed);
|
|
246
|
+
statusBarColor = Color.BLACK;
|
|
247
|
+
navBarColor = Color.BLACK;
|
|
237
248
|
} else {
|
|
238
|
-
// Default: Auto-detect based on system theme (follow device theme)
|
|
239
249
|
boolean isSystemDarkMode = isSystemInDarkMode(activity);
|
|
240
250
|
lightBackground = !isSystemDarkMode;
|
|
251
|
+
statusBarColor = isSystemDarkMode ? Color.BLACK : Color.WHITE;
|
|
252
|
+
navBarColor = statusBarColor;
|
|
241
253
|
}
|
|
242
254
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
int color = parseColorOrDefault(colorHex, lightBackground ? Color.WHITE : Color.BLACK);
|
|
246
|
-
currentStatusBarColor = color;
|
|
247
|
-
currentNavBarColor = color;
|
|
248
|
-
applyStatusBarBackground(activity, color);
|
|
249
|
-
applyNavigationBarBackground(activity, color);
|
|
250
|
-
} else if ("LIGHT".equalsIgnoreCase(style)) {
|
|
251
|
-
currentStatusBarColor = Color.WHITE;
|
|
252
|
-
currentNavBarColor = Color.WHITE;
|
|
253
|
-
applyStatusBarBackground(activity, Color.WHITE);
|
|
254
|
-
applyNavigationBarBackground(activity, Color.WHITE);
|
|
255
|
-
} else if ("DARK".equalsIgnoreCase(style)) {
|
|
256
|
-
currentStatusBarColor = Color.BLACK;
|
|
257
|
-
currentNavBarColor = Color.BLACK;
|
|
258
|
-
applyStatusBarBackground(activity, Color.BLACK);
|
|
259
|
-
applyNavigationBarBackground(activity, Color.BLACK);
|
|
260
|
-
} else {
|
|
261
|
-
// Default: Auto-detect based on system theme
|
|
262
|
-
boolean isSystemDarkMode = isSystemInDarkMode(activity);
|
|
263
|
-
int themeColor = isSystemDarkMode ? Color.BLACK : Color.WHITE;
|
|
264
|
-
currentStatusBarColor = themeColor;
|
|
265
|
-
currentNavBarColor = themeColor;
|
|
266
|
-
applyStatusBarBackground(activity, themeColor);
|
|
267
|
-
applyNavigationBarBackground(activity, themeColor);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Set icon appearance AFTER background operations.
|
|
271
|
-
// On Android 13 (API 33), applyStatusBarBackground triggers
|
|
272
|
-
// requestApplyInsets()
|
|
273
|
-
// which causes the WindowInsetsController to reset setSystemBarsAppearance.
|
|
274
|
-
// Calling setLightStatusBarIcons last ensures the icon style is not overridden.
|
|
275
|
-
setLightStatusBarIcons(window, lightBackground);
|
|
276
|
-
}
|
|
255
|
+
currentStatusBarColor = statusBarColor;
|
|
256
|
+
currentNavBarColor = navBarColor;
|
|
277
257
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
public void setBackground(Activity activity, @Nullable String colorHex) {
|
|
285
|
-
Log.d(TAG, "setBackground: colorHex=" + colorHex);
|
|
286
|
-
|
|
287
|
-
if (colorHex == null) {
|
|
288
|
-
Log.w(TAG, "setBackground: colorHex is null");
|
|
289
|
-
return;
|
|
258
|
+
if (!isAndroid15OrAbove()) {
|
|
259
|
+
// Clear any translucent scrim the framework/theme may have applied,
|
|
260
|
+
// and ensure the system draws bar backgrounds with our colors.
|
|
261
|
+
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
|
262
|
+
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
|
263
|
+
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
|
290
264
|
}
|
|
291
265
|
|
|
292
|
-
|
|
293
|
-
|
|
266
|
+
applyLegacyContrastPolicy(window);
|
|
267
|
+
setLightStatusBarIcons(window, lightBackground);
|
|
268
|
+
applyStatusBarBackground(activity, statusBarColor);
|
|
269
|
+
applyNavigationBarBackground(activity, navBarColor);
|
|
270
|
+
|
|
271
|
+
applyLegacyContrastPolicy(window);
|
|
272
|
+
|
|
273
|
+
if (!isAndroid15OrAbove()) {
|
|
274
|
+
// Re-apply on the next frame to win against same-tick framework/plugin updates.
|
|
275
|
+
window.getDecorView().post(() -> {
|
|
276
|
+
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
|
277
|
+
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
|
278
|
+
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
|
279
|
+
applyLegacyContrastPolicy(window);
|
|
280
|
+
window.setStatusBarColor(statusBarColor);
|
|
281
|
+
window.setNavigationBarColor(navBarColor);
|
|
282
|
+
setLightStatusBarIcons(window, lightBackground);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
294
285
|
}
|
|
295
286
|
|
|
296
|
-
/**
|
|
297
|
-
* Get the safe area insets.
|
|
298
|
-
* Returns the insets for status bar, navigation bar, and notch areas.
|
|
299
|
-
*
|
|
300
|
-
* @param activity The activity to get the insets from
|
|
301
|
-
* @return A map containing top, bottom, left, and right inset values in dp
|
|
302
|
-
* (density-independent pixels)
|
|
303
|
-
*/
|
|
304
287
|
public java.util.Map<String, Integer> getSafeAreaInsets(Activity activity) {
|
|
305
|
-
Log.d(TAG, "getSafeAreaInsets");
|
|
306
288
|
java.util.Map<String, Integer> insets = new java.util.HashMap<>();
|
|
307
289
|
float density = activity.getResources().getDisplayMetrics().density;
|
|
308
|
-
|
|
309
290
|
View decorView = activity.getWindow().getDecorView();
|
|
310
291
|
WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(decorView);
|
|
311
292
|
|
|
312
|
-
if (windowInsets
|
|
313
|
-
// Use WindowInsetsCompat for accurate, type-specific insets across all API
|
|
314
|
-
// levels
|
|
315
|
-
androidx.core.graphics.Insets statusBars = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars());
|
|
316
|
-
androidx.core.graphics.Insets navBars = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars());
|
|
317
|
-
androidx.core.graphics.Insets displayCutout = windowInsets
|
|
318
|
-
.getInsets(WindowInsetsCompat.Type.displayCutout());
|
|
319
|
-
|
|
320
|
-
int topPx = Math.max(statusBars.top, displayCutout.top);
|
|
321
|
-
int bottomPx = Math.max(navBars.bottom, displayCutout.bottom);
|
|
322
|
-
int leftPx = Math.max(navBars.left, displayCutout.left);
|
|
323
|
-
int rightPx = Math.max(navBars.right, displayCutout.right);
|
|
324
|
-
|
|
325
|
-
// Convert physical pixels to dp (CSS pixels) for the WebView layer
|
|
326
|
-
insets.put("top", Math.round(topPx / density));
|
|
327
|
-
insets.put("bottom", Math.round(bottomPx / density));
|
|
328
|
-
insets.put("left", Math.round(leftPx / density));
|
|
329
|
-
insets.put("right", Math.round(rightPx / density));
|
|
330
|
-
|
|
331
|
-
Log.d(TAG, "getSafeAreaInsets: topPx=" + topPx + " bottomPx=" + bottomPx
|
|
332
|
-
+ " density=" + density
|
|
333
|
-
+ " -> top=" + insets.get("top") + "dp"
|
|
334
|
-
+ " bottom=" + insets.get("bottom") + "dp"
|
|
335
|
-
+ " left=" + insets.get("left") + "dp"
|
|
336
|
-
+ " right=" + insets.get("right") + "dp");
|
|
337
|
-
} else {
|
|
293
|
+
if (windowInsets == null) {
|
|
338
294
|
insets.put("top", 0);
|
|
339
295
|
insets.put("bottom", 0);
|
|
340
296
|
insets.put("left", 0);
|
|
341
297
|
insets.put("right", 0);
|
|
342
298
|
Log.w(TAG, "getSafeAreaInsets: windowInsets is null");
|
|
299
|
+
return insets;
|
|
343
300
|
}
|
|
344
301
|
|
|
302
|
+
androidx.core.graphics.Insets statusBars = windowInsets
|
|
303
|
+
.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.statusBars());
|
|
304
|
+
androidx.core.graphics.Insets navBars = windowInsets
|
|
305
|
+
.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.navigationBars());
|
|
306
|
+
androidx.core.graphics.Insets displayCutout = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout());
|
|
307
|
+
|
|
308
|
+
int topPx;
|
|
309
|
+
int bottomPx;
|
|
310
|
+
int leftPx;
|
|
311
|
+
int rightPx;
|
|
312
|
+
|
|
313
|
+
if (isAndroid15OrAbove()) {
|
|
314
|
+
topPx = Math.max(statusBars.top, displayCutout.top);
|
|
315
|
+
bottomPx = Math.max(navBars.bottom, displayCutout.bottom);
|
|
316
|
+
leftPx = Math.max(navBars.left, displayCutout.left);
|
|
317
|
+
rightPx = Math.max(navBars.right, displayCutout.right);
|
|
318
|
+
} else {
|
|
319
|
+
topPx = statusBarHidden ? statusBars.top : 0;
|
|
320
|
+
bottomPx = navBarHidden ? navBars.bottom : 0;
|
|
321
|
+
leftPx = 0;
|
|
322
|
+
rightPx = 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
insets.put("top", Math.round(topPx / density));
|
|
326
|
+
insets.put("bottom", Math.round(bottomPx / density));
|
|
327
|
+
insets.put("left", Math.round(leftPx / density));
|
|
328
|
+
insets.put("right", Math.round(rightPx / density));
|
|
329
|
+
|
|
330
|
+
Log.d(TAG, "getSafeAreaInsets: top=" + insets.get("top") + "px"
|
|
331
|
+
+ " bottom=" + insets.get("bottom") + "px"
|
|
332
|
+
+ " left=" + insets.get("left") + "px"
|
|
333
|
+
+ " right=" + insets.get("right") + "px"
|
|
334
|
+
+ " (statusHidden=" + statusBarHidden + ", navHidden=" + navBarHidden + ")");
|
|
335
|
+
|
|
345
336
|
return insets;
|
|
346
337
|
}
|
|
347
338
|
|
|
@@ -349,26 +340,33 @@ public class CapacitorStatusBar extends Plugin {
|
|
|
349
340
|
Log.d(TAG, "showNavigationBar: animated=" + animated + ", API=" + Build.VERSION.SDK_INT);
|
|
350
341
|
Window window = activity.getWindow();
|
|
351
342
|
View decorView = window.getDecorView();
|
|
343
|
+
navBarHidden = false;
|
|
352
344
|
|
|
353
|
-
if (
|
|
345
|
+
if (!isAndroid15OrAbove() && !statusBarHidden) {
|
|
346
|
+
WindowCompat.setDecorFitsSystemWindows(window, true);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (supportsInsetsController()) {
|
|
354
350
|
WindowInsetsController controller = window.getInsetsController();
|
|
355
351
|
if (controller != null) {
|
|
356
|
-
Log.d(TAG, "showNavigationBar: showing navigation bar (API 30+)");
|
|
357
352
|
controller.show(WindowInsets.Type.navigationBars());
|
|
358
353
|
controller.setSystemBarsBehavior(
|
|
359
354
|
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
360
|
-
} else {
|
|
361
|
-
Log.w(TAG, "showNavigationBar: WindowInsetsController is null");
|
|
362
355
|
}
|
|
363
356
|
} else {
|
|
364
|
-
Log.d(TAG, "showNavigationBar: showing using system UI flags (API 29)");
|
|
365
357
|
int flags = decorView.getSystemUiVisibility();
|
|
366
358
|
flags &= ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
|
|
367
359
|
flags &= ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
|
360
|
+
flags &= ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
|
|
368
361
|
decorView.setSystemUiVisibility(flags);
|
|
369
362
|
}
|
|
370
363
|
|
|
371
|
-
|
|
364
|
+
ViewGroup dv = (ViewGroup) decorView;
|
|
365
|
+
View navOverlay = dv.findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
366
|
+
if (navOverlay != null) {
|
|
367
|
+
navOverlay.setVisibility(View.VISIBLE);
|
|
368
|
+
}
|
|
369
|
+
|
|
372
370
|
applyNavigationBarBackground(activity, currentNavBarColor);
|
|
373
371
|
}
|
|
374
372
|
|
|
@@ -376,169 +374,150 @@ public class CapacitorStatusBar extends Plugin {
|
|
|
376
374
|
Log.d(TAG, "hideNavigationBar: animation=" + animation + ", API=" + Build.VERSION.SDK_INT);
|
|
377
375
|
Window window = activity.getWindow();
|
|
378
376
|
View decorView = window.getDecorView();
|
|
377
|
+
navBarHidden = true;
|
|
379
378
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
Log.d(TAG, "hideNavigationBar: fade mode - hiding navigation bar with transparent background");
|
|
379
|
+
if (!isAndroid15OrAbove()) {
|
|
380
|
+
WindowCompat.setDecorFitsSystemWindows(window, false);
|
|
381
|
+
}
|
|
384
382
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
392
|
-
}
|
|
393
|
-
} else {
|
|
394
|
-
decorView.setSystemUiVisibility(
|
|
395
|
-
decorView.getSystemUiVisibility()
|
|
396
|
-
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
397
|
-
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
398
|
-
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
399
|
-
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
|
383
|
+
if (supportsInsetsController()) {
|
|
384
|
+
WindowInsetsController controller = window.getInsetsController();
|
|
385
|
+
if (controller != null) {
|
|
386
|
+
controller.hide(WindowInsets.Type.navigationBars());
|
|
387
|
+
controller.setSystemBarsBehavior(
|
|
388
|
+
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
400
389
|
}
|
|
390
|
+
} else {
|
|
391
|
+
decorView.setSystemUiVisibility(
|
|
392
|
+
decorView.getSystemUiVisibility()
|
|
393
|
+
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
394
|
+
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
395
|
+
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
396
|
+
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
|
397
|
+
}
|
|
401
398
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
navOverlay.setBackgroundColor(Color.TRANSPARENT);
|
|
408
|
-
}
|
|
409
|
-
} else {
|
|
410
|
-
window.setNavigationBarColor(Color.TRANSPARENT);
|
|
411
|
-
}
|
|
412
|
-
} else if ("slide".equals(animationType)) {
|
|
413
|
-
Log.d(TAG, "hideNavigationBar: slide mode - hiding navigation bar completely");
|
|
414
|
-
|
|
415
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
416
|
-
WindowInsetsController controller = window.getInsetsController();
|
|
417
|
-
if (controller != null) {
|
|
418
|
-
controller.hide(WindowInsets.Type.navigationBars());
|
|
419
|
-
controller.setSystemBarsBehavior(
|
|
420
|
-
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
421
|
-
} else {
|
|
422
|
-
Log.w(TAG, "hideNavigationBar: WindowInsetsController is null");
|
|
423
|
-
}
|
|
424
|
-
} else {
|
|
425
|
-
Log.d(TAG, "hideNavigationBar: hiding using system UI flags (API 29)");
|
|
426
|
-
decorView.setSystemUiVisibility(
|
|
427
|
-
decorView.getSystemUiVisibility()
|
|
428
|
-
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
429
|
-
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
430
|
-
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
431
|
-
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
|
432
|
-
}
|
|
399
|
+
ViewGroup dv = (ViewGroup) decorView;
|
|
400
|
+
View navOverlay = dv.findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
401
|
+
if (navOverlay != null) {
|
|
402
|
+
navOverlay.setVisibility(View.GONE);
|
|
403
|
+
}
|
|
433
404
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
ViewGroup dv = (ViewGroup) decorView;
|
|
437
|
-
View navOverlay = dv.findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
438
|
-
if (navOverlay != null) {
|
|
439
|
-
navOverlay.setBackgroundColor(Color.TRANSPARENT);
|
|
440
|
-
}
|
|
441
|
-
} else {
|
|
442
|
-
window.setNavigationBarColor(Color.TRANSPARENT);
|
|
443
|
-
}
|
|
444
|
-
} else {
|
|
445
|
-
Log.w(TAG, "hideNavigationBar: unknown animation type '" + animationType + "', defaulting to slide");
|
|
446
|
-
hideNavigationBar(activity, "slide");
|
|
405
|
+
if (!isAndroid15OrAbove()) {
|
|
406
|
+
window.setNavigationBarColor(Color.TRANSPARENT);
|
|
447
407
|
}
|
|
448
408
|
}
|
|
449
409
|
|
|
450
|
-
/**
|
|
451
|
-
* Reapply the current style and colors after showing bars.
|
|
452
|
-
* This ensures colors are preserved when hiding and then showing.
|
|
453
|
-
*/
|
|
454
410
|
private void reapplyCurrentStyle(Activity activity) {
|
|
455
|
-
Log.d(TAG, "reapplyCurrentStyle: style=" + currentStyle + ", colorHex=" + currentColorHex);
|
|
456
411
|
Window window = activity.getWindow();
|
|
457
412
|
|
|
458
|
-
|
|
413
|
+
if (!isAndroid15OrAbove() && !statusBarHidden && !navBarHidden) {
|
|
414
|
+
ensureLegacyOpaqueSystemBars(window);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
boolean light;
|
|
459
418
|
if ("LIGHT".equalsIgnoreCase(currentStyle)) {
|
|
460
|
-
|
|
419
|
+
light = true;
|
|
461
420
|
} else if ("DARK".equalsIgnoreCase(currentStyle)) {
|
|
462
|
-
|
|
421
|
+
light = false;
|
|
463
422
|
} else if ("CUSTOM".equalsIgnoreCase(currentStyle)) {
|
|
464
|
-
|
|
465
|
-
boolean isLight = isEffectiveLightColor(parsed);
|
|
466
|
-
setLightStatusBarIcons(window, isLight);
|
|
423
|
+
light = isEffectiveLightColor(parseColorOrDefault(currentColorHex, Color.BLACK));
|
|
467
424
|
} else {
|
|
468
|
-
|
|
469
|
-
boolean isSystemDarkMode = isSystemInDarkMode(activity);
|
|
470
|
-
setLightStatusBarIcons(window, !isSystemDarkMode);
|
|
425
|
+
light = !isSystemInDarkMode(activity);
|
|
471
426
|
}
|
|
472
427
|
|
|
473
|
-
|
|
428
|
+
applyLegacyContrastPolicy(window);
|
|
429
|
+
setLightStatusBarIcons(window, light);
|
|
474
430
|
applyStatusBarBackground(activity, currentStatusBarColor);
|
|
475
431
|
applyNavigationBarBackground(activity, currentNavBarColor);
|
|
432
|
+
|
|
433
|
+
applyLegacyContrastPolicy(window);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private void ensureLegacyOpaqueSystemBars(Window window) {
|
|
437
|
+
WindowCompat.setDecorFitsSystemWindows(window, true);
|
|
438
|
+
|
|
439
|
+
// Ensure bar backgrounds are drawn opaquely on API < 35.
|
|
440
|
+
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
|
441
|
+
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
|
442
|
+
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
|
443
|
+
|
|
444
|
+
if (supportsInsetsController()) {
|
|
445
|
+
WindowInsetsController controller = window.getInsetsController();
|
|
446
|
+
if (controller != null) {
|
|
447
|
+
controller.setSystemBarsBehavior(
|
|
448
|
+
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
449
|
+
controller.show(WindowInsets.Type.systemBars());
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!supportsInsetsController()) {
|
|
454
|
+
View decorView = window.getDecorView();
|
|
455
|
+
int flags = decorView.getSystemUiVisibility();
|
|
456
|
+
flags &= ~View.SYSTEM_UI_FLAG_FULLSCREEN;
|
|
457
|
+
flags &= ~View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
|
|
458
|
+
flags &= ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
|
|
459
|
+
flags &= ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
|
|
460
|
+
flags &= ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
|
461
|
+
decorView.setSystemUiVisibility(flags);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
applyLegacyContrastPolicy(window);
|
|
465
|
+
|
|
466
|
+
window.getDecorView().requestApplyInsets();
|
|
476
467
|
}
|
|
477
468
|
|
|
478
469
|
private void setLightStatusBarIcons(Window window, boolean light) {
|
|
479
|
-
Log.d(TAG, "setLightStatusBarIcons: light=" + light + ", API=" + Build.VERSION.SDK_INT);
|
|
480
470
|
View decorView = window.getDecorView();
|
|
481
471
|
|
|
482
|
-
if (
|
|
483
|
-
// API 30+ - Use WindowInsetsController
|
|
472
|
+
if (supportsInsetsController()) {
|
|
484
473
|
WindowInsetsController controller = window.getInsetsController();
|
|
485
474
|
if (controller == null) {
|
|
486
|
-
Log.w(TAG, "setLightStatusBarIcons: WindowInsetsController is null");
|
|
487
475
|
return;
|
|
488
476
|
}
|
|
489
477
|
int mask = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
|
490
478
|
| WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
|
|
491
479
|
controller.setSystemBarsAppearance(light ? mask : 0, mask);
|
|
492
|
-
Log.d(TAG, "setLightStatusBarIcons: applied using WindowInsetsController (API 30+)");
|
|
493
480
|
} else {
|
|
494
481
|
int flags = decorView.getSystemUiVisibility();
|
|
495
482
|
if (light) {
|
|
496
|
-
// Light background -> dark icons
|
|
497
483
|
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
|
|
498
484
|
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
|
499
|
-
Log.d(TAG, "setLightStatusBarIcons: set light icons (dark text) (API 29)");
|
|
500
485
|
} else {
|
|
501
|
-
// Dark background -> light icons
|
|
502
486
|
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
|
|
503
487
|
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
|
504
|
-
Log.d(TAG, "setLightStatusBarIcons: set dark icons (light text) (API 29)");
|
|
505
488
|
}
|
|
506
489
|
decorView.setSystemUiVisibility(flags);
|
|
507
490
|
}
|
|
508
491
|
}
|
|
509
492
|
|
|
510
493
|
private void applyStatusBarBackground(Activity activity, @ColorInt int color) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
} else {
|
|
517
|
-
// API 29–30: no edge-to-edge, native API works reliably.
|
|
518
|
-
activity.getWindow().setStatusBarColor(color);
|
|
494
|
+
Window window = activity.getWindow();
|
|
495
|
+
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
|
496
|
+
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
|
497
|
+
if (supportsContrastControl()) {
|
|
498
|
+
window.setStatusBarContrastEnforced(false);
|
|
519
499
|
}
|
|
500
|
+
// Paint status bar transparent; our overlay view paints the color so the
|
|
501
|
+
// framework's contrast scrim cannot affect it.
|
|
502
|
+
window.setStatusBarColor(Color.TRANSPARENT);
|
|
503
|
+
ensureStatusBarOverlay(activity, color);
|
|
520
504
|
}
|
|
521
505
|
|
|
522
506
|
private void applyNavigationBarBackground(Activity activity, @ColorInt int color) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
// Use an overlay View drawn at the bottom of the screen.
|
|
529
|
-
ensureNavBarOverlay(activity, color);
|
|
530
|
-
} else {
|
|
531
|
-
// API 29–30: no edge-to-edge, native API works for button navigation.
|
|
532
|
-
activity.getWindow().setNavigationBarColor(color);
|
|
507
|
+
Window window = activity.getWindow();
|
|
508
|
+
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
|
509
|
+
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
|
510
|
+
if (supportsContrastControl()) {
|
|
511
|
+
window.setNavigationBarContrastEnforced(false);
|
|
533
512
|
}
|
|
513
|
+
window.setNavigationBarColor(Color.TRANSPARENT);
|
|
514
|
+
ensureNavBarOverlay(activity, color);
|
|
534
515
|
}
|
|
535
516
|
|
|
536
517
|
private void ensureStatusBarOverlay(Activity activity, @ColorInt int color) {
|
|
537
|
-
Log.d(TAG, "ensureStatusBarOverlay: color=#" + Integer.toHexString(color));
|
|
538
518
|
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
539
519
|
View existing = decorView.findViewWithTag(STATUS_BAR_OVERLAY_TAG);
|
|
540
520
|
|
|
541
|
-
// Get current status bar height synchronously for immediate sizing
|
|
542
521
|
int initialHeight = 0;
|
|
543
522
|
WindowInsetsCompat rootInsets = ViewCompat.getRootWindowInsets(decorView);
|
|
544
523
|
if (rootInsets != null) {
|
|
@@ -546,7 +525,6 @@ public class CapacitorStatusBar extends Plugin {
|
|
|
546
525
|
}
|
|
547
526
|
|
|
548
527
|
if (existing == null) {
|
|
549
|
-
Log.d(TAG, "ensureStatusBarOverlay: creating new overlay, initialHeight=" + initialHeight);
|
|
550
528
|
View overlay = new View(activity);
|
|
551
529
|
overlay.setTag(STATUS_BAR_OVERLAY_TAG);
|
|
552
530
|
overlay.setBackgroundColor(color);
|
|
@@ -557,38 +535,21 @@ public class CapacitorStatusBar extends Plugin {
|
|
|
557
535
|
overlay.setLayoutParams(lp);
|
|
558
536
|
|
|
559
537
|
decorView.addView(overlay);
|
|
560
|
-
// Sizing updates are handled by the unified listener in
|
|
561
|
-
// ensureEdgeToEdgeConfigured
|
|
562
538
|
decorView.requestApplyInsets();
|
|
563
539
|
} else {
|
|
564
|
-
Log.d(TAG, "ensureStatusBarOverlay: updating existing overlay");
|
|
565
540
|
existing.setBackgroundColor(color);
|
|
566
|
-
// Update height if it was 0 (listener hadn't fired yet)
|
|
567
541
|
ViewGroup.LayoutParams params = existing.getLayoutParams();
|
|
568
542
|
if (params.height == 0 && initialHeight > 0) {
|
|
569
543
|
params.height = initialHeight;
|
|
570
544
|
existing.setLayoutParams(params);
|
|
571
|
-
Log.d(TAG, "ensureStatusBarOverlay: fixed height to " + initialHeight);
|
|
572
545
|
}
|
|
573
546
|
}
|
|
574
547
|
}
|
|
575
548
|
|
|
576
|
-
private void removeStatusBarOverlayIfPresent(Activity activity) {
|
|
577
|
-
Log.d(TAG, "removeStatusBarOverlayIfPresent");
|
|
578
|
-
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
579
|
-
View existing = decorView.findViewWithTag(STATUS_BAR_OVERLAY_TAG);
|
|
580
|
-
if (existing != null) {
|
|
581
|
-
Log.d(TAG, "removeStatusBarOverlayIfPresent: removing overlay");
|
|
582
|
-
decorView.removeView(existing);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
549
|
private void ensureNavBarOverlay(Activity activity, @ColorInt int color) {
|
|
587
|
-
Log.d(TAG, "ensureNavBarOverlay: color=#" + Integer.toHexString(color));
|
|
588
550
|
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
589
551
|
View existing = decorView.findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
590
552
|
|
|
591
|
-
// Get current nav bar height synchronously for immediate sizing
|
|
592
553
|
int initialHeight = 0;
|
|
593
554
|
WindowInsetsCompat rootInsets = ViewCompat.getRootWindowInsets(decorView);
|
|
594
555
|
if (rootInsets != null) {
|
|
@@ -596,7 +557,6 @@ public class CapacitorStatusBar extends Plugin {
|
|
|
596
557
|
}
|
|
597
558
|
|
|
598
559
|
if (existing == null) {
|
|
599
|
-
Log.d(TAG, "ensureNavBarOverlay: creating new overlay, initialHeight=" + initialHeight);
|
|
600
560
|
View overlay = new View(activity);
|
|
601
561
|
overlay.setTag(NAV_BAR_OVERLAY_TAG);
|
|
602
562
|
overlay.setBackgroundColor(color);
|
|
@@ -608,94 +568,24 @@ public class CapacitorStatusBar extends Plugin {
|
|
|
608
568
|
overlay.setLayoutParams(lp);
|
|
609
569
|
|
|
610
570
|
decorView.addView(overlay);
|
|
611
|
-
// Sizing updates are handled by the unified listener in
|
|
612
|
-
// ensureEdgeToEdgeConfigured
|
|
613
571
|
decorView.requestApplyInsets();
|
|
614
572
|
} else {
|
|
615
|
-
Log.d(TAG, "ensureNavBarOverlay: updating existing overlay");
|
|
616
573
|
existing.setBackgroundColor(color);
|
|
617
|
-
// Update height if it was 0 (listener hadn't fired yet)
|
|
618
574
|
ViewGroup.LayoutParams params = existing.getLayoutParams();
|
|
619
575
|
if (params.height == 0 && initialHeight > 0) {
|
|
620
576
|
params.height = initialHeight;
|
|
621
577
|
existing.setLayoutParams(params);
|
|
622
|
-
Log.d(TAG, "ensureNavBarOverlay: fixed height to " + initialHeight);
|
|
623
578
|
}
|
|
624
579
|
}
|
|
625
580
|
}
|
|
626
581
|
|
|
627
|
-
private void removeNavBarOverlayIfPresent(Activity activity) {
|
|
628
|
-
Log.d(TAG, "removeNavBarOverlayIfPresent");
|
|
629
|
-
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
630
|
-
View existing = decorView.findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
631
|
-
if (existing != null) {
|
|
632
|
-
Log.d(TAG, "removeNavBarOverlayIfPresent: removing overlay");
|
|
633
|
-
decorView.removeView(existing);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
private void applyWindowBackground(Activity activity, @ColorInt int color) {
|
|
638
|
-
Log.d(TAG, "applyWindowBackground: body is always transparent; status/nav bars use overlays/native APIs");
|
|
639
|
-
View decorView = activity.getWindow().getDecorView();
|
|
640
|
-
decorView.setBackgroundColor(Color.TRANSPARENT);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Makes the status bar and navigation bar backgrounds transparent.
|
|
645
|
-
* This allows content to show through when the bars are hidden.
|
|
646
|
-
*/
|
|
647
|
-
private void makeStatusBarBackgroundTransparent(Activity activity) {
|
|
648
|
-
Log.d(TAG, "makeStatusBarBackgroundTransparent: API=" + Build.VERSION.SDK_INT);
|
|
649
|
-
Window window = activity.getWindow();
|
|
650
|
-
ViewGroup decorView = (ViewGroup) window.getDecorView();
|
|
651
|
-
|
|
652
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
653
|
-
// API 31+: edge-to-edge uses overlay views for both bars
|
|
654
|
-
View statusBarOverlay = decorView.findViewWithTag(STATUS_BAR_OVERLAY_TAG);
|
|
655
|
-
if (statusBarOverlay != null) {
|
|
656
|
-
statusBarOverlay.setBackgroundColor(Color.TRANSPARENT);
|
|
657
|
-
Log.d(TAG, "makeStatusBarBackgroundTransparent: status bar overlay made transparent");
|
|
658
|
-
}
|
|
659
|
-
View navBarOverlay = decorView.findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
660
|
-
if (navBarOverlay != null) {
|
|
661
|
-
navBarOverlay.setBackgroundColor(Color.TRANSPARENT);
|
|
662
|
-
Log.d(TAG, "makeStatusBarBackgroundTransparent: nav bar overlay made transparent");
|
|
663
|
-
}
|
|
664
|
-
} else {
|
|
665
|
-
// API 29–30: no edge-to-edge, native window API
|
|
666
|
-
window.setStatusBarColor(Color.TRANSPARENT);
|
|
667
|
-
window.setNavigationBarColor(Color.TRANSPARENT);
|
|
668
|
-
Log.d(TAG, "makeStatusBarBackgroundTransparent: set native bar colors to transparent");
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Restores the status bar and navigation bar backgrounds to their stored
|
|
674
|
-
* colors.
|
|
675
|
-
* Called when showing the bars after they were hidden.
|
|
676
|
-
*/
|
|
677
|
-
private void restoreStatusBarBackground(Activity activity) {
|
|
678
|
-
Log.d(TAG, "restoreStatusBarBackground: API=" + Build.VERSION.SDK_INT
|
|
679
|
-
+ ", currentStatusBarColor=#" + Integer.toHexString(currentStatusBarColor)
|
|
680
|
-
+ ", currentNavBarColor=#" + Integer.toHexString(currentNavBarColor));
|
|
681
|
-
|
|
682
|
-
// Restore all backgrounds to their stored colors
|
|
683
|
-
applyStatusBarBackground(activity, currentStatusBarColor);
|
|
684
|
-
applyNavigationBarBackground(activity, currentNavBarColor);
|
|
685
|
-
|
|
686
|
-
Log.d(TAG, "restoreStatusBarBackground: backgrounds restored");
|
|
687
|
-
}
|
|
688
|
-
|
|
689
582
|
@ColorInt
|
|
690
583
|
private int parseColorOrDefault(@Nullable String color, @ColorInt int def) {
|
|
691
584
|
if (color == null) {
|
|
692
|
-
Log.d(TAG, "parseColorOrDefault: color is null, using default");
|
|
693
585
|
return def;
|
|
694
586
|
}
|
|
695
587
|
try {
|
|
696
|
-
|
|
697
|
-
Log.d(TAG, "parseColorOrDefault: parsed color=" + color + " -> #" + Integer.toHexString(parsed));
|
|
698
|
-
return parsed;
|
|
588
|
+
return parseHexColor(color);
|
|
699
589
|
} catch (IllegalArgumentException ex) {
|
|
700
590
|
Log.w(TAG, "parseColorOrDefault: invalid color=" + color + ", using default");
|
|
701
591
|
return def;
|
|
@@ -703,12 +593,7 @@ public class CapacitorStatusBar extends Plugin {
|
|
|
703
593
|
}
|
|
704
594
|
|
|
705
595
|
/**
|
|
706
|
-
* Parse hex color string
|
|
707
|
-
* Handles both 6-digit (#RRGGBB) and 8-digit (#RRGGBBAA) formats.
|
|
708
|
-
*
|
|
709
|
-
* @param hex The hex color string (with or without # prefix)
|
|
710
|
-
* @return The parsed color as an integer
|
|
711
|
-
* @throws IllegalArgumentException if the color format is invalid
|
|
596
|
+
* Parse hex color string. Handles #RRGGBB and #RRGGBBAA formats.
|
|
712
597
|
*/
|
|
713
598
|
private int parseHexColor(String hex) throws IllegalArgumentException {
|
|
714
599
|
String hexSanitized = hex.trim().replaceFirst("^#", "");
|
|
@@ -721,15 +606,12 @@ public class CapacitorStatusBar extends Plugin {
|
|
|
721
606
|
long rgb = Long.parseLong(hexSanitized, 16);
|
|
722
607
|
|
|
723
608
|
if (hexSanitized.length() == 6) {
|
|
724
|
-
// 6-digit format: #RRGGBB (opaque)
|
|
725
609
|
return (int) (0xFF000000L | rgb);
|
|
726
610
|
} else {
|
|
727
|
-
// 8-digit format: #RRGGBBAA
|
|
728
611
|
int r = (int) ((rgb & 0xFF000000L) >> 24);
|
|
729
612
|
int g = (int) ((rgb & 0x00FF0000L) >> 16);
|
|
730
613
|
int b = (int) ((rgb & 0x0000FF00L) >> 8);
|
|
731
614
|
int a = (int) (rgb & 0x000000FFL);
|
|
732
|
-
|
|
733
615
|
return Color.argb(a, r, g, b);
|
|
734
616
|
}
|
|
735
617
|
} catch (NumberFormatException e) {
|
|
@@ -737,64 +619,33 @@ public class CapacitorStatusBar extends Plugin {
|
|
|
737
619
|
}
|
|
738
620
|
}
|
|
739
621
|
|
|
740
|
-
/**
|
|
741
|
-
* Calculate effective brightness considering alpha channel.
|
|
742
|
-
* For transparent colors, we assume they will be blended over a white
|
|
743
|
-
* background.
|
|
744
|
-
*
|
|
745
|
-
* @param color The color to analyze
|
|
746
|
-
* @return true if the effective color appears light, false if dark
|
|
747
|
-
*/
|
|
748
622
|
private boolean isEffectiveLightColor(@ColorInt int color) {
|
|
749
623
|
int alpha = Color.alpha(color);
|
|
750
624
|
|
|
751
625
|
if (alpha == 255) {
|
|
752
|
-
// Fully opaque - use standard luminance calculation
|
|
753
626
|
return ColorUtils.calculateLuminance(color) > 0.5;
|
|
754
627
|
}
|
|
755
628
|
|
|
756
|
-
// For transparent colors, calculate effective color when blended over white
|
|
757
|
-
// background
|
|
758
629
|
float alphaRatio = alpha / 255.0f;
|
|
759
630
|
int r = Color.red(color);
|
|
760
631
|
int g = Color.green(color);
|
|
761
632
|
int b = Color.blue(color);
|
|
762
633
|
|
|
763
|
-
// Blend with white background (255, 255, 255)
|
|
764
634
|
int effectiveR = (int) (r * alphaRatio + 255 * (1 - alphaRatio));
|
|
765
635
|
int effectiveG = (int) (g * alphaRatio + 255 * (1 - alphaRatio));
|
|
766
636
|
int effectiveB = (int) (b * alphaRatio + 255 * (1 - alphaRatio));
|
|
767
637
|
|
|
768
|
-
|
|
769
|
-
return ColorUtils.calculateLuminance(effectiveColor) > 0.5;
|
|
638
|
+
return ColorUtils.calculateLuminance(Color.rgb(effectiveR, effectiveG, effectiveB)) > 0.5;
|
|
770
639
|
}
|
|
771
640
|
|
|
772
|
-
/**
|
|
773
|
-
* Apply default status bar style based on system theme.
|
|
774
|
-
* Automatically detects if the device is in light or dark mode and applies the
|
|
775
|
-
* appropriate style.
|
|
776
|
-
*
|
|
777
|
-
* @param activity The activity to apply the style to
|
|
778
|
-
*/
|
|
779
641
|
public void applyDefaultStyle(Activity activity) {
|
|
780
642
|
boolean isDarkMode = isSystemInDarkMode(activity);
|
|
781
|
-
|
|
782
|
-
Log.d(TAG, "applyDefaultStyle: detected system theme=" + (isDarkMode ? "dark" : "light") + ", applying style="
|
|
783
|
-
+ style);
|
|
784
|
-
setStyle(activity, style, null);
|
|
643
|
+
setStyle(activity, isDarkMode ? "DARK" : "LIGHT", null);
|
|
785
644
|
}
|
|
786
645
|
|
|
787
|
-
/**
|
|
788
|
-
* Check if the system is currently in dark mode.
|
|
789
|
-
*
|
|
790
|
-
* @param activity The activity to check the configuration from
|
|
791
|
-
* @return true if system is in dark mode, false otherwise
|
|
792
|
-
*/
|
|
793
646
|
private boolean isSystemInDarkMode(Activity activity) {
|
|
794
647
|
int nightModeFlags = activity.getResources().getConfiguration().uiMode
|
|
795
648
|
& android.content.res.Configuration.UI_MODE_NIGHT_MASK;
|
|
796
|
-
|
|
797
|
-
Log.d(TAG, "isSystemInDarkMode: " + isDarkMode);
|
|
798
|
-
return isDarkMode;
|
|
649
|
+
return nightModeFlags == android.content.res.Configuration.UI_MODE_NIGHT_YES;
|
|
799
650
|
}
|
|
800
651
|
}
|