Skip to content

Commit

Permalink
feat(android): Edge-to-edge Modal (navigationBarTranslucent prop) (#4…
Browse files Browse the repository at this point in the history
…7254)

Summary:
The future of Android is [edge-to-edge](react-native-community/discussions-and-proposals#827) and to make the React Native developer experience seamless in this regard, the ecosystem needs to transition from “opaque system bars by default” to “edge-to-edge by default.”

Currently, there's no easy way to have edge-to-edge modals, as they are implemented using `Dialog` instances (a separate `Window`) and only provide a `statusBarTranslucent` prop.

I tried to implement it in [`react-native-edge-to-edge`](https://github.com/zoontek/react-native-edge-to-edge) by listening to the `topShow` `UIManager` event. But if it works well when there's a defined animation, we can see a quick jump when there's none, because there's too much delay before the event, and edge-to-edge cannot be applied quick enough to the dialog window.

### react-native-edge-to-edge implem with animation (no jump)

https://github.com/user-attachments/assets/4933a102-87a5-40e4-98d9-47f8c0817592

### react-native-edge-to-edge implem without animation (jump)

https://github.com/user-attachments/assets/e4675589-08fe-44fe-b9d8-0a6b3552b461

 ---

For this reason, and because listening to event feels a bit hacky, I think it will be better to go for a new prop directly on RN Modal component: `navigationBarTranslucent`

> [!NOTE]
> `navigationBarTranslucent` cannot be used without `statusBarTranslucent`, as setting both enable edge-to-edge, like [AndroidX would do](https://github.com/androidx/androidx/blob/androidx-main/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt) and it would requires extra (and unecessary, given the direction Android is taking) work to find a way to keep the status bar opaque but the navigation bar transparent that work on Android 6 to 15+

### Additional infos

- Colors used for the buttons navigation bar in the PR are the default Android ones ([light](https://github.com/androidx/androidx/blob/androidx-main/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt#L37) and [dark](https://github.com/androidx/androidx/blob/androidx-main/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt#L42))
- Compared to the Google implementation, the light scrim is applied from `O_MR1` to `Q` (and not `O` to `Q`) as the [`android:windowLightNavigationBar`](https://developer.android.com/reference/android/R.attr#windowLightNavigationBar) style attribute is not available on `O` (it can only be applied programmatically on API 26).

## Changelog:

[ANDROID] [ADDED] - Add navigationBarTranslucent prop to Modal component

Pull Request resolved: #47254

Test Plan:
Run the tester app, toggle `navigationBarTranslucent`:

https://github.com/user-attachments/assets/286d173b-35a5-4951-9105-f9f7562d6764

-----
did some additional testing with RNTester using different justification

|flex-start|flex-end|
|https://pxl.cl/5Rd20|https://pxl.cl/5Rd21|

Reviewed By: javache

Differential Revision: D65103501

Pulled By: alanleedev

fbshipit-source-id: ef6473ecd785976d3e26c77bbc212222ec96c9f2
  • Loading branch information
zoontek authored and facebook-github-bot committed Nov 7, 2024
1 parent 61e660b commit 7a6c7a4
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 2 deletions.
5 changes: 5 additions & 0 deletions packages/react-native/Libraries/Modal/Modal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export interface ModalPropsAndroid {
* Determines whether your modal should go under the system statusbar.
*/
statusBarTranslucent?: boolean | undefined;

/**
* Determines whether your modal should go under the system navigationbar.
*/
navigationBarTranslucent?: boolean | undefined;
}

export type ModalProps = ModalBaseProps &
Expand Down
17 changes: 17 additions & 0 deletions packages/react-native/Libraries/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ export type Props = $ReadOnly<{|
*/
statusBarTranslucent?: ?boolean,

/**
* The `navigationBarTranslucent` prop determines whether your modal should go under
* the system navigationbar.
*
* See https://reactnative.dev/docs/modal.html#navigationbartranslucent-android
*/
navigationBarTranslucent?: ?boolean,

/**
* The `hardwareAccelerated` prop controls whether to force hardware
* acceleration for the underlying window.
Expand Down Expand Up @@ -176,6 +184,14 @@ function confirmProps(props: Props) {
`Modal with '${props.presentationStyle}' presentation style and 'transparent' value is not supported.`,
);
}
if (
props.navigationBarTranslucent === true &&
props.statusBarTranslucent !== true
) {
console.warn(
'Modal with translucent navigation bar and without translucent status bar is not supported.',
);
}
}
}

Expand Down Expand Up @@ -301,6 +317,7 @@ class Modal extends React.Component<Props, State> {
onDismiss={onDismiss}
visible={this.props.visible}
statusBarTranslucent={this.props.statusBarTranslucent}
navigationBarTranslucent={this.props.navigationBarTranslucent}
identifier={this._identifier}
style={styles.modal}
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6411,6 +6411,7 @@ export type Props = $ReadOnly<{|
),
transparent?: ?boolean,
statusBarTranslucent?: ?boolean,
navigationBarTranslucent?: ?boolean,
hardwareAccelerated?: ?boolean,
visible?: ?boolean,
onRequestClose?: ?DirectEventHandler<null>,
Expand Down
6 changes: 6 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -6264,6 +6264,7 @@ public abstract interface class com/facebook/react/viewmanagers/ModalHostViewMan
public abstract fun setAnimationType (Landroid/view/View;Ljava/lang/String;)V
public abstract fun setHardwareAccelerated (Landroid/view/View;Z)V
public abstract fun setIdentifier (Landroid/view/View;I)V
public abstract fun setNavigationBarTranslucent (Landroid/view/View;Z)V
public abstract fun setPresentationStyle (Landroid/view/View;Ljava/lang/String;)V
public abstract fun setStatusBarTranslucent (Landroid/view/View;Z)V
public abstract fun setSupportedOrientations (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V
Expand Down Expand Up @@ -6647,6 +6648,8 @@ public final class com/facebook/react/views/modal/ReactModalHostManager : com/fa
public fun setHardwareAccelerated (Lcom/facebook/react/views/modal/ReactModalHostView;Z)V
public synthetic fun setIdentifier (Landroid/view/View;I)V
public fun setIdentifier (Lcom/facebook/react/views/modal/ReactModalHostView;I)V
public synthetic fun setNavigationBarTranslucent (Landroid/view/View;Z)V
public fun setNavigationBarTranslucent (Lcom/facebook/react/views/modal/ReactModalHostView;Z)V
public synthetic fun setPresentationStyle (Landroid/view/View;Ljava/lang/String;)V
public fun setPresentationStyle (Lcom/facebook/react/views/modal/ReactModalHostView;Ljava/lang/String;)V
public synthetic fun setStatusBarTranslucent (Landroid/view/View;Z)V
Expand Down Expand Up @@ -6675,6 +6678,7 @@ public final class com/facebook/react/views/modal/ReactModalHostView : android/v
public fun getChildCount ()I
public final fun getEventDispatcher ()Lcom/facebook/react/uimanager/events/EventDispatcher;
public final fun getHardwareAccelerated ()Z
public final fun getNavigationBarTranslucent ()Z
public final fun getOnRequestCloseListener ()Lcom/facebook/react/views/modal/ReactModalHostView$OnRequestCloseListener;
public final fun getOnShowListener ()Landroid/content/DialogInterface$OnShowListener;
public final fun getStateWrapper ()Lcom/facebook/react/uimanager/StateWrapper;
Expand All @@ -6690,6 +6694,7 @@ public final class com/facebook/react/views/modal/ReactModalHostView : android/v
public final fun setEventDispatcher (Lcom/facebook/react/uimanager/events/EventDispatcher;)V
public final fun setHardwareAccelerated (Z)V
public fun setId (I)V
public final fun setNavigationBarTranslucent (Z)V
public final fun setOnRequestCloseListener (Lcom/facebook/react/views/modal/ReactModalHostView$OnRequestCloseListener;)V
public final fun setOnShowListener (Landroid/content/DialogInterface$OnShowListener;)V
public final fun setStateWrapper (Lcom/facebook/react/uimanager/StateWrapper;)V
Expand Down Expand Up @@ -7965,5 +7970,6 @@ public final class com/facebook/react/views/view/ViewGroupClickEvent : com/faceb
public final class com/facebook/react/views/view/WindowUtilKt {
public static final fun setStatusBarTranslucency (Landroid/view/Window;Z)V
public static final fun setStatusBarVisibility (Landroid/view/Window;Z)V
public static final fun setSystemBarsTranslucency (Landroid/view/Window;Z)V
}

Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ public class ReactModalHostManager :
view.statusBarTranslucent = statusBarTranslucent
}

@ReactProp(name = "navigationBarTranslucent")
public override fun setNavigationBarTranslucent(
view: ReactModalHostView,
navigationBarTranslucent: Boolean
) {
view.navigationBarTranslucent = navigationBarTranslucent
}

@ReactProp(name = "hardwareAccelerated")
public override fun setHardwareAccelerated(
view: ReactModalHostView,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import com.facebook.react.uimanager.events.EventDispatcher
import com.facebook.react.views.common.ContextUtils
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.react.views.view.setStatusBarTranslucency
import com.facebook.react.views.view.setSystemBarsTranslucency
import java.util.Objects

/**
Expand Down Expand Up @@ -79,6 +80,12 @@ public class ReactModalHostView(context: ThemedReactContext) :
createNewDialog = true
}

public var navigationBarTranslucent: Boolean = false
set(value) {
field = value
createNewDialog = true
}

public var animationType: String? = null
set(value) {
field = value
Expand Down Expand Up @@ -335,7 +342,12 @@ public class ReactModalHostView(context: ThemedReactContext) :
}
}

dialogWindow.setStatusBarTranslucency(statusBarTranslucent)
// Navigation bar cannot be translucent without status bar being translucent too
dialogWindow.setSystemBarsTranslucency(navigationBarTranslucent)

if (!navigationBarTranslucent) {
dialogWindow.setStatusBarTranslucency(statusBarTranslucent)
}

if (transparent) {
dialogWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@

package com.facebook.react.views.view

import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.view.Window
import android.view.WindowManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat

@Suppress("DEPRECATION")
public fun Window.setStatusBarTranslucency(isTranslucent: Boolean) {
Expand Down Expand Up @@ -61,3 +65,41 @@ private fun Window.statusBarShow() {
addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}

@Suppress("DEPRECATION")
public fun Window.setSystemBarsTranslucency(isTranslucent: Boolean) {
WindowCompat.setDecorFitsSystemWindows(this, !isTranslucent)

if (isTranslucent) {
val isDarkMode =
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
isStatusBarContrastEnforced = false
isNavigationBarContrastEnforced = true
}

statusBarColor = Color.TRANSPARENT
navigationBarColor =
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Color.TRANSPARENT
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && !isDarkMode ->
Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
else -> Color.argb(0x80, 0x1b, 0x1b, 0x1b)
}

WindowInsetsControllerCompat(this, this.decorView).run {
isAppearanceLightNavigationBars = !isDarkMode
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
attributes.layoutInDisplayCutoutMode =
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
else -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ type NativeProps = $ReadOnly<{|
*/
statusBarTranslucent?: WithDefault<boolean, false>,

/**
* The `navigationBarTranslucent` prop determines whether your modal should go under
* the system navigationbar.
*
* See https://reactnative.dev/docs/modal#navigationBarTranslucent
*/
navigationBarTranslucent?: WithDefault<boolean, false>,

/**
* The `hardwareAccelerated` prop controls whether to force hardware
* acceleration for the underlying window.
Expand Down
23 changes: 22 additions & 1 deletion packages/rn-tester/js/examples/Modal/ModalPresentation.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function ModalPresentation() {
transparent: false,
hardwareAccelerated: false,
statusBarTranslucent: false,
navigationBarTranslucent: false,
presentationStyle: Platform.select({
ios: 'fullScreen',
default: undefined,
Expand All @@ -72,6 +73,7 @@ function ModalPresentation() {
const presentationStyle = props.presentationStyle;
const hardwareAccelerated = props.hardwareAccelerated;
const statusBarTranslucent = props.statusBarTranslucent;
const navigationBarTranslucent = props.navigationBarTranslucent;
const backdropColor = props.backdropColor;
const backgroundColor = useContext(RNTesterThemeContext).BackgroundColor;

Expand All @@ -92,10 +94,29 @@ function ModalPresentation() {
<Switch
value={statusBarTranslucent}
onValueChange={enabled =>
setProps(prev => ({...prev, statusBarTranslucent: enabled}))
setProps(prev => ({
...prev,
statusBarTranslucent: enabled,
navigationBarTranslucent: false,
}))
}
/>
</View>
<View style={styles.inlineBlock}>
<RNTesterText style={styles.title}>
Navigation Bar Translucent 🟢
</RNTesterText>
<Switch
value={navigationBarTranslucent}
onValueChange={enabled => {
setProps(prev => ({
...prev,
statusBarTranslucent: enabled,
navigationBarTranslucent: enabled,
}));
}}
/>
</View>
<View style={styles.inlineBlock}>
<RNTesterText style={styles.title}>
Hardware Acceleration 🟢
Expand Down

0 comments on commit 7a6c7a4

Please sign in to comment.