Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Edge-to-edge support #47554

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ org.gradle.caching=true

android.useAndroidX=true

edgeToEdgeEnabled=true

# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ object PropertyUtils {
const val NEW_ARCH_ENABLED = "newArchEnabled"
const val SCOPED_NEW_ARCH_ENABLED = "react.newArchEnabled"

/** Public property that toggles the New Architecture */
/** Public property that toggles Hermes */
const val HERMES_ENABLED = "hermesEnabled"
const val SCOPED_HERMES_ENABLED = "react.hermesEnabled"

Expand Down
1 change: 1 addition & 0 deletions packages/helloworld/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ android.useAndroidX=true
reactNativeArchitectures=arm64-v8a
newArchEnabled=true
hermesEnabled=true
edgeToEdgeEnabled=false
9 changes: 7 additions & 2 deletions packages/react-native/Libraries/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const AppContainer = require('../ReactNative/AppContainer');
const I18nManager = require('../ReactNative/I18nManager');
const {RootTagContext} = require('../ReactNative/RootTag');
const StyleSheet = require('../StyleSheet/StyleSheet');
const Appearance = require('../Utilities/Appearance');
const Platform = require('../Utilities/Platform');
const React = require('react');

Expand Down Expand Up @@ -272,6 +273,8 @@ class Modal extends React.Component<Props, State> {
return null;
}

const isEdgeToEdge = Appearance.isEdgeToEdge();

