Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-edge-to-edge-window-color-api.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -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);
Expand All @@ -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() {
Expand All @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
Loading