capacitor-plugin-status-bar 2.0.0
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/CapacitorPluginStatusBar.podspec +17 -0
- package/LICENSE +21 -0
- package/Package.swift +28 -0
- package/README.md +221 -0
- package/android/build.gradle +58 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/cap/plugins/statusbar/StatusBar.java +691 -0
- package/android/src/main/java/com/cap/plugins/statusbar/StatusBarPlugin.java +120 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +332 -0
- package/dist/esm/definitions.d.ts +97 -0
- package/dist/esm/definitions.js +13 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +10 -0
- package/dist/esm/web.js +35 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +62 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +65 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/StatusBarPlugin/StatusBar.swift +325 -0
- package/ios/Sources/StatusBarPlugin/StatusBarPlugin.swift +81 -0
- package/ios/Tests/StatusBarPluginTests/StatusBarPluginTests.swift +15 -0
- package/package.json +99 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
package com.cap.plugins.statusbar;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.graphics.Color;
|
|
5
|
+
import android.os.Build;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
import android.view.View;
|
|
8
|
+
import android.view.ViewGroup;
|
|
9
|
+
import android.view.Window;
|
|
10
|
+
import android.view.WindowInsets;
|
|
11
|
+
import android.view.WindowInsetsController;
|
|
12
|
+
import android.widget.FrameLayout;
|
|
13
|
+
import android.view.Gravity;
|
|
14
|
+
|
|
15
|
+
import androidx.annotation.ColorInt;
|
|
16
|
+
import androidx.annotation.Nullable;
|
|
17
|
+
import androidx.core.graphics.ColorUtils;
|
|
18
|
+
import androidx.core.view.ViewCompat;
|
|
19
|
+
import androidx.core.view.WindowCompat;
|
|
20
|
+
import androidx.core.view.WindowInsetsCompat;
|
|
21
|
+
|
|
22
|
+
import com.getcapacitor.Plugin;
|
|
23
|
+
|
|
24
|
+
import java.util.Objects;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Android Status Bar utilities with Android 10-15+ (API 29-35+) support.
|
|
28
|
+
* Supports:
|
|
29
|
+
* - API 29 (Android 10): Uses deprecated SYSTEM_UI_FLAG for backward
|
|
30
|
+
* compatibility
|
|
31
|
+
* - API 30+ (Android 11+): Uses modern WindowInsetsController API
|
|
32
|
+
* - API 35+ (Android 15+): Fully compatible with edge-to-edge display
|
|
33
|
+
* enforcement
|
|
34
|
+
*/
|
|
35
|
+
public class StatusBar extends Plugin {
|
|
36
|
+
private static final String TAG = "StatusBar";
|
|
37
|
+
private static final String STATUS_BAR_OVERLAY_TAG = "capacitor_status_bar_overlay";
|
|
38
|
+
private static final String NAV_BAR_OVERLAY_TAG = "capacitor_navigation_bar_overlay";
|
|
39
|
+
|
|
40
|
+
// Store current state to preserve colors when hiding/showing
|
|
41
|
+
private String currentStyle = "LIGHT";
|
|
42
|
+
private String currentColorHex = null;
|
|
43
|
+
private int currentStatusBarColor = Color.BLACK;
|
|
44
|
+
private int currentNavBarColor = Color.BLACK;
|
|
45
|
+
|
|
46
|
+
@Override
|
|
47
|
+
public void load() {
|
|
48
|
+
super.load();
|
|
49
|
+
setupEdgeToEdgeBehavior();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private void setupEdgeToEdgeBehavior() {
|
|
53
|
+
Activity activity = getActivity();
|
|
54
|
+
if (activity == null)
|
|
55
|
+
return;
|
|
56
|
+
|
|
57
|
+
Window window = activity.getWindow();
|
|
58
|
+
View decorView = window.getDecorView();
|
|
59
|
+
|
|
60
|
+
WindowCompat.setDecorFitsSystemWindows(window, false);
|
|
61
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 14+
|
|
62
|
+
ViewCompat.setOnApplyWindowInsetsListener(decorView, (v, insets) -> {
|
|
63
|
+
ViewCompat.onApplyWindowInsets(v, insets);
|
|
64
|
+
return insets;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Ensures edge-to-edge is properly configured for Android 15+.
|
|
71
|
+
* This fixes the keyboard extra space issue by properly handling IME insets
|
|
72
|
+
* using the modern WindowInsets API instead of deprecated soft input modes.
|
|
73
|
+
*
|
|
74
|
+
* @param activity The activity to configure
|
|
75
|
+
*/
|
|
76
|
+
public void ensureEdgeToEdgeConfigured(Activity activity) {
|
|
77
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { // Android 15 (API 35)
|
|
78
|
+
Window window = activity.getWindow();
|
|
79
|
+
View decorView = window.getDecorView();
|
|
80
|
+
|
|
81
|
+
// Enable edge-to-edge mode for Android 15+
|
|
82
|
+
WindowCompat.setDecorFitsSystemWindows(window, false);
|
|
83
|
+
|
|
84
|
+
ViewCompat.setOnApplyWindowInsetsListener(decorView, (v, insets) -> {
|
|
85
|
+
androidx.core.graphics.Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
|
|
86
|
+
androidx.core.graphics.Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
|
87
|
+
|
|
88
|
+
boolean isKeyboardVisible = imeInsets.bottom > 0;
|
|
89
|
+
Log.d(TAG, "ensureEdgeToEdgeConfigured: IME visible=" + isKeyboardVisible
|
|
90
|
+
+ ", IME bottom=" + imeInsets.bottom
|
|
91
|
+
+ ", system bars bottom=" + systemBarsInsets.bottom);
|
|
92
|
+
|
|
93
|
+
ViewCompat.onApplyWindowInsets(v, insets);
|
|
94
|
+
return insets;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
Log.d(TAG,
|
|
98
|
+
"ensureEdgeToEdgeConfigured: Edge-to-edge enabled with WindowInsets API for Android 15+ (API 35+)");
|
|
99
|
+
} else {
|
|
100
|
+
Log.d(TAG, "ensureEdgeToEdgeConfigured: Android < 15, no action needed");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public void setOverlaysWebView(Activity activity, boolean overlay) {
|
|
105
|
+
// No-op on Android. Exposed for API parity with iOS.
|
|
106
|
+
Log.d(TAG, "setOverlaysWebView: no-op on Android, overlay=" + overlay);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public void showStatusBar(Activity activity, boolean animated) {
|
|
110
|
+
Log.d(TAG, "showStatusBar: animated=" + animated + ", currentStyle=" + currentStyle + ", API="
|
|
111
|
+
+ Build.VERSION.SDK_INT);
|
|
112
|
+
Window window = activity.getWindow();
|
|
113
|
+
View decorView = window.getDecorView();
|
|
114
|
+
|
|
115
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
116
|
+
// API 30+ (Android 11+) - Use WindowInsetsController
|
|
117
|
+
WindowInsetsController controller = window.getInsetsController();
|
|
118
|
+
if (controller != null) {
|
|
119
|
+
Log.d(TAG, "showStatusBar: showing system bars (API 30+)");
|
|
120
|
+
// Show both status and navigation bars together
|
|
121
|
+
controller.show(WindowInsets.Type.systemBars());
|
|
122
|
+
// Set behavior for transient bars (user can swipe to reveal)
|
|
123
|
+
controller.setSystemBarsBehavior(
|
|
124
|
+
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
125
|
+
} else {
|
|
126
|
+
Log.w(TAG, "showStatusBar: WindowInsetsController is null");
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// API 29 (Android 10) - Use system UI visibility flags (deprecated but
|
|
130
|
+
// necessary)
|
|
131
|
+
Log.d(TAG, "showStatusBar: showing using system UI flags (API 29)");
|
|
132
|
+
// Set to visible state - clear all immersive flags
|
|
133
|
+
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Reapply the stored colors and style instead of removing them
|
|
137
|
+
reapplyCurrentStyle(activity);
|
|
138
|
+
|
|
139
|
+
// Restore the overlay backgrounds to their original colors
|
|
140
|
+
restoreStatusBarBackground(activity);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public void hideStatusBar(Activity activity, String animation) {
|
|
144
|
+
Log.d(TAG, "hideStatusBar: animation=" + animation + ", API=" + Build.VERSION.SDK_INT);
|
|
145
|
+
Window window = activity.getWindow();
|
|
146
|
+
View decorView = window.getDecorView();
|
|
147
|
+
|
|
148
|
+
String animationType = animation != null ? animation.toLowerCase() : "slide";
|
|
149
|
+
|
|
150
|
+
if ("fade".equals(animationType)) {
|
|
151
|
+
// Fade mode: Make background transparent without removing status bar and
|
|
152
|
+
// navigation bar
|
|
153
|
+
Log.d(TAG, "hideStatusBar: fade mode - making backgrounds transparent");
|
|
154
|
+
makeStatusBarBackgroundTransparent(activity);
|
|
155
|
+
} else if ("slide".equals(animationType)) {
|
|
156
|
+
// Slide mode: Hide status bar and navigation bar completely (current behavior)
|
|
157
|
+
Log.d(TAG, "hideStatusBar: slide mode - hiding bars completely");
|
|
158
|
+
|
|
159
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
160
|
+
// API 30+ (Android 11+) - Use WindowInsetsController
|
|
161
|
+
WindowInsetsController controller = window.getInsetsController();
|
|
162
|
+
if (controller != null) {
|
|
163
|
+
Log.d(TAG, "hideStatusBar: hiding system bars (API 30+)");
|
|
164
|
+
// Hide both status and navigation bars together
|
|
165
|
+
controller.hide(WindowInsets.Type.systemBars());
|
|
166
|
+
// Set behavior for immersive mode (user can swipe to reveal temporarily)
|
|
167
|
+
controller.setSystemBarsBehavior(
|
|
168
|
+
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
169
|
+
} else {
|
|
170
|
+
Log.w(TAG, "hideStatusBar: WindowInsetsController is null");
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// API 29 (Android 10) - Use system UI visibility flags (deprecated but
|
|
174
|
+
// necessary)
|
|
175
|
+
Log.d(TAG, "hideStatusBar: hiding using system UI flags (API 29)");
|
|
176
|
+
// Use immersive sticky mode with proper layout flags for Android 10
|
|
177
|
+
decorView.setSystemUiVisibility(
|
|
178
|
+
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
179
|
+
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
180
|
+
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
181
|
+
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
182
|
+
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
183
|
+
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Make the overlay backgrounds transparent so content shows through
|
|
187
|
+
makeStatusBarBackgroundTransparent(activity);
|
|
188
|
+
} else {
|
|
189
|
+
// Unknown animation type, default to slide
|
|
190
|
+
Log.w(TAG, "hideStatusBar: unknown animation type '" + animationType + "', defaulting to slide");
|
|
191
|
+
hideStatusBar(activity, "slide");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public void setStyle(Activity activity, String style, @Nullable String colorHex) {
|
|
196
|
+
Log.d(TAG, "setStyle: style=" + style + ", colorHex=" + colorHex);
|
|
197
|
+
Window window = activity.getWindow();
|
|
198
|
+
|
|
199
|
+
// Enable drawing of system bar backgrounds (required for color changes)
|
|
200
|
+
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
|
201
|
+
window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
|
202
|
+
window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
|
203
|
+
|
|
204
|
+
// Store the current style and color for later reapplication
|
|
205
|
+
currentStyle = style;
|
|
206
|
+
currentColorHex = colorHex;
|
|
207
|
+
|
|
208
|
+
// Set icon appearance (light/dark) regardless of background approach
|
|
209
|
+
boolean lightBackground;
|
|
210
|
+
if ("LIGHT".equalsIgnoreCase(style)) {
|
|
211
|
+
// Light background -> dark icons
|
|
212
|
+
setLightStatusBarIcons(window, true);
|
|
213
|
+
lightBackground = true;
|
|
214
|
+
} else if ("DARK".equalsIgnoreCase(style)) {
|
|
215
|
+
// Dark background -> light icons
|
|
216
|
+
setLightStatusBarIcons(window, false);
|
|
217
|
+
lightBackground = false;
|
|
218
|
+
} else if ("CUSTOM".equalsIgnoreCase(style)) {
|
|
219
|
+
// CUSTOM: Derive icon color from provided custom color
|
|
220
|
+
int parsed = parseColorOrDefault(colorHex, Color.BLACK);
|
|
221
|
+
boolean isLight = isEffectiveLightColor(parsed);
|
|
222
|
+
// If background is light, request dark icons
|
|
223
|
+
setLightStatusBarIcons(window, isLight);
|
|
224
|
+
lightBackground = isLight;
|
|
225
|
+
} else {
|
|
226
|
+
// Default: Auto-detect based on system theme (follow device theme)
|
|
227
|
+
boolean isSystemDarkMode = isSystemInDarkMode(activity);
|
|
228
|
+
setLightStatusBarIcons(window, !isSystemDarkMode);
|
|
229
|
+
lightBackground = !isSystemDarkMode;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if ("CUSTOM".equalsIgnoreCase(style) && colorHex != null) {
|
|
233
|
+
int color = parseColorOrDefault(colorHex, lightBackground ? Color.WHITE : Color.BLACK);
|
|
234
|
+
currentStatusBarColor = color;
|
|
235
|
+
currentNavBarColor = color;
|
|
236
|
+
applyStatusBarBackground(activity, color);
|
|
237
|
+
applyNavigationBarBackground(activity, color);
|
|
238
|
+
} else if ("LIGHT".equalsIgnoreCase(style)) {
|
|
239
|
+
currentStatusBarColor = Color.WHITE;
|
|
240
|
+
currentNavBarColor = Color.WHITE;
|
|
241
|
+
applyStatusBarBackground(activity, Color.WHITE);
|
|
242
|
+
applyNavigationBarBackground(activity, Color.WHITE);
|
|
243
|
+
} else if ("DARK".equalsIgnoreCase(style)) {
|
|
244
|
+
currentStatusBarColor = Color.BLACK;
|
|
245
|
+
currentNavBarColor = Color.BLACK;
|
|
246
|
+
applyStatusBarBackground(activity, Color.BLACK);
|
|
247
|
+
applyNavigationBarBackground(activity, Color.BLACK);
|
|
248
|
+
} else {
|
|
249
|
+
// Default: Auto-detect based on system theme
|
|
250
|
+
boolean isSystemDarkMode = isSystemInDarkMode(activity);
|
|
251
|
+
int themeColor = isSystemDarkMode ? Color.BLACK : Color.WHITE;
|
|
252
|
+
currentStatusBarColor = themeColor;
|
|
253
|
+
currentNavBarColor = themeColor;
|
|
254
|
+
applyStatusBarBackground(activity, themeColor);
|
|
255
|
+
applyNavigationBarBackground(activity, themeColor);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Set the window background color.
|
|
261
|
+
*
|
|
262
|
+
* @param activity The activity to apply the background color to
|
|
263
|
+
* @param colorHex The hex color string (e.g., "#FFFFFF" or "#FF5733")
|
|
264
|
+
*/
|
|
265
|
+
public void setBackground(Activity activity, @Nullable String colorHex) {
|
|
266
|
+
Log.d(TAG, "setBackground: colorHex=" + colorHex);
|
|
267
|
+
|
|
268
|
+
if (colorHex == null) {
|
|
269
|
+
Log.w(TAG, "setBackground: colorHex is null");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
int color = parseColorOrDefault(colorHex, Color.WHITE);
|
|
274
|
+
applyWindowBackground(activity, color);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get the safe area insets.
|
|
279
|
+
* Returns the insets for status bar, navigation bar, and notch areas.
|
|
280
|
+
*
|
|
281
|
+
* @param activity The activity to get the insets from
|
|
282
|
+
* @return A map containing top, bottom, left, and right inset values in pixels
|
|
283
|
+
*/
|
|
284
|
+
public java.util.Map<String, Integer> getSafeAreaInsets(Activity activity) {
|
|
285
|
+
Log.d(TAG, "getSafeAreaInsets");
|
|
286
|
+
java.util.Map<String, Integer> insets = new java.util.HashMap<>();
|
|
287
|
+
|
|
288
|
+
Window window = activity.getWindow();
|
|
289
|
+
View decorView = window.getDecorView();
|
|
290
|
+
|
|
291
|
+
WindowInsets windowInsets = decorView.getRootWindowInsets();
|
|
292
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
293
|
+
// API 30+ (Android 11+) - Use WindowInsets API
|
|
294
|
+
if (windowInsets != null) {
|
|
295
|
+
android.graphics.Insets systemBarsInsets = windowInsets.getInsets(WindowInsets.Type.systemBars());
|
|
296
|
+
android.graphics.Insets displayCutoutInsets = windowInsets.getInsets(WindowInsets.Type.displayCutout());
|
|
297
|
+
|
|
298
|
+
// Combine system bars and display cutout insets (use maximum of both)
|
|
299
|
+
insets.put("top", Math.max(systemBarsInsets.top, displayCutoutInsets.top));
|
|
300
|
+
insets.put("bottom", Math.max(systemBarsInsets.bottom, displayCutoutInsets.bottom));
|
|
301
|
+
insets.put("left", Math.max(systemBarsInsets.left, displayCutoutInsets.left));
|
|
302
|
+
insets.put("right", Math.max(systemBarsInsets.right, displayCutoutInsets.right));
|
|
303
|
+
|
|
304
|
+
Log.d(TAG, "getSafeAreaInsets (API 30+): top=" + insets.get("top")
|
|
305
|
+
+ ", bottom=" + insets.get("bottom")
|
|
306
|
+
+ ", left=" + insets.get("left")
|
|
307
|
+
+ ", right=" + insets.get("right"));
|
|
308
|
+
} else {
|
|
309
|
+
// Fallback to zero insets
|
|
310
|
+
insets.put("top", 0);
|
|
311
|
+
insets.put("bottom", 0);
|
|
312
|
+
insets.put("left", 0);
|
|
313
|
+
insets.put("right", 0);
|
|
314
|
+
Log.w(TAG, "getSafeAreaInsets: windowInsets is null");
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
// API 29 (Android 10) - Use deprecated system window insets
|
|
318
|
+
if (windowInsets != null) {
|
|
319
|
+
insets.put("top", windowInsets.getSystemWindowInsetTop());
|
|
320
|
+
insets.put("bottom", windowInsets.getSystemWindowInsetBottom());
|
|
321
|
+
insets.put("left", windowInsets.getSystemWindowInsetLeft());
|
|
322
|
+
insets.put("right", windowInsets.getSystemWindowInsetRight());
|
|
323
|
+
|
|
324
|
+
Log.d(TAG, "getSafeAreaInsets (API 29): top=" + insets.get("top")
|
|
325
|
+
+ ", bottom=" + insets.get("bottom")
|
|
326
|
+
+ ", left=" + insets.get("left")
|
|
327
|
+
+ ", right=" + insets.get("right"));
|
|
328
|
+
} else {
|
|
329
|
+
// Fallback to zero insets
|
|
330
|
+
insets.put("top", 0);
|
|
331
|
+
insets.put("bottom", 0);
|
|
332
|
+
insets.put("left", 0);
|
|
333
|
+
insets.put("right", 0);
|
|
334
|
+
Log.w(TAG, "getSafeAreaInsets: windowInsets is null");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return insets;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Reapply the current style and colors after showing bars.
|
|
343
|
+
* This ensures colors are preserved when hiding and then showing.
|
|
344
|
+
*/
|
|
345
|
+
private void reapplyCurrentStyle(Activity activity) {
|
|
346
|
+
Log.d(TAG, "reapplyCurrentStyle: style=" + currentStyle + ", colorHex=" + currentColorHex);
|
|
347
|
+
Window window = activity.getWindow();
|
|
348
|
+
|
|
349
|
+
// Reapply icon appearance
|
|
350
|
+
if ("LIGHT".equalsIgnoreCase(currentStyle)) {
|
|
351
|
+
setLightStatusBarIcons(window, true);
|
|
352
|
+
} else if ("DARK".equalsIgnoreCase(currentStyle)) {
|
|
353
|
+
setLightStatusBarIcons(window, false);
|
|
354
|
+
} else if ("CUSTOM".equalsIgnoreCase(currentStyle)) {
|
|
355
|
+
int parsed = parseColorOrDefault(currentColorHex, Color.BLACK);
|
|
356
|
+
boolean isLight = isEffectiveLightColor(parsed);
|
|
357
|
+
setLightStatusBarIcons(window, isLight);
|
|
358
|
+
} else {
|
|
359
|
+
// Default: Auto-detect based on system theme
|
|
360
|
+
boolean isSystemDarkMode = isSystemInDarkMode(activity);
|
|
361
|
+
setLightStatusBarIcons(window, !isSystemDarkMode);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Reapply colors
|
|
365
|
+
applyStatusBarBackground(activity, currentStatusBarColor);
|
|
366
|
+
applyNavigationBarBackground(activity, currentNavBarColor);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private void setLightStatusBarIcons(Window window, boolean light) {
|
|
370
|
+
Log.d(TAG, "setLightStatusBarIcons: light=" + light + ", API=" + Build.VERSION.SDK_INT);
|
|
371
|
+
View decorView = window.getDecorView();
|
|
372
|
+
|
|
373
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
374
|
+
// API 30+ - Use WindowInsetsController
|
|
375
|
+
WindowInsetsController controller = window.getInsetsController();
|
|
376
|
+
if (controller == null) {
|
|
377
|
+
Log.w(TAG, "setLightStatusBarIcons: WindowInsetsController is null");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
int mask = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
|
381
|
+
| WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
|
|
382
|
+
controller.setSystemBarsAppearance(light ? mask : 0, mask);
|
|
383
|
+
Log.d(TAG, "setLightStatusBarIcons: applied using WindowInsetsController (API 30+)");
|
|
384
|
+
} else {
|
|
385
|
+
int flags = decorView.getSystemUiVisibility();
|
|
386
|
+
if (light) {
|
|
387
|
+
// Light background -> dark icons
|
|
388
|
+
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
|
|
389
|
+
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
|
390
|
+
Log.d(TAG, "setLightStatusBarIcons: set light icons (dark text) (API 29)");
|
|
391
|
+
} else {
|
|
392
|
+
// Dark background -> light icons
|
|
393
|
+
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
|
|
394
|
+
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
|
395
|
+
Log.d(TAG, "setLightStatusBarIcons: set dark icons (light text) (API 29)");
|
|
396
|
+
}
|
|
397
|
+
decorView.setSystemUiVisibility(flags);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private void applyStatusBarBackground(Activity activity, @ColorInt int color) {
|
|
402
|
+
Log.d(TAG, "applyStatusBarBackground: color=#" + Integer.toHexString(color) + ", API=" + Build.VERSION.SDK_INT);
|
|
403
|
+
Window window = activity.getWindow();
|
|
404
|
+
if (Build.VERSION.SDK_INT >= 35) {
|
|
405
|
+
ensureStatusBarOverlay(activity, color);
|
|
406
|
+
} else {
|
|
407
|
+
removeStatusBarOverlayIfPresent(activity);
|
|
408
|
+
window.setStatusBarColor(color);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private void applyNavigationBarBackground(Activity activity, @ColorInt int color) {
|
|
413
|
+
Log.d(TAG, "applyNavigationBarBackground: color=#" + Integer.toHexString(color) + ", API="
|
|
414
|
+
+ Build.VERSION.SDK_INT);
|
|
415
|
+
Window window = activity.getWindow();
|
|
416
|
+
if (Build.VERSION.SDK_INT >= 35) {
|
|
417
|
+
ensureNavBarOverlay(activity, color);
|
|
418
|
+
} else {
|
|
419
|
+
removeNavBarOverlayIfPresent(activity);
|
|
420
|
+
window.setNavigationBarColor(color);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private void ensureStatusBarOverlay(Activity activity, @ColorInt int color) {
|
|
425
|
+
Log.d(TAG, "ensureStatusBarOverlay: color=#" + Integer.toHexString(color));
|
|
426
|
+
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
427
|
+
View existing = decorView.findViewWithTag(STATUS_BAR_OVERLAY_TAG);
|
|
428
|
+
if (existing == null) {
|
|
429
|
+
Log.d(TAG, "ensureStatusBarOverlay: creating new overlay");
|
|
430
|
+
View overlay = new View(activity);
|
|
431
|
+
overlay.setTag(STATUS_BAR_OVERLAY_TAG);
|
|
432
|
+
overlay.setBackgroundColor(color);
|
|
433
|
+
|
|
434
|
+
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
|
|
435
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
436
|
+
0);
|
|
437
|
+
lp.topMargin = 0;
|
|
438
|
+
overlay.setLayoutParams(lp);
|
|
439
|
+
|
|
440
|
+
// Add to the top of the decor view
|
|
441
|
+
decorView.addView(overlay);
|
|
442
|
+
|
|
443
|
+
// Apply correct height from insets
|
|
444
|
+
ViewCompat.setOnApplyWindowInsetsListener(overlay, (v, windowInsets) -> {
|
|
445
|
+
int top;
|
|
446
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
447
|
+
top = Objects.requireNonNull(windowInsets.toWindowInsets())
|
|
448
|
+
.getInsets(WindowInsets.Type.statusBars()).top;
|
|
449
|
+
} else {
|
|
450
|
+
top = Objects.requireNonNull(windowInsets.toWindowInsets()).getSystemWindowInsetTop();
|
|
451
|
+
}
|
|
452
|
+
ViewGroup.LayoutParams params = v.getLayoutParams();
|
|
453
|
+
params.height = top;
|
|
454
|
+
v.setLayoutParams(params);
|
|
455
|
+
// Don't set color here - it's set before listener and should not be overridden
|
|
456
|
+
return windowInsets;
|
|
457
|
+
});
|
|
458
|
+
overlay.requestApplyInsets();
|
|
459
|
+
} else {
|
|
460
|
+
Log.d(TAG, "ensureStatusBarOverlay: updating existing overlay");
|
|
461
|
+
existing.setBackgroundColor(color);
|
|
462
|
+
existing.requestApplyInsets();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private void removeStatusBarOverlayIfPresent(Activity activity) {
|
|
467
|
+
Log.d(TAG, "removeStatusBarOverlayIfPresent");
|
|
468
|
+
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
469
|
+
View existing = decorView.findViewWithTag(STATUS_BAR_OVERLAY_TAG);
|
|
470
|
+
if (existing != null) {
|
|
471
|
+
Log.d(TAG, "removeStatusBarOverlayIfPresent: removing overlay");
|
|
472
|
+
decorView.removeView(existing);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private void ensureNavBarOverlay(Activity activity, @ColorInt int color) {
|
|
477
|
+
Log.d(TAG, "ensureNavBarOverlay: color=#" + Integer.toHexString(color));
|
|
478
|
+
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
479
|
+
View existing = decorView.findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
480
|
+
if (existing == null) {
|
|
481
|
+
Log.d(TAG, "ensureNavBarOverlay: creating new overlay");
|
|
482
|
+
View overlay = new View(activity);
|
|
483
|
+
overlay.setTag(NAV_BAR_OVERLAY_TAG);
|
|
484
|
+
overlay.setBackgroundColor(color);
|
|
485
|
+
|
|
486
|
+
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
|
|
487
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
488
|
+
0);
|
|
489
|
+
lp.gravity = Gravity.BOTTOM;
|
|
490
|
+
overlay.setLayoutParams(lp);
|
|
491
|
+
|
|
492
|
+
decorView.addView(overlay);
|
|
493
|
+
|
|
494
|
+
ViewCompat.setOnApplyWindowInsetsListener(overlay, (v, windowInsets) -> {
|
|
495
|
+
int bottom;
|
|
496
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
497
|
+
bottom = Objects.requireNonNull(windowInsets.toWindowInsets())
|
|
498
|
+
.getInsets(WindowInsets.Type.navigationBars()).bottom;
|
|
499
|
+
} else {
|
|
500
|
+
bottom = Objects.requireNonNull(windowInsets.toWindowInsets()).getSystemWindowInsetBottom();
|
|
501
|
+
}
|
|
502
|
+
ViewGroup.LayoutParams params = v.getLayoutParams();
|
|
503
|
+
params.height = bottom;
|
|
504
|
+
v.setLayoutParams(params);
|
|
505
|
+
// Don't set color here - it's set before listener and should not be overridden
|
|
506
|
+
return windowInsets;
|
|
507
|
+
});
|
|
508
|
+
overlay.requestApplyInsets();
|
|
509
|
+
} else {
|
|
510
|
+
Log.d(TAG, "ensureNavBarOverlay: updating existing overlay");
|
|
511
|
+
existing.setBackgroundColor(color);
|
|
512
|
+
existing.requestApplyInsets();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private void removeNavBarOverlayIfPresent(Activity activity) {
|
|
517
|
+
Log.d(TAG, "removeNavBarOverlayIfPresent");
|
|
518
|
+
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
519
|
+
View existing = decorView.findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
520
|
+
if (existing != null) {
|
|
521
|
+
Log.d(TAG, "removeNavBarOverlayIfPresent: removing overlay");
|
|
522
|
+
decorView.removeView(existing);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private void applyWindowBackground(Activity activity, @ColorInt int color) {
|
|
527
|
+
Log.d(TAG, "applyWindowBackground: color=#" + Integer.toHexString(color));
|
|
528
|
+
View decorView = activity.getWindow().getDecorView();
|
|
529
|
+
decorView.setBackgroundColor(color);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Makes the status bar and navigation bar backgrounds transparent.
|
|
534
|
+
* This allows content to show through when the bars are hidden.
|
|
535
|
+
*/
|
|
536
|
+
private void makeStatusBarBackgroundTransparent(Activity activity) {
|
|
537
|
+
Log.d(TAG, "makeStatusBarBackgroundTransparent: API=" + Build.VERSION.SDK_INT);
|
|
538
|
+
Window window = activity.getWindow();
|
|
539
|
+
|
|
540
|
+
if (Build.VERSION.SDK_INT >= 35) {
|
|
541
|
+
// API 35+ (Android 15+) - Make overlay views transparent
|
|
542
|
+
ViewGroup decorView = (ViewGroup) window.getDecorView();
|
|
543
|
+
View statusBarOverlay = decorView.findViewWithTag(STATUS_BAR_OVERLAY_TAG);
|
|
544
|
+
View navBarOverlay = decorView.findViewWithTag(NAV_BAR_OVERLAY_TAG);
|
|
545
|
+
|
|
546
|
+
if (statusBarOverlay != null) {
|
|
547
|
+
statusBarOverlay.setBackgroundColor(Color.TRANSPARENT);
|
|
548
|
+
Log.d(TAG, "makeStatusBarBackgroundTransparent: status bar overlay made transparent");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (navBarOverlay != null) {
|
|
552
|
+
navBarOverlay.setBackgroundColor(Color.TRANSPARENT);
|
|
553
|
+
Log.d(TAG, "makeStatusBarBackgroundTransparent: navigation bar overlay made transparent");
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
// API 29-34 - Make window bars transparent
|
|
557
|
+
window.setStatusBarColor(Color.TRANSPARENT);
|
|
558
|
+
window.setNavigationBarColor(Color.TRANSPARENT);
|
|
559
|
+
Log.d(TAG, "makeStatusBarBackgroundTransparent: window bars made transparent");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Restores the status bar and navigation bar backgrounds to their stored
|
|
565
|
+
* colors.
|
|
566
|
+
* Called when showing the bars after they were hidden.
|
|
567
|
+
*/
|
|
568
|
+
private void restoreStatusBarBackground(Activity activity) {
|
|
569
|
+
Log.d(TAG, "restoreStatusBarBackground: API=" + Build.VERSION.SDK_INT
|
|
570
|
+
+ ", currentStatusBarColor=#" + Integer.toHexString(currentStatusBarColor)
|
|
571
|
+
+ ", currentNavBarColor=#" + Integer.toHexString(currentNavBarColor));
|
|
572
|
+
|
|
573
|
+
// Restore all backgrounds to their stored colors
|
|
574
|
+
applyStatusBarBackground(activity, currentStatusBarColor);
|
|
575
|
+
applyNavigationBarBackground(activity, currentNavBarColor);
|
|
576
|
+
|
|
577
|
+
Log.d(TAG, "restoreStatusBarBackground: backgrounds restored");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
@ColorInt
|
|
581
|
+
private int parseColorOrDefault(@Nullable String color, @ColorInt int def) {
|
|
582
|
+
if (color == null) {
|
|
583
|
+
Log.d(TAG, "parseColorOrDefault: color is null, using default");
|
|
584
|
+
return def;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
int parsed = parseHexColor(color);
|
|
588
|
+
Log.d(TAG, "parseColorOrDefault: parsed color=" + color + " -> #" + Integer.toHexString(parsed));
|
|
589
|
+
return parsed;
|
|
590
|
+
} catch (IllegalArgumentException ex) {
|
|
591
|
+
Log.w(TAG, "parseColorOrDefault: invalid color=" + color + ", using default");
|
|
592
|
+
return def;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Parse hex color string similar to iOS implementation.
|
|
598
|
+
* Handles both 6-digit (#RRGGBB) and 8-digit (#RRGGBBAA) formats.
|
|
599
|
+
*
|
|
600
|
+
* @param hex The hex color string (with or without # prefix)
|
|
601
|
+
* @return The parsed color as an integer
|
|
602
|
+
* @throws IllegalArgumentException if the color format is invalid
|
|
603
|
+
*/
|
|
604
|
+
private int parseHexColor(String hex) throws IllegalArgumentException {
|
|
605
|
+
String hexSanitized = hex.trim().replaceFirst("^#", "");
|
|
606
|
+
|
|
607
|
+
if (hexSanitized.length() != 6 && hexSanitized.length() != 8) {
|
|
608
|
+
throw new IllegalArgumentException("Invalid hex color length: " + hexSanitized.length());
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
long rgb = Long.parseLong(hexSanitized, 16);
|
|
613
|
+
|
|
614
|
+
if (hexSanitized.length() == 6) {
|
|
615
|
+
// 6-digit format: #RRGGBB (opaque)
|
|
616
|
+
return (int) (0xFF000000L | rgb);
|
|
617
|
+
} else {
|
|
618
|
+
// 8-digit format: #RRGGBBAA
|
|
619
|
+
int r = (int) ((rgb & 0xFF000000L) >> 24);
|
|
620
|
+
int g = (int) ((rgb & 0x00FF0000L) >> 16);
|
|
621
|
+
int b = (int) ((rgb & 0x0000FF00L) >> 8);
|
|
622
|
+
int a = (int) (rgb & 0x000000FFL);
|
|
623
|
+
|
|
624
|
+
return Color.argb(a, r, g, b);
|
|
625
|
+
}
|
|
626
|
+
} catch (NumberFormatException e) {
|
|
627
|
+
throw new IllegalArgumentException("Invalid hex color format: " + hex, e);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Calculate effective brightness considering alpha channel.
|
|
633
|
+
* For transparent colors, we assume they will be blended over a white
|
|
634
|
+
* background.
|
|
635
|
+
*
|
|
636
|
+
* @param color The color to analyze
|
|
637
|
+
* @return true if the effective color appears light, false if dark
|
|
638
|
+
*/
|
|
639
|
+
private boolean isEffectiveLightColor(@ColorInt int color) {
|
|
640
|
+
int alpha = Color.alpha(color);
|
|
641
|
+
|
|
642
|
+
if (alpha == 255) {
|
|
643
|
+
// Fully opaque - use standard luminance calculation
|
|
644
|
+
return ColorUtils.calculateLuminance(color) > 0.5;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// For transparent colors, calculate effective color when blended over white
|
|
648
|
+
// background
|
|
649
|
+
float alphaRatio = alpha / 255.0f;
|
|
650
|
+
int r = Color.red(color);
|
|
651
|
+
int g = Color.green(color);
|
|
652
|
+
int b = Color.blue(color);
|
|
653
|
+
|
|
654
|
+
// Blend with white background (255, 255, 255)
|
|
655
|
+
int effectiveR = (int) (r * alphaRatio + 255 * (1 - alphaRatio));
|
|
656
|
+
int effectiveG = (int) (g * alphaRatio + 255 * (1 - alphaRatio));
|
|
657
|
+
int effectiveB = (int) (b * alphaRatio + 255 * (1 - alphaRatio));
|
|
658
|
+
|
|
659
|
+
int effectiveColor = Color.rgb(effectiveR, effectiveG, effectiveB);
|
|
660
|
+
return ColorUtils.calculateLuminance(effectiveColor) > 0.5;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Apply default status bar style based on system theme.
|
|
665
|
+
* Automatically detects if the device is in light or dark mode and applies the
|
|
666
|
+
* appropriate style.
|
|
667
|
+
*
|
|
668
|
+
* @param activity The activity to apply the style to
|
|
669
|
+
*/
|
|
670
|
+
public void applyDefaultStyle(Activity activity) {
|
|
671
|
+
boolean isDarkMode = isSystemInDarkMode(activity);
|
|
672
|
+
String style = isDarkMode ? "DARK" : "LIGHT";
|
|
673
|
+
Log.d(TAG, "applyDefaultStyle: detected system theme=" + (isDarkMode ? "dark" : "light") + ", applying style="
|
|
674
|
+
+ style);
|
|
675
|
+
setStyle(activity, style, null);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Check if the system is currently in dark mode.
|
|
680
|
+
*
|
|
681
|
+
* @param activity The activity to check the configuration from
|
|
682
|
+
* @return true if system is in dark mode, false otherwise
|
|
683
|
+
*/
|
|
684
|
+
private boolean isSystemInDarkMode(Activity activity) {
|
|
685
|
+
int nightModeFlags = activity.getResources().getConfiguration().uiMode
|
|
686
|
+
& android.content.res.Configuration.UI_MODE_NIGHT_MASK;
|
|
687
|
+
boolean isDarkMode = nightModeFlags == android.content.res.Configuration.UI_MODE_NIGHT_YES;
|
|
688
|
+
Log.d(TAG, "isSystemInDarkMode: " + isDarkMode);
|
|
689
|
+
return isDarkMode;
|
|
690
|
+
}
|
|
691
|
+
}
|