const containerStyles = {
backgroundColor:
this.props.transparent === true
Expand Down Expand Up @@ -316,8 +319,10 @@ class Modal extends React.Component<Props, State> {
onShow={this.props.onShow}
onDismiss={onDismiss}
visible={this.props.visible}
statusBarTranslucent={this.props.statusBarTranslucent}
navigationBarTranslucent={this.props.navigationBarTranslucent}
statusBarTranslucent={isEdgeToEdge || this.props.statusBarTranslucent}
navigationBarTranslucent={
isEdgeToEdge || this.props.navigationBarTranslucent
}
identifier={this._identifier}
style={styles.modal}
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
Expand Down
19 changes: 19 additions & 0 deletions packages/react-native/Libraries/Utilities/Appearance.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ let lazyState: ?{
// Cache the color scheme to reduce the cost of reading it between changes.
// NOTE: If `NativeAppearance` is null, this will always be null.
appearance: ?Appearance,
// NOTE: If `NativeAppearance` is null, this will always be null.
edgeToEdge: ?boolean,
// NOTE: This is non-nullable to make it easier for `onChangedListener` to
// return a non-nullable `EventSubscription` value. This is not the common
// path, so we do not have to over-optimize it.
Expand All @@ -47,12 +49,14 @@ function getState(): $NonMaybeType<typeof lazyState> {
lazyState = {
NativeAppearance: null,
appearance: null,
edgeToEdge: null,
eventEmitter,
};
} else {
const state: $NonMaybeType<typeof lazyState> = {
NativeAppearance,
appearance: null,
edgeToEdge: null,
eventEmitter,
};
new NativeEventEmitter<{
Expand Down Expand Up @@ -111,6 +115,21 @@ export function setColorScheme(colorScheme: ?ColorSchemeName): void {
}
}

export function isEdgeToEdge(): boolean {
let edgeToEdge = false;
const state = getState();
const {NativeAppearance} = state;
if (NativeAppearance != null) {
if (state.edgeToEdge == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use === instead of ==? Or there is a chance that edgeToEdge can be undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just to please the type system, copied from the line above.

// Lazily initialize `state.edgeToEdge`. This should only
// happen once because we never reassign a null value to it.
state.edgeToEdge = NativeAppearance.isEdgeToEdge();
}
edgeToEdge = state.edgeToEdge;
}
return edgeToEdge;
}

/**
* Add an event handler that is fired when appearance preferences change.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8840,6 +8840,7 @@ declare export default typeof UTFSequence;
exports[`public API should not change unintentionally Libraries/Utilities/Appearance.js 1`] = `
"declare export function getColorScheme(): ?ColorSchemeName;
declare export function setColorScheme(colorScheme: ?ColorSchemeName): void;
declare export function isEdgeToEdge(): boolean;
declare export function addChangeListener(
listener: ({ colorScheme: ?ColorSchemeName }) => void
): EventSubscription;
Expand Down
5 changes: 5 additions & 0 deletions packages/react-native/React/CoreModules/RCTAppearance.mm
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ - (dispatch_queue_t)methodQueue
return _currentColorScheme;
}

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, isEdgeToEdge)
{
return @(true);
}

- (void)appearanceChanged:(NSNotification *)notification
{
NSDictionary *userInfo = [notification userInfo];
Expand Down
6 changes: 6 additions & 0 deletions packages/react-native/ReactAndroid/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,11 @@ fun enableWarningsAsErrors(): Boolean {
return value?.toString()?.toBoolean() ?: false
}

fun isEdgeToEdgeEnabled(): Boolean {
val value = rootProject.properties["edgeToEdgeEnabled"]
return value?.toString()?.toBoolean() ?: false
}

val packageReactNdkLibsForBuck by
tasks.registering(Copy::class) {
dependsOn("mergeDebugNativeLibs")
Expand Down Expand Up @@ -518,6 +523,7 @@ android {
consumerProguardFiles("proguard-rules.pro")

buildConfigField("boolean", "IS_INTERNAL_BUILD", "false")
buildConfigField("boolean", "IS_EDGE_TO_EDGE_ENABLED", isEdgeToEdgeEnabled().toString())
buildConfigField("int", "EXOPACKAGE_FLAGS", "0")
buildConfigField("boolean", "UNSTABLE_ENABLE_FUSEBOX_RELEASE", "false")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.facebook.react.common.annotations.DeprecatedInNewArchitecture;
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.views.view.WindowUtilKt;
import com.facebook.systrace.Systrace;

/**
Expand Down Expand Up @@ -120,6 +121,9 @@ public void onCreate(Bundle savedInstanceState) {
() -> {
String mainComponentName = getMainComponentName();
final Bundle launchOptions = composeLaunchOptions();
if (mActivity != null && BuildConfig.IS_EDGE_TO_EDGE_ENABLED) {
WindowUtilKt.enableEdgeToEdge(mActivity.getWindow());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isWideColorGamutEnabled()) {
mActivity.getWindow().setColorMode(ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
package com.facebook.react.modules.appearance

import android.content.Context
import android.content.res.Configuration
import androidx.appcompat.app.AppCompatDelegate
import com.facebook.fbreact.specs.NativeAppearanceSpec
import com.facebook.react.BuildConfig
import com.facebook.react.ReactActivity
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.views.common.ContextUtils

/** Module that exposes the user's preferred color scheme. */
@ReactModule(name = NativeAppearanceSpec.NAME)
Expand All @@ -41,12 +43,9 @@ constructor(
return overrideColorScheme.getScheme()
}

val currentNightMode =
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return when (currentNightMode) {
Configuration.UI_MODE_NIGHT_NO -> "light"
Configuration.UI_MODE_NIGHT_YES -> "dark"
else -> "light"
return when (ContextUtils.isDarkMode(context)) {
true -> "dark"
false -> "light"
}
}

Expand All @@ -69,6 +68,9 @@ constructor(
}
}

public override fun isEdgeToEdge(): Boolean =
BuildConfig.IS_EDGE_TO_EDGE_ENABLED

/** Stub */
public override fun addListener(eventName: String): Unit = Unit

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.facebook.common.logging.FLog
import com.facebook.fbreact.specs.NativeStatusBarManagerAndroidSpec
import com.facebook.react.BuildConfig
import com.facebook.react.ReactActivity
import com.facebook.react.bridge.GuardedRunnable
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
Expand Down Expand Up @@ -67,6 +69,12 @@ public class StatusBarModule(reactContext: ReactApplicationContext?) :
"StatusBarModule: Ignored status bar change, current activity is null.")
return
}
if (BuildConfig.IS_EDGE_TO_EDGE_ENABLED) {
FLog.w(
ReactConstants.TAG,
"StatusBarModule: Ignored status bar change, current activity is edge-to-edge.")
return
}
UiThreadUtil.runOnUiThread(
object : GuardedRunnable(reactApplicationContext) {
override fun runGuarded() {
Expand Down Expand Up @@ -96,6 +104,12 @@ public class StatusBarModule(reactContext: ReactApplicationContext?) :
"StatusBarModule: Ignored status bar change, current activity is null.")
return
}
if (BuildConfig.IS_EDGE_TO_EDGE_ENABLED) {
FLog.w(
ReactConstants.TAG,
"StatusBarModule: Ignored status bar change, current activity is edge-to-edge.")
return
}
UiThreadUtil.runOnUiThread(
object : GuardedRunnable(reactApplicationContext) {
override fun runGuarded() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.facebook.infer.annotation.ThreadConfined;
import com.facebook.infer.annotation.ThreadSafe;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.BuildConfig;
import com.facebook.react.MemoryPressureRouter;
import com.facebook.react.ReactHost;
import com.facebook.react.ReactInstanceEventListener;
Expand Down Expand Up @@ -72,6 +73,7 @@
import com.facebook.react.uimanager.events.BlackHoleEventDispatcher;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
import com.facebook.react.views.view.WindowUtilKt;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -823,6 +825,19 @@ public void onNewIntent(Intent intent) {
@ThreadConfined(UI)
@Override
public void onConfigurationChanged(Context updatedContext) {
if (BuildConfig.IS_EDGE_TO_EDGE_ENABLED) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
WindowUtilKt.enableEdgeToEdge(currentActivity.getWindow());
}
}
});
}

ReactContext currentReactContext = getCurrentReactContext();
if (currentReactContext != null) {
AppearanceModule appearanceModule =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ package com.facebook.react.views.common

import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration

/**
* Class containing static methods involving manipulations of Contexts and their related subclasses.
*/
public object ContextUtils {

@JvmStatic
public fun isDarkMode(context: Context): Boolean =
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES

/**
* Returns the nearest context in the chain (as defined by ContextWrapper.getBaseContext()) which
* is an instance of the specified type, or null if one could not be found
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ 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 com.facebook.react.views.view.enableEdgeToEdge
import com.facebook.react.views.view.disableEdgeToEdge
import java.util.Objects

/**
Expand Down Expand Up @@ -343,9 +344,10 @@ public class ReactModalHostView(context: ThemedReactContext) :
}

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

if (!navigationBarTranslucent) {
if (navigationBarTranslucent) {
dialogWindow.enableEdgeToEdge()
} else {
dialogWindow.disableEdgeToEdge()
dialogWindow.setStatusBarTranslucency(statusBarTranslucent)
}

Expand Down
Loading
Loading