diff --git a/.changeset/fix-edge-to-edge-window-color-api.md b/.changeset/fix-edge-to-edge-window-color-api.md new file mode 100644 index 000000000..f33137a1e --- /dev/null +++ b/.changeset/fix-edge-to-edge-window-color-api.md @@ -0,0 +1,5 @@ +--- +"@capawesome/capacitor-android-edge-to-edge-support": patch +--- + +fix(android-edge-to-edge-support): use Window color API when edge-to-edge is not enforced diff --git a/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdge.java b/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdge.java index a82c10f61..6b9fe440d 100644 --- a/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdge.java +++ b/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdge.java @@ -1,9 +1,13 @@ package io.capawesome.capacitorjs.plugins.androidedgetoedgesupport; import android.graphics.Color; +import android.os.Build; +import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -27,9 +31,11 @@ public class EdgeToEdge { @Nullable private View statusBarOverlay; - private int currentNavigationBarColor; + @Nullable + private Integer currentNavigationBarColor; - private int currentStatusBarColor; + @Nullable + private Integer currentStatusBarColor; public EdgeToEdge(@NonNull EdgeToEdgePlugin plugin, @NonNull EdgeToEdgeConfig config) { this.config = config; @@ -42,17 +48,27 @@ public EdgeToEdge(@NonNull EdgeToEdgePlugin plugin, @NonNull EdgeToEdgeConfig co } public void enable() { - // Create color overlays if they don't exist - createColorOverlays(); - // Restore previously set colors - setStatusBarColor(currentStatusBarColor); - setNavigationBarColor(currentNavigationBarColor); - // Apply insets - applyInsets(); + if (isEdgeToEdgeEnforced()) { + // Create color overlays if they don't exist + createColorOverlays(); + // Restore previously set colors + applyStatusBarColor(); + applyNavigationBarColor(); + // Apply insets + applyInsets(); + } else { + // Pre-Android 15 (or Android 15 with opt-out): the platform still honors + // Window.setStatusBarColor/setNavigationBarColor, so we use that directly + // instead of overlay views that depend on the inset dispatch chain. + applyStatusBarColor(); + applyNavigationBarColor(); + } } public void disable() { - removeInsets(); + if (isEdgeToEdgeEnforced()) { + removeInsets(); + } } public ViewGroup.MarginLayoutParams getInsets() { @@ -98,14 +114,7 @@ private void applyInsetsInternal(View view, WindowInsetsCompat currentInsets) { ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); mlp.bottomMargin = bottomMargin; - // Apply top margin when edge-to-edge is active. On Android 15+ it is always enforced. - // On older versions, use the system content view's top padding only as a heuristic that - // the top inset may already be handled elsewhere (for example by decor fitting or another - // insets/padding adjustment), rather than as a guaranteed signal of decorFitsSystemWindows. - View contentView = plugin.getActivity().findViewById(android.R.id.content); - boolean topInsetLikelyHandledBySystem = - contentView != null && contentView.getPaddingTop() >= systemBarsInsets.top && systemBarsInsets.top > 0; - mlp.topMargin = topInsetLikelyHandledBySystem ? 0 : systemBarsInsets.top; + mlp.topMargin = systemBarsInsets.top; mlp.leftMargin = systemBarsInsets.left; mlp.rightMargin = systemBarsInsets.right; view.setLayoutParams(mlp); @@ -114,23 +123,64 @@ private void applyInsetsInternal(View view, WindowInsetsCompat currentInsets) { updateColorOverlays(systemBarsInsets); } - private void removeInsets() { - View view = plugin.getBridge().getWebView(); - // Get parent view - ViewGroup parent = (ViewGroup) view.getParent(); - // Reset insets - ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); - mlp.topMargin = 0; - mlp.leftMargin = 0; - mlp.rightMargin = 0; - mlp.bottomMargin = 0; - view.setLayoutParams(mlp); - // Set a no-op listener that consumes insets without applying them. - // Using null would allow Android 15's default edge-to-edge handling to take over, - // which can cause extra padding when re-enabling. - ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> WindowInsetsCompat.CONSUMED); - // Remove color overlays - removeColorOverlays(); + private void applyNavigationBarColor() { + if (currentNavigationBarColor == null) { + return; + } + if (isEdgeToEdgeEnforced()) { + if (navigationBarOverlay != null) { + navigationBarOverlay.setBackgroundColor(currentNavigationBarColor); + } + } else { + Window window = plugin.getActivity().getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + setNavigationBarColorDeprecated(window, currentNavigationBarColor); + } + } + + private void applyStatusBarColor() { + if (currentStatusBarColor == null) { + return; + } + if (isEdgeToEdgeEnforced()) { + if (statusBarOverlay != null) { + statusBarOverlay.setBackgroundColor(currentStatusBarColor); + } + } else { + Window window = plugin.getActivity().getWindow(); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + setStatusBarColorDeprecated(window, currentStatusBarColor); + } + } + + private ViewGroup.LayoutParams createLayoutParams(ViewGroup parent, int width, int height, int gravity) { + String parentClassName = parent.getClass().getName(); + + // Handle CoordinatorLayout using reflection + if (parentClassName.contains("CoordinatorLayout")) { + try { + Class layoutParamsClass = Class.forName("androidx.coordinatorlayout.widget.CoordinatorLayout$LayoutParams"); + Constructor constructor = layoutParamsClass.getConstructor(int.class, int.class); + ViewGroup.LayoutParams params = (ViewGroup.LayoutParams) constructor.newInstance(width, height); + // Set gravity using reflection + layoutParamsClass.getField("gravity").setInt(params, gravity); + return params; + } catch (Exception e) { + Logger.error("EdgeToEdge", "Failed to create CoordinatorLayout.LayoutParams", e); + } + } + + // Handle FrameLayout + if (parent instanceof FrameLayout) { + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, height); + params.gravity = gravity; + return params; + } + + // Fallback to MarginLayoutParams + ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(width, height); + return params; } private void createColorOverlays() { @@ -148,6 +198,30 @@ private void createColorOverlays() { } } + private boolean isEdgeToEdgeEnforced() { + int deviceApi = Build.VERSION.SDK_INT; + if (deviceApi < Build.VERSION_CODES.VANILLA_ICE_CREAM) { + // Pre-Android 15: platform honors legacy Window.setStatusBarColor/setNavigationBarColor + return false; + } + if (deviceApi == Build.VERSION_CODES.VANILLA_ICE_CREAM) { + // Android 15: edge-to-edge enforced unless the app opted out via + // android:windowOptOutEdgeToEdgeEnforcement on its theme + return !isEdgeToEdgeOptOutEnabled(); + } + // Android 16+: opt-out is ignored, edge-to-edge is always enforced + return true; + } + + private boolean isEdgeToEdgeOptOutEnabled() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { + return false; + } + TypedValue value = new TypedValue(); + plugin.getActivity().getTheme().resolveAttribute(android.R.attr.windowOptOutEdgeToEdgeEnforcement, value, true); + return value.data != 0; + } + private void removeColorOverlays() { View webView = plugin.getBridge().getWebView(); ViewGroup parent = (ViewGroup) webView.getParent(); @@ -163,18 +237,41 @@ private void removeColorOverlays() { } } + private void removeInsets() { + View view = plugin.getBridge().getWebView(); + // Reset insets + ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + mlp.topMargin = 0; + mlp.leftMargin = 0; + mlp.rightMargin = 0; + mlp.bottomMargin = 0; + view.setLayoutParams(mlp); + // Set a no-op listener that consumes insets without applying them. + // Using null would allow Android 15's default edge-to-edge handling to take over, + // which can cause extra padding when re-enabling. + ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> WindowInsetsCompat.CONSUMED); + // Remove color overlays + removeColorOverlays(); + } + private void setNavigationBarColor(int color) { this.currentNavigationBarColor = color; - if (navigationBarOverlay != null) { - navigationBarOverlay.setBackgroundColor(color); - } + applyNavigationBarColor(); + } + + @SuppressWarnings("deprecation") + private void setNavigationBarColorDeprecated(@NonNull Window window, int color) { + window.setNavigationBarColor(color); } private void setStatusBarColor(int color) { this.currentStatusBarColor = color; - if (statusBarOverlay != null) { - statusBarOverlay.setBackgroundColor(color); - } + applyStatusBarColor(); + } + + @SuppressWarnings("deprecation") + private void setStatusBarColorDeprecated(@NonNull Window window, int color) { + window.setStatusBarColor(color); } private void updateColorOverlays(Insets systemBarsInsets) { @@ -203,33 +300,4 @@ private void updateColorOverlays(Insets systemBarsInsets) { navigationBarOverlay.setLayoutParams(navParams); } } - - private ViewGroup.LayoutParams createLayoutParams(ViewGroup parent, int width, int height, int gravity) { - String parentClassName = parent.getClass().getName(); - - // Handle CoordinatorLayout using reflection - if (parentClassName.contains("CoordinatorLayout")) { - try { - Class layoutParamsClass = Class.forName("androidx.coordinatorlayout.widget.CoordinatorLayout$LayoutParams"); - Constructor constructor = layoutParamsClass.getConstructor(int.class, int.class); - ViewGroup.LayoutParams params = (ViewGroup.LayoutParams) constructor.newInstance(width, height); - // Set gravity using reflection - layoutParamsClass.getField("gravity").setInt(params, gravity); - return params; - } catch (Exception e) { - Logger.error("EdgeToEdge", "Failed to create CoordinatorLayout.LayoutParams", e); - } - } - - // Handle FrameLayout - if (parent instanceof FrameLayout) { - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, height); - params.gravity = gravity; - return params; - } - - // Fallback to MarginLayoutParams - ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(width, height); - return params; - } } diff --git a/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdgeConfig.java b/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdgeConfig.java index 949ba7f26..81861146e 100644 --- a/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdgeConfig.java +++ b/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdgeConfig.java @@ -1,34 +1,42 @@ package io.capawesome.capacitorjs.plugins.androidedgetoedgesupport; -import android.graphics.Color; +import androidx.annotation.Nullable; public class EdgeToEdgeConfig { - private int backgroundColor = Color.TRANSPARENT; - private int navigationBarColor = Color.TRANSPARENT; - private int statusBarColor = Color.TRANSPARENT; + @Nullable + private Integer backgroundColor; - public int getBackgroundColor() { + @Nullable + private Integer navigationBarColor; + + @Nullable + private Integer statusBarColor; + + @Nullable + public Integer getBackgroundColor() { return this.backgroundColor; } - public int getNavigationBarColor() { + @Nullable + public Integer getNavigationBarColor() { return this.navigationBarColor; } - public int getStatusBarColor() { + @Nullable + public Integer getStatusBarColor() { return this.statusBarColor; } - public void setBackgroundColor(int backgroundColor) { + public void setBackgroundColor(@Nullable Integer backgroundColor) { this.backgroundColor = backgroundColor; } - public void setNavigationBarColor(int navigationBarColor) { + public void setNavigationBarColor(@Nullable Integer navigationBarColor) { this.navigationBarColor = navigationBarColor; } - public void setStatusBarColor(int statusBarColor) { + public void setStatusBarColor(@Nullable Integer statusBarColor) { this.statusBarColor = statusBarColor; } } diff --git a/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdgePlugin.java b/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdgePlugin.java index cbdc28115..92f51a473 100644 --- a/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdgePlugin.java +++ b/packages/android-edge-to-edge-support/android/src/main/java/io/capawesome/capacitorjs/plugins/androidedgetoedgesupport/EdgeToEdgePlugin.java @@ -139,7 +139,6 @@ private EdgeToEdgeConfig getEdgeToEdgeConfig() { config.setNavigationBarColor(Color.parseColor(navigationBarColor)); } - // Keep backgroundColor for any legacy code that might use it if (backgroundColor != null) { config.setBackgroundColor(Color.parseColor(backgroundColor)); }