From cf8e83625c55b2eae7c40a2fb7468423cf53188f Mon Sep 17 00:00:00 2001 From: Mathieu Acthernoene Date: Tue, 17 Jun 2025 17:06:14 +0200 Subject: [PATCH 1/4] Implement enableEdgeToEdge --- .../react/tasks/GenerateEntryPointTask.kt | 5 + .../react/utils/AgpConfiguratorUtils.kt | 5 + .../com/facebook/react/utils/ProjectUtils.kt | 9 ++ .../com/facebook/react/utils/PropertyUtils.kt | 6 +- .../react/tasks/GenerateEntryPointTaskTest.kt | 5 + .../facebook/react/utils/ProjectUtilsTest.kt | 29 ++++- .../facebook/react/ReactActivityDelegate.java | 4 + .../modules/deviceinfo/DeviceInfoModule.kt | 7 +- .../modules/statusbar/StatusBarModule.kt | 13 ++ .../react/views/modal/ReactModalHostView.kt | 34 +++-- .../facebook/react/views/view/WindowUtil.kt | 118 ++++++++++++------ .../modules/NativeDeviceInfo.js | 1 + .../rn-tester/android/app/gradle.properties | 2 + .../react/uiapp/RNTesterApplication.kt | 5 + private/helloworld/android/gradle.properties | 1 + 15 files changed, 193 insertions(+), 51 deletions(-) diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateEntryPointTask.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateEntryPointTask.kt index c845624f05eb77..f069eef477d96e 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateEntryPointTask.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateEntryPointTask.kt @@ -70,6 +70,7 @@ abstract class GenerateEntryPointTask : DefaultTask() { import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger; + import com.facebook.react.views.view.WindowUtilKt; import com.facebook.react.soloader.OpenSourceMergedSoMapping; import com.facebook.soloader.SoLoader; @@ -93,6 +94,10 @@ abstract class GenerateEntryPointTask : DefaultTask() { if ({{packageName}}.BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { DefaultNewArchitectureEntryPoint.load(); } + + if ({{packageName}}.BuildConfig.IS_EDGE_TO_EDGE_ENABLED) { + WindowUtilKt.setEdgeToEdgeFeatureFlagOn(); + } } } """ diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt index a4fcb893a9dc18..2617e7b287c322 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt @@ -11,6 +11,7 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension import com.android.build.gradle.LibraryExtension import com.facebook.react.ReactExtension +import com.facebook.react.utils.ProjectUtils.isEdgeToEdgeEnabled import com.facebook.react.utils.ProjectUtils.isHermesEnabled import com.facebook.react.utils.ProjectUtils.isNewArchEnabled import java.io.File @@ -39,6 +40,10 @@ internal object AgpConfiguratorUtils { project.isNewArchEnabled(extension).toString()) ext.defaultConfig.buildConfigField( "boolean", "IS_HERMES_ENABLED", project.isHermesEnabled.toString()) + ext.defaultConfig.buildConfigField( + "boolean", + "IS_EDGE_TO_EDGE_ENABLED", + project.isEdgeToEdgeEnabled.toString()) } } project.pluginManager.withPlugin("com.android.application", action) diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/ProjectUtils.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/ProjectUtils.kt index 2b1174c2992418..a77c98af228d02 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/ProjectUtils.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/ProjectUtils.kt @@ -11,9 +11,11 @@ import com.facebook.react.ReactExtension import com.facebook.react.model.ModelPackageJson import com.facebook.react.utils.KotlinStdlibCompatUtils.lowercaseCompat import com.facebook.react.utils.KotlinStdlibCompatUtils.toBooleanStrictOrNullCompat +import com.facebook.react.utils.PropertyUtils.EDGE_TO_EDGE_ENABLED import com.facebook.react.utils.PropertyUtils.HERMES_ENABLED import com.facebook.react.utils.PropertyUtils.NEW_ARCH_ENABLED import com.facebook.react.utils.PropertyUtils.REACT_NATIVE_ARCHITECTURES +import com.facebook.react.utils.PropertyUtils.SCOPED_EDGE_TO_EDGE_ENABLED import com.facebook.react.utils.PropertyUtils.SCOPED_HERMES_ENABLED import com.facebook.react.utils.PropertyUtils.SCOPED_NEW_ARCH_ENABLED import com.facebook.react.utils.PropertyUtils.SCOPED_REACT_NATIVE_ARCHITECTURES @@ -59,6 +61,13 @@ internal object ProjectUtils { HERMES_FALLBACK } + internal val Project.isEdgeToEdgeEnabled: Boolean + get() = + (project.hasProperty(EDGE_TO_EDGE_ENABLED) && + project.property(EDGE_TO_EDGE_ENABLED).toString().toBoolean()) || + (project.hasProperty(SCOPED_EDGE_TO_EDGE_ENABLED) && + project.property(SCOPED_EDGE_TO_EDGE_ENABLED).toString().toBoolean()) + internal val Project.useThirdPartyJSC: Boolean get() = (project.hasProperty(USE_THIRD_PARTY_JSC) && diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PropertyUtils.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PropertyUtils.kt index e3c010094650d1..ab42f67508c9d3 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PropertyUtils.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PropertyUtils.kt @@ -14,10 +14,14 @@ 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" + /** Public property that toggles edge-to-edge */ + const val EDGE_TO_EDGE_ENABLED = "edgeToEdgeEnabled" + const val SCOPED_EDGE_TO_EDGE_ENABLED = "react.edgeToEdgeEnabled" + /** Public property that excludes jsctooling from core */ const val USE_THIRD_PARTY_JSC = "useThirdPartyJSC" const val SCOPED_USE_THIRD_PARTY_JSC = "react.useThirdPartyJSC" diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateEntryPointTaskTest.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateEntryPointTaskTest.kt index eaa5f540a75f5a..ea55e19e7eb2ef 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateEntryPointTaskTest.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateEntryPointTaskTest.kt @@ -55,6 +55,7 @@ class GenerateEntryPointTaskTest { import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger; + import com.facebook.react.views.view.WindowUtilKt; import com.facebook.react.soloader.OpenSourceMergedSoMapping; import com.facebook.soloader.SoLoader; @@ -78,6 +79,10 @@ class GenerateEntryPointTaskTest { if (com.facebook.react.BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { DefaultNewArchitectureEntryPoint.load(); } + + if (com.facebook.react.BuildConfig.IS_EDGE_TO_EDGE_ENABLED) { + WindowUtilKt.setEdgeToEdgeFeatureFlagOn(); + } } } """ diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/ProjectUtilsTest.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/ProjectUtilsTest.kt index 7cd48df1ee43f2..8b30877a7b0e79 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/ProjectUtilsTest.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/ProjectUtilsTest.kt @@ -12,6 +12,7 @@ import com.facebook.react.model.ModelCodegenConfig import com.facebook.react.model.ModelPackageJson import com.facebook.react.tests.createProject import com.facebook.react.utils.ProjectUtils.getReactNativeArchitectures +import com.facebook.react.utils.ProjectUtils.isEdgeToEdgeEnabled import com.facebook.react.utils.ProjectUtils.isHermesEnabled import com.facebook.react.utils.ProjectUtils.isNewArchEnabled import com.facebook.react.utils.ProjectUtils.needsCodegenFromPackageJson @@ -98,7 +99,7 @@ class ProjectUtilsTest { } @Test - fun isNewArchEnabled_withDisabledViaProperty_returnsFalse() { + fun isHermesEnabled_withDisabledViaProperty_returnsFalse() { val project = createProject() project.extensions.extraProperties.set("hermesEnabled", "false") assertThat(project.isHermesEnabled).isFalse() @@ -150,6 +151,32 @@ class ProjectUtilsTest { assertThat(project.isHermesEnabled).isTrue() } + @Test + fun isEdgeToEdgeEnabled_returnsFalseByDefault() { + assertThat(createProject().isEdgeToEdgeEnabled).isFalse() + } + + @Test + fun isEdgeToEdgeEnabled_withDisabledViaProperty_returnsFalse() { + val project = createProject() + project.extensions.extraProperties.set("edgeToEdgeEnabled", "false") + assertThat(project.isEdgeToEdgeEnabled).isFalse() + } + + @Test + fun isEdgeToEdgeEnabled_withEnabledViaProperty_returnsTrue() { + val project = createProject() + project.extensions.extraProperties.set("edgeToEdgeEnabled", "true") + assertThat(project.isEdgeToEdgeEnabled).isTrue() + } + + @Test + fun isEdgeToEdgeEnabled_withInvalidViaProperty_returnsFalse() { + val project = createProject() + project.extensions.extraProperties.set("edgeToEdgeEnabled", "¯\\_(ツ)_/¯") + assertThat(project.isEdgeToEdgeEnabled).isFalse() + } + @Test fun needsCodegenFromPackageJson_withCodegenConfigInPackageJson_returnsTrue() { val project = createProject() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index 13892ad72b0479..e16189e739f154 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -23,6 +23,7 @@ import com.facebook.react.interfaces.fabric.ReactSurface; import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags; import com.facebook.react.modules.core.PermissionListener; +import com.facebook.react.views.view.WindowUtilKt; import com.facebook.systrace.Systrace; /** @@ -121,6 +122,9 @@ public void onCreate(Bundle savedInstanceState) { () -> { String mainComponentName = getMainComponentName(); final Bundle launchOptions = composeLaunchOptions(); + if (WindowUtilKt.isEdgeToEdgeFeatureFlagOn() && mActivity != null) { + WindowUtilKt.enableEdgeToEdge(mActivity.getWindow()); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isWideColorGamutEnabled()) { mActivity.getWindow().setColorMode(ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt index df4bd0dd278184..8602e70b283533 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt @@ -16,6 +16,7 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.DisplayMetricsHolder.getDisplayMetricsWritableMap import com.facebook.react.uimanager.DisplayMetricsHolder.initDisplayMetricsIfNotInitialized +import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn /** Module that exposes Android Constants to JS. */ @ReactModule(name = NativeDeviceInfoSpec.NAME) @@ -34,7 +35,11 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) : // Cache the initial dimensions for later comparison in emitUpdateDimensionsEvent previousDisplayMetrics = displayMetrics.copy() - return mapOf("Dimensions" to displayMetrics.toHashMap()) + + return mapOf( + "Dimensions" to displayMetrics.toHashMap(), + "isEdgeToEdge" to isEdgeToEdgeFeatureFlagOn, + ) } override fun onHostResume() { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.kt index bd5fcc8022ddbb..7a96e3d88915e8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.kt @@ -23,6 +23,7 @@ import com.facebook.react.common.ReactConstants import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.DisplayMetricsHolder.getStatusBarHeightPx import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn import com.facebook.react.views.view.setStatusBarTranslucency import com.facebook.react.views.view.setStatusBarVisibility @@ -54,6 +55,12 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) : "StatusBarModule: Ignored status bar change, current activity is null.") return } + if (isEdgeToEdgeFeatureFlagOn) { + FLog.w( + ReactConstants.TAG, + "StatusBarModule: Ignored status bar change, current activity is edge-to-edge.") + return + } UiThreadUtil.runOnUiThread( object : GuardedRunnable(reactApplicationContext) { override fun runGuarded() { @@ -82,6 +89,12 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) : "StatusBarModule: Ignored status bar change, current activity is null.") return } + if (isEdgeToEdgeFeatureFlagOn) { + FLog.w( + ReactConstants.TAG, + "StatusBarModule: Ignored status bar change, current activity is edge-to-edge.") + return + } UiThreadUtil.runOnUiThread( object : GuardedRunnable(reactApplicationContext) { override fun runGuarded() { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt index 8408b83815498c..ae68c1f9bd87a2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt @@ -53,8 +53,10 @@ import com.facebook.react.uimanager.events.EventDispatcher import com.facebook.react.views.common.ContextUtils import com.facebook.react.views.modal.ReactModalHostView.DialogRootViewGroup import com.facebook.react.views.view.ReactViewGroup +import com.facebook.react.views.view.disableEdgeToEdge +import com.facebook.react.views.view.enableEdgeToEdge +import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn import com.facebook.react.views.view.setStatusBarTranslucency -import com.facebook.react.views.view.setSystemBarsTranslucency import com.facebook.yoga.annotations.DoNotStrip /** @@ -83,14 +85,22 @@ public class ReactModalHostView(context: ThemedReactContext) : public var onRequestCloseListener: OnRequestCloseListener? = null public var statusBarTranslucent: Boolean = false set(value) { - field = value - createNewDialog = true + if (isEdgeToEdgeFeatureFlagOn) { + field = true + } else { + field = value + createNewDialog = true + } } public var navigationBarTranslucent: Boolean = false set(value) { - field = value - createNewDialog = true + if (isEdgeToEdgeFeatureFlagOn) { + field = true + } else { + field = value + createNewDialog = true + } } public var animationType: String? = null @@ -378,9 +388,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) } @@ -416,6 +427,13 @@ public class ReactModalHostView(context: ThemedReactContext) : val dialogWindowInsetsController = WindowInsetsControllerCompat(dialogWindow, dialogWindow.decorView) + if (isEdgeToEdgeFeatureFlagOn) { + activityWindowInsetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + dialogWindowInsetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + dialogWindowInsetsController.isAppearanceLightStatusBars = activityWindowInsetsController.isAppearanceLightStatusBars diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt index 680d34893ca947..47ae485e7a35f0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt @@ -13,9 +13,30 @@ import android.view.Window import android.view.WindowManager import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.facebook.react.views.common.UiModeUtils +// The light scrim color used in the platform API 29+ +// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/policy/DecorView.java;drc=6ef0f022c333385dba2c294e35b8de544455bf19;l=142 +internal val LightNavigationBarColor = Color.argb(0xe6, 0xFF, 0xFF, 0xFF) + +// The dark scrim color used in the platform. +// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/res/res/color/system_bar_background_semi_transparent.xml +// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/res/remote_color_resources_res/values/colors.xml;l=67 +internal val DarkNavigationBarColor = Color.argb(0x80, 0x1b, 0x1b, 0x1b) + +/** + * This does not enable or apply edge-to-edge behavior, it simply tracks whether it has been + * flagged as enabled elsewhere in the application. + */ +public var isEdgeToEdgeFeatureFlagOn: Boolean = false + private set + +public fun setEdgeToEdgeFeatureFlagOn() { + isEdgeToEdgeFeatureFlagOn = true +} + @Suppress("DEPRECATION") internal fun Window.setStatusBarTranslucency(isTranslucent: Boolean) { // If the status bar is translucent hook into the window insets calculations @@ -45,59 +66,76 @@ internal fun Window.setStatusBarVisibility(isHidden: Boolean) { @Suppress("DEPRECATION") private fun Window.statusBarHide() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Ensure the content extends into the cutout area - attributes.layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - setDecorFitsSystemWindows(false) + if (isEdgeToEdgeFeatureFlagOn) { + WindowInsetsControllerCompat(this, decorView).run { + systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + hide(WindowInsetsCompat.Type.statusBars()) + } + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Ensure the content extends into the cutout area + attributes.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + setDecorFitsSystemWindows(false) + } + addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) + clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) } - addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) - clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) } @Suppress("DEPRECATION") private fun Window.statusBarShow() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - attributes.layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT - setDecorFitsSystemWindows(true) + if (isEdgeToEdgeFeatureFlagOn) { + WindowInsetsControllerCompat(this, decorView).run { + systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + show(WindowInsetsCompat.Type.statusBars()) + } + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + attributes.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT + setDecorFitsSystemWindows(true) + } + addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) + clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) } - addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) - clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) } @Suppress("DEPRECATION") -internal fun Window.setSystemBarsTranslucency(isTranslucent: Boolean) { - WindowCompat.setDecorFitsSystemWindows(this, !isTranslucent) +internal fun Window.enableEdgeToEdge() { + WindowCompat.setDecorFitsSystemWindows(this, false) - if (isTranslucent) { - val isDarkMode = UiModeUtils.isDarkMode(context) + val isDarkMode = UiModeUtils.isDarkMode(context) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - isStatusBarContrastEnforced = false - isNavigationBarContrastEnforced = true - } + 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) - } + statusBarColor = Color.TRANSPARENT + navigationBarColor = + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Color.TRANSPARENT + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isDarkMode -> LightNavigationBarColor + else -> DarkNavigationBarColor + } - WindowInsetsControllerCompat(this, this.decorView).run { - isAppearanceLightNavigationBars = !isDarkMode - } + WindowInsetsControllerCompat(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 - } - } + 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 + } } } + +internal fun Window.disableEdgeToEdge() { + WindowCompat.setDecorFitsSystemWindows(this, true) +} diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js index adb85f1a782f2c..4ef344afe8ea13 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js @@ -36,6 +36,7 @@ export type DimensionsPayload = { export type DeviceInfoConstants = { +Dimensions: DimensionsPayload, + +isEdgeToEdge?: boolean, +isIPhoneX_deprecated?: boolean, }; diff --git a/packages/rn-tester/android/app/gradle.properties b/packages/rn-tester/android/app/gradle.properties index 378eb16caa07dd..5c17e99d3fa44f 100644 --- a/packages/rn-tester/android/app/gradle.properties +++ b/packages/rn-tester/android/app/gradle.properties @@ -7,3 +7,5 @@ android.useAndroidX=true newArchEnabled=true # RN-Tester is running with Hermes always enabled hermesEnabled=true +# RN-Tester is running with EdgeToEdge always enabled +edgeToEdgeEnabled=true diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt index d3a65bb9d8ec46..dddd3312063df5 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt @@ -33,6 +33,7 @@ import com.facebook.react.uiapp.component.MyNativeViewManager import com.facebook.react.uiapp.component.ReportFullyDrawnViewManager import com.facebook.react.uimanager.ReactShadowNode import com.facebook.react.uimanager.ViewManager +import com.facebook.react.views.view.setEdgeToEdgeFeatureFlagOn import com.facebook.soloader.SoLoader internal class RNTesterApplication : Application(), ReactApplication { @@ -135,5 +136,9 @@ internal class RNTesterApplication : Application(), ReactApplication { if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { load() } + + if (BuildConfig.IS_EDGE_TO_EDGE_ENABLED) { + setEdgeToEdgeFeatureFlagOn() + } } } diff --git a/private/helloworld/android/gradle.properties b/private/helloworld/android/gradle.properties index eeb920d344cf18..0f939574cb16d5 100644 --- a/private/helloworld/android/gradle.properties +++ b/private/helloworld/android/gradle.properties @@ -10,3 +10,4 @@ android.useAndroidX=true reactNativeArchitectures=arm64-v8a newArchEnabled=true hermesEnabled=true +edgeToEdgeEnabled=false From c9d290c65c6adfe8153bfb08806c21292866352a Mon Sep 17 00:00:00 2001 From: Mathieu Acthernoene Date: Mon, 23 Jun 2025 17:41:30 +0200 Subject: [PATCH 2/4] Fix DeviceInfoModule.h constants --- .../ReactCxxPlatform/react/coremodules/DeviceInfoModule.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h b/packages/react-native/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h index 73775536972b80..63b50871e9cdc9 100644 --- a/packages/react-native/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h +++ b/packages/react-native/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h @@ -28,8 +28,10 @@ using DimensionsPayload = NativeDeviceInfoDimensionsPayload< std::optional, std::optional>; -using DeviceInfoConstants = - NativeDeviceInfoDeviceInfoConstants>; +using DeviceInfoConstants = NativeDeviceInfoDeviceInfoConstants< + DimensionsPayload, + std::optional, + std::optional>; template <> struct Bridging From 63a176aa678ea54ba7612d4ff843247c91317819 Mon Sep 17 00:00:00 2001 From: Mathieu Acthernoene Date: Wed, 25 Jun 2025 07:52:37 +0200 Subject: [PATCH 3/4] Fix nullable window lint error --- .../facebook/react/ReactActivityDelegate.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index e16189e739f154..9f54c4b168ba6b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -15,6 +15,7 @@ import android.os.Build; import android.os.Bundle; import android.view.KeyEvent; +import android.view.Window; import androidx.annotation.Nullable; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Callback; @@ -122,11 +123,16 @@ public void onCreate(Bundle savedInstanceState) { () -> { String mainComponentName = getMainComponentName(); final Bundle launchOptions = composeLaunchOptions(); - if (WindowUtilKt.isEdgeToEdgeFeatureFlagOn() && mActivity != null) { - WindowUtilKt.enableEdgeToEdge(mActivity.getWindow()); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isWideColorGamutEnabled()) { - mActivity.getWindow().setColorMode(ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT); + if (mActivity != null) { + Window window = mActivity.getWindow(); + if (window != null) { + if (WindowUtilKt.isEdgeToEdgeFeatureFlagOn()) { + WindowUtilKt.enableEdgeToEdge(window); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isWideColorGamutEnabled()) { + window.setColorMode(ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT); + } + } } if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) { mReactDelegate = From 376f3120682bb5493669fccdc2623f5a686fa4bc Mon Sep 17 00:00:00 2001 From: Mathieu Acthernoene Date: Thu, 26 Jun 2025 09:32:45 +0200 Subject: [PATCH 4/4] Override getter instead of setter --- .../react/views/modal/ReactModalHostView.kt | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt index ae68c1f9bd87a2..42a6b247d38083 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt @@ -83,24 +83,19 @@ public class ReactModalHostView(context: ThemedReactContext) : public var transparent: Boolean = false public var onShowListener: DialogInterface.OnShowListener? = null public var onRequestCloseListener: OnRequestCloseListener? = null + public var statusBarTranslucent: Boolean = false + get() = field || isEdgeToEdgeFeatureFlagOn set(value) { - if (isEdgeToEdgeFeatureFlagOn) { - field = true - } else { - field = value - createNewDialog = true - } + field = value + createNewDialog = !isEdgeToEdgeFeatureFlagOn } public var navigationBarTranslucent: Boolean = false + get() = field || isEdgeToEdgeFeatureFlagOn set(value) { - if (isEdgeToEdgeFeatureFlagOn) { - field = true - } else { - field = value - createNewDialog = true - } + field = value + createNewDialog = !isEdgeToEdgeFeatureFlagOn } public var animationType: String? = null