Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.graphics.Color;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
Expand All @@ -25,9 +26,11 @@

import androidx.activity.EdgeToEdge;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.SystemBarStyle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.WindowCompat;

public class NavigationActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity, JsDevReloadHandler.ReloadListener {
@Nullable
Expand Down Expand Up @@ -189,8 +192,18 @@ protected void enableEdgeToEdge() {
* calling {@code EdgeToEdge.enable()} directly.
*/
protected void activateEdgeToEdge() {
EdgeToEdge.enable(this);
EdgeToEdge.enable(
this,
SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)
);
SystemUiUtils.activateEdgeToEdge();
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
SystemUiUtils.setNavigationBarContrastEnforced(getWindow(), false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(Color.TRANSPARENT);
getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
}

protected void addDefaultSplashLayout() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,38 @@ public static NavigationBarOptions parse(Context context, JSONObject json) {

result.backgroundColor = ThemeColour.parse(context, json.optJSONObject("backgroundColor"));
result.isVisible = BoolParser.parse(json, "visible");
result.drawBehind = BoolParser.parse(json, "drawBehind");

return result;
}

public ThemeColour backgroundColor = new NullThemeColour();
public Bool isVisible = new NullBool();
public Bool drawBehind = new NullBool();

public void mergeWith(NavigationBarOptions other) {
if (other.isVisible.hasValue()) isVisible = other.isVisible;
if (other.backgroundColor.hasValue()) backgroundColor = other.backgroundColor;
if (other.drawBehind.hasValue()) drawBehind = other.drawBehind;
}

public void mergeWithDefault(NavigationBarOptions defaultOptions) {
if (!isVisible.hasValue()) isVisible = defaultOptions.isVisible;
if (!backgroundColor.hasValue()) backgroundColor = defaultOptions.backgroundColor;
if (!drawBehind.hasValue()) drawBehind = defaultOptions.drawBehind;
}
}

public boolean shouldDrawBehind() {
if (drawBehind.isFalse()) return false;
if (drawBehind.isTrue()) return true;
return backgroundColor.hasTransparency();
}

public boolean isDrawBehindAndVisible() {
return shouldDrawBehind() && isVisible.isTrueOrUndefined();
}

public boolean hasAnyValue() {
return isVisible.hasValue() || drawBehind.hasValue() || backgroundColor.hasValue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Activity
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
Expand All @@ -15,6 +16,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.max

object SystemUiUtils {
private const val STATUS_BAR_HEIGHT_M = 24
Expand Down Expand Up @@ -187,6 +189,29 @@ object SystemUiUtils {
isEdgeToEdgeActive = true
}

@JvmStatic
fun setNavigationBarContrastEnforced(window: Window?, enforced: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window?.isNavigationBarContrastEnforced = enforced
}
}

@JvmStatic
fun getContentBottomSystemBarInset(insets: WindowInsetsCompat, drawBehindNavigationBar: Boolean): Int {
val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
if (!isEdgeToEdgeActive) return imeBottom
if (drawBehindNavigationBar) return imeBottom
val navBarBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
return max(imeBottom, navBarBottom)
}

@JvmStatic
fun getBottomTabsSystemBarPadding(insets: WindowInsetsCompat, drawBehindNavigationBar: Boolean): Int {
if (insets.getInsets(WindowInsetsCompat.Type.ime()).bottom > 0) return 0
if (isEdgeToEdgeActive && drawBehindNavigationBar) return 0
return insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
}

/**
* Clears references to system bar background views.
* Call from Activity.onDestroy to avoid leaking views across activity recreation.
Expand Down Expand Up @@ -327,17 +352,36 @@ object SystemUiUtils {
* falls back to the deprecated window API on older configurations.
*/
@JvmStatic
fun setNavigationBarBackgroundColor(window: Window?, color: Int, lightColor: Boolean) {
lastExplicitNavBarColor = color
fun setNavigationBarBackgroundColor(window: Window?, color: Int, lightColor: Boolean, hideOverlay: Boolean) {
window?.let {
WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars = lightColor
}
navBarBackgroundView?.setBackgroundColor(color)
if (hideOverlay) {
lastExplicitNavBarColor = null
navBarBackgroundView?.apply {
visibility = View.GONE
setBackgroundColor(Color.TRANSPARENT)
}
@Suppress("DEPRECATION")
window?.navigationBarColor = Color.TRANSPARENT
return
}

lastExplicitNavBarColor = color
navBarBackgroundView?.apply {
visibility = View.VISIBLE
setBackgroundColor(color)
}
if (!isEdgeToEdgeActive) {
@Suppress("DEPRECATION")
window?.navigationBarColor = color
}
}

@JvmStatic
fun setNavigationBarBackgroundColor(window: Window?, color: Int, lightColor: Boolean) {
setNavigationBarBackgroundColor(window, color, lightColor, Color.alpha(color) == 0)
}

// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

import com.aurelhubert.ahbottomnavigation.AHBottomNavigation;
Expand All @@ -25,6 +25,7 @@
import com.reactnativenavigation.react.CommandListenerAdapter;
import com.reactnativenavigation.react.events.EventEmitter;
import com.reactnativenavigation.utils.ImageLoader;
import com.reactnativenavigation.utils.SystemUiUtils;
import com.reactnativenavigation.viewcontrollers.bottomtabs.attacher.BottomTabsAttacher;
import com.reactnativenavigation.viewcontrollers.child.ChildControllersRegistry;
import com.reactnativenavigation.viewcontrollers.parent.ParentController;
Expand Down Expand Up @@ -156,6 +157,7 @@ public void mergeOptions(Options options) {
public void applyChildOptions(Options options, ViewController<?> child) {
super.applyChildOptions(options, child);
presenter.applyChildOptions(resolveCurrentOptions(), child);
onNavigationBarOptionsChanged(options);
performOnParentController(parent -> parent.applyChildOptions(
this.options.copy()
.clearBottomTabsOptions()
Expand All @@ -170,6 +172,7 @@ public void mergeChildOptions(Options options, ViewController<?> child) {
super.mergeChildOptions(options, child);
presenter.mergeChildOptions(options, child);
tabPresenter.mergeChildOptions(options, child);
onNavigationBarOptionsChanged(options);
performOnParentController(parent -> parent.mergeChildOptions(options.copy().clearBottomTabsOptions(), child));
}

Expand Down Expand Up @@ -316,14 +319,23 @@ public Animator getPopAnimation(Options appearingOptions, Options disappearingOp

@Override
protected WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) {
Insets sysInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars());
Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());

int bottomInset = (imeInsets.bottom > 0) ? 0 : sysInsets.bottom;
view.setPaddingRelative(0, 0, 0, bottomInset);
boolean drawBehindNavBar = resolveCurrentOptions().navigationBar.isDrawBehindAndVisible();
view.setPaddingRelative(0, 0, 0, SystemUiUtils.getBottomTabsSystemBarPadding(insets, drawBehindNavBar));
return insets;
}

private void onNavigationBarOptionsChanged(Options options) {
if (!options.navigationBar.hasAnyValue()) return;
refreshNavigationBarInsets();
}

private void refreshNavigationBarInsets() {
if (getView() != null) {
applyBottomInset();
ViewCompat.requestApplyInsets(getView());
}
}

@RestrictTo(RestrictTo.Scope.TESTS)
public BottomTabs getBottomTabs() {
return bottomTabs;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ class BottomTabsPresenter(
private fun syncNavigationBarColor(options: Options, tabsColor: Int) {
val resolved = options.copy().withDefaultOptions(defaultOptions)
if (resolved.navigationBar.backgroundColor.hasValue()) return
if (resolved.navigationBar.shouldDrawBehind()) return
val window = (bottomTabsContainer.context as? Activity)?.window ?: return
SystemUiUtils.setNavigationBarBackgroundColor(window, tabsColor, isColorLight(tabsColor))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,8 @@ protected WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat i
insets.getInsets(WindowInsetsCompat.Type.navigationBars()).top -
systemBarsInsets.top;

int navBarBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
int imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
int systemWindowInsetBottom = SystemUiUtils.isEdgeToEdgeActive()
? Math.max(imeBottom, navBarBottom)
: imeBottom;
boolean drawBehindNavBar = resolveCurrentOptions(presenter.defaultOptions).navigationBar.isDrawBehindAndVisible();
int systemWindowInsetBottom = SystemUiUtils.getContentBottomSystemBarInset(insets, drawBehindNavBar);

WindowInsetsCompat finalInsets = new WindowInsetsCompat.Builder()
.setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;

import androidx.core.view.ViewCompat;

import com.reactnativenavigation.options.NavigationBarOptions;
import com.reactnativenavigation.options.Options;
import com.reactnativenavigation.options.OrientationOptions;
Expand Down Expand Up @@ -114,16 +116,16 @@ private void mergeStatusBarOptions(View view, StatusBarOptions statusBarOptions)

private void applyNavigationBarOptions(NavigationBarOptions options) {
applyNavigationBarVisibility(options);
setNavigationBarBackgroundColor(options);
applyNavigationBarBackground(options);
refreshNavigationBarInsets();
}

private void mergeNavigationBarOptions(NavigationBarOptions options) {
mergeNavigationBarVisibility(options);
setNavigationBarBackgroundColor(options);
}

private void mergeNavigationBarVisibility(NavigationBarOptions options) {
if (options.isVisible.hasValue()) applyNavigationBarOptions(options);
if (options.isVisible.hasValue()) {
applyNavigationBarVisibility(options);
}
applyNavigationBarBackground(options);
refreshNavigationBarInsets();
}

private void applyNavigationBarVisibility(NavigationBarOptions options) {
Expand All @@ -136,20 +138,38 @@ private void applyNavigationBarVisibility(NavigationBarOptions options) {
}
}

private void setNavigationBarBackgroundColor(NavigationBarOptions navigationBar) {
private void applyNavigationBarBackground(NavigationBarOptions navigationBar) {
if (activity == null) return;
int defaultColor = SystemUiUtils.getDefaultNavBarColor();
if (navigationBar.backgroundColor.canApplyValue()) {
int color = navigationBar.backgroundColor.get(defaultColor);
SystemUiUtils.setNavigationBarBackgroundColor(activity.getWindow(), color, isColorLight(color));
int color;
boolean hideOverlay;
if (navigationBar.isDrawBehindAndVisible()) {
if (navigationBar.backgroundColor.canApplyValue()) {
color = navigationBar.backgroundColor.get(defaultColor);
hideOverlay = Color.alpha(color) == 0;
} else {
color = Color.TRANSPARENT;
hideOverlay = true;
}
} else {
SystemUiUtils.setNavigationBarBackgroundColor(activity.getWindow(), defaultColor, isColorLight(defaultColor));
hideOverlay = false;
color = navigationBar.backgroundColor.canApplyValue()
? navigationBar.backgroundColor.get(defaultColor)
: defaultColor;
}
SystemUiUtils.setNavigationBarBackgroundColor(
activity.getWindow(), color, isColorLight(color), hideOverlay);
}

private void refreshNavigationBarInsets() {
if (activity == null) return;
ViewCompat.requestApplyInsets(activity.getWindow().getDecorView());
}

public void onConfigurationChanged(ViewController controller, Options options) {
Options withDefault = options.withDefaultOptions(defaultOptions);
setNavigationBarBackgroundColor(withDefault.navigationBar);
applyNavigationBarBackground(withDefault.navigationBar);
refreshNavigationBarInsets();
StatusBarPresenter.instance.onConfigurationChanged(withDefault.statusBar);
applyBackgroundColor(controller, withDefault);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,18 @@ public void shouldMergeInsetsOnTopMostParent(){
verify(parentView).setPadding(2,1,4,3);
}

@Test
public void applyNavigationBarDrawBehind_usesTransparentOverlay() {
mockSystemUiUtils(0, 0, (mockedStatic) -> {
ViewGroup spy = spy(new FrameLayout(activity));
Mockito.when(controller.getView()).thenReturn(spy);
Mockito.when(controller.resolveCurrentOptions()).thenReturn(Options.EMPTY);
Options options = new Options();
options.navigationBar.drawBehind = new Bool(true);
uut.applyOptions(controller, options);
mockedStatic.verify(() -> SystemUiUtils.setNavigationBarBackgroundColor(
any(), eq(android.graphics.Color.TRANSPARENT), eq(false), eq(true)), times(1));
});
}

}
5 changes: 5 additions & 0 deletions src/interfaces/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1574,6 +1574,11 @@ export interface AnimationOptions {
export interface NavigationBarOptions {
backgroundColor?: Color;
visible?: boolean;
/**
* Draw screen content behind the system navigation bar while keeping it visible.
* On Android 15+ edge-to-edge, use with `backgroundColor: 'transparent'`.
*/
drawBehind?: boolean;
}

/**
Expand Down
10 changes: 9 additions & 1 deletion website/docs/api/options-navigationBar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@ Set the navigation bar color. When a light background color is used, the color o

| Type | Required | Platform | Default |
| --------------------- | -------- | -------- | ------- |
| Color | No | Android | 'black' |
| Color | No | Android | 'black' |

### `drawBehind`

Draw screen content behind the system navigation bar while keeping it visible (gesture pill / 3-button bar remain on screen). Use with edge-to-edge enabled in your activity. With `backgroundColor: 'transparent'`, content shows through the nav bar area; with an opaque color, RNN paints a scrim overlay without reserving bottom layout inset.

| Type | Required | Platform |
| ------- | -------- | -------- |
| boolean | No | Android |
Loading