From cf5b1b88bb11ef67dfbed42f94cec9b84486a30e Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Thu, 19 Jun 2025 03:37:49 -0700 Subject: [PATCH] Fix Modal first frame being rendered on top-left corner (#51048) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51048 Fixes https://github.com/facebook/react-native/issues/50442 Closes https://github.com/facebook/react-native/pull/50704 Users reported that Modals on Android are first renderer anchored in 0,0. That results in them being on the top left corner of the screen for some seconds. This is happening because the native state of the Modal on Android as width/height set at 0,0 - which we then update in a subsequent callback. I'm fixing this by making sure we render the Modal the first time with the right screen size - the status bar size Changelog: [Android] [Fixed] - Fix Modal first frame being rendered on top-left corner Reviewed By: javache Differential Revision: D73948178 --- packages/react-native/Package.swift | 2 + .../modules/statusbar/StatusBarModule.kt | 24 ++---------- .../react/uimanager/DisplayMetricsHolder.kt | 13 +++++++ .../react/views/modal/ReactModalHostView.kt | 25 ++++++++++++ .../React-FabricComponents.podspec | 2 +- .../renderer/components/modal/CMakeLists.txt | 5 ++- .../modal/ModalHostViewComponentDescriptor.h | 5 ++- .../components/modal/ModalHostViewState.h | 16 ++------ .../components/modal/ModalHostViewUtils.h | 4 +- .../components/modal/ModalHostViewUtils.mm | 2 +- .../platform/android/JReactModalHostView.h | 38 +++++++++++++++++++ .../platform/android/ModalHostViewUtils.cpp | 18 +++++++++ .../modal/platform/cxx/ModalHostViewUtils.cpp | 17 +++++++++ 13 files changed, 132 insertions(+), 39 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/components/modal/platform/android/JReactModalHostView.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/modal/platform/android/ModalHostViewUtils.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/components/modal/platform/cxx/ModalHostViewUtils.cpp diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index b4d72ba0dc45..4ca8eefdb9ca 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -416,6 +416,8 @@ let reactFabricComponents = RNTarget( name: .reactFabricComponents, path: "ReactCommon/react/renderer", excludedPaths: [ + "components/modal/platform/android", + "components/modal/platform/cxx", "components/view/platform/android", "components/view/platform/windows", "components/view/platform/macos", 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 07bba9ded311..bd5fcc8022dd 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 @@ -13,8 +13,6 @@ import android.os.Build import android.view.View import android.view.WindowInsetsController import android.view.WindowManager -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.bridge.GuardedRunnable @@ -23,6 +21,7 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.UiThreadUtil 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.setStatusBarTranslucency import com.facebook.react.views.view.setStatusBarVisibility @@ -34,32 +33,17 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) : @Suppress("DEPRECATION") override fun getTypedExportedConstants(): Map { + val currentActivity = reactApplicationContext.currentActivity val statusBarColor = - reactApplicationContext.getCurrentActivity()?.window?.statusBarColor?.let { color -> + currentActivity?.window?.statusBarColor?.let { color -> String.format("#%06X", 0xFFFFFF and color) } ?: "black" return mapOf( - HEIGHT_KEY to PixelUtil.toDIPFromPixel(getStatusBarHeightPx()), + HEIGHT_KEY to PixelUtil.toDIPFromPixel(getStatusBarHeightPx(currentActivity).toFloat()), DEFAULT_BACKGROUND_COLOR_KEY to statusBarColor, ) } - private fun getStatusBarHeightPx(): Float { - val windowInsets = - reactApplicationContext - .getCurrentActivity() - ?.window - ?.decorView - ?.let(ViewCompat::getRootWindowInsets) ?: return 0f - return windowInsets - .getInsets( - WindowInsetsCompat.Type.statusBars() or - WindowInsetsCompat.Type.navigationBars() or - WindowInsetsCompat.Type.displayCutout()) - .top - .toFloat() - } - @Suppress("DEPRECATION") override fun setColor(colorDouble: Double, animated: Boolean) { val color = colorDouble.toInt() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.kt index 0826e95bb2c4..9f6c9310a690 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.kt @@ -7,9 +7,12 @@ package com.facebook.react.uimanager +import android.app.Activity import android.content.Context import android.util.DisplayMetrics import android.view.WindowManager +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeMap @@ -98,4 +101,14 @@ public object DisplayMetricsHolder { putDouble("fontScale", fontScale) putDouble("densityDpi", displayMetrics.densityDpi.toDouble()) } + + internal fun getStatusBarHeightPx(activity: Activity?): Int { + val windowInsets = activity?.window?.decorView?.let(ViewCompat::getRootWindowInsets) ?: return 0 + return windowInsets + .getInsets( + WindowInsetsCompat.Type.statusBars() or + WindowInsetsCompat.Type.navigationBars() or + WindowInsetsCompat.Type.displayCutout()) + .top + } } 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 293ea5c0d88f..7f2798ae02e3 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 @@ -40,6 +40,8 @@ import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.common.annotations.VisibleForTesting import com.facebook.react.common.build.ReactBuildConfig import com.facebook.react.config.ReactFeatureFlags +import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.uimanager.DisplayMetricsHolder.getStatusBarHeightPx import com.facebook.react.uimanager.JSPointerDispatcher import com.facebook.react.uimanager.JSTouchDispatcher import com.facebook.react.uimanager.PixelUtil.pxToDp @@ -53,6 +55,7 @@ import com.facebook.react.views.modal.ReactModalHostView.DialogRootViewGroup import com.facebook.react.views.view.ReactViewGroup import com.facebook.react.views.view.setStatusBarTranslucency import com.facebook.react.views.view.setSystemBarsTranslucency +import com.facebook.yoga.annotations.DoNotStrip /** * ReactModalHostView is a view that sits in the view hierarchy representing a Modal view. @@ -122,6 +125,7 @@ public class ReactModalHostView(context: ThemedReactContext) : private var createNewDialog = false init { + initStatusBarHeight(context) dialogRootViewGroup = DialogRootViewGroup(context) } @@ -461,6 +465,26 @@ public class ReactModalHostView(context: ThemedReactContext) : private companion object { private const val TAG = "ReactModalHost" + + // We store the status bar height to be able to properly position + // the modal on the first render. + private var statusBarHeight = 0 + + private fun initStatusBarHeight(reactContext: ReactContext) { + statusBarHeight = getStatusBarHeightPx(reactContext.currentActivity) + } + + @JvmStatic + @DoNotStrip + private fun getScreenDisplayMetricsWithoutInsets(): Long { + val displayMetrics = DisplayMetricsHolder.getScreenDisplayMetrics() + return encodeFloatsToLong( + displayMetrics.widthPixels.toFloat().pxToDp(), + (displayMetrics.heightPixels - statusBarHeight).toFloat().pxToDp()) + } + + private fun encodeFloatsToLong(width: Float, height: Float): Long = + (width.toRawBits().toLong()) shl 32 or (height.toRawBits().toLong()) } /** @@ -476,6 +500,7 @@ public class ReactModalHostView(context: ThemedReactContext) : */ public class DialogRootViewGroup internal constructor(context: Context) : ReactViewGroup(context), RootView { + internal var stateWrapper: StateWrapper? = null internal var eventDispatcher: EventDispatcher? = null diff --git a/packages/react-native/ReactCommon/React-FabricComponents.podspec b/packages/react-native/ReactCommon/React-FabricComponents.podspec index cf039719e3c6..0b87e5b3b552 100644 --- a/packages/react-native/ReactCommon/React-FabricComponents.podspec +++ b/packages/react-native/ReactCommon/React-FabricComponents.podspec @@ -89,7 +89,7 @@ Pod::Spec.new do |s| end ss.subspec "modal" do |sss| - sss.source_files = "react/renderer/components/modal/**/*.{m,mm,cpp,h}" + sss.source_files = "react/renderer/components/modal/*.{m,mm,cpp,h}" sss.exclude_files = "react/renderer/components/modal/tests" sss.header_dir = "react/renderer/components/modal" end diff --git a/packages/react-native/ReactCommon/react/renderer/components/modal/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/components/modal/CMakeLists.txt index a996222eadce..b11531023a22 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/modal/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/components/modal/CMakeLists.txt @@ -8,7 +8,10 @@ set(CMAKE_VERBOSE_MAKEFILE on) include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) -file(GLOB rrc_modal_SRC CONFIGURE_DEPENDS *.cpp) +file(GLOB rrc_modal_SRC CONFIGURE_DEPENDS + *.cpp + platform/android/*.cpp) + add_library(rrc_modal STATIC ${rrc_modal_SRC}) target_include_directories(rrc_modal PUBLIC ${REACT_COMMON_DIR}) diff --git a/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewComponentDescriptor.h index 9ecdc54de4cf..db62b76165c2 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewComponentDescriptor.h +++ b/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewComponentDescriptor.h @@ -30,8 +30,9 @@ class ModalHostViewComponentDescriptor final *shadowNode.getState()) .getData(); - layoutableShadowNode.setSize( - Size{stateData.screenSize.width, stateData.screenSize.height}); + layoutableShadowNode.setSize(Size{ + .width = stateData.screenSize.width, + .height = stateData.screenSize.height}); layoutableShadowNode.setPositionType(YGPositionTypeAbsolute); ConcreteComponentDescriptor::adopt(shadowNode); diff --git a/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewState.h b/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewState.h index 183188de443b..76879cf7347b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewState.h +++ b/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewState.h @@ -9,15 +9,12 @@ #include #include +#include "ModalHostViewUtils.h" #ifdef RN_SERIALIZABLE_STATE #include #endif -#if defined(__APPLE__) && TARGET_OS_IOS -#include "ModalHostViewUtils.h" -#endif - namespace facebook::react { /* @@ -27,12 +24,7 @@ class ModalHostViewState final { public: using Shared = std::shared_ptr; -#if defined(__APPLE__) && TARGET_OS_IOS - ModalHostViewState() : screenSize(RCTModalHostViewScreenSize()) { -#else - ModalHostViewState(){ -#endif - }; + ModalHostViewState() : screenSize(ModalHostViewScreenSize()) {} ModalHostViewState(Size screenSize_) : screenSize(screenSize_){}; #ifdef RN_SERIALIZABLE_STATE @@ -40,8 +32,8 @@ class ModalHostViewState final { const ModalHostViewState& previousState, folly::dynamic data) : screenSize(Size{ - (Float)data["screenWidth"].getDouble(), - (Float)data["screenHeight"].getDouble()}){}; + .width = (Float)data["screenWidth"].getDouble(), + .height = (Float)data["screenHeight"].getDouble()}){}; #endif const Size screenSize{}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewUtils.h b/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewUtils.h index fc038ad33de5..86b375d850c3 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewUtils.h +++ b/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewUtils.h @@ -7,10 +7,10 @@ #pragma once -#include +#include namespace facebook::react { -Size RCTModalHostViewScreenSize(void); +Size ModalHostViewScreenSize(void); } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewUtils.mm b/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewUtils.mm index d2c3560dca8f..7444e3a76e72 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/components/modal/ModalHostViewUtils.mm @@ -11,7 +11,7 @@ namespace facebook::react { -Size RCTModalHostViewScreenSize(void) +Size ModalHostViewScreenSize(void) { CGSize screenSize = RCTScreenSize(); return {screenSize.width, screenSize.height}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/modal/platform/android/JReactModalHostView.h b/packages/react-native/ReactCommon/react/renderer/components/modal/platform/android/JReactModalHostView.h new file mode 100644 index 000000000000..f4300046116a --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/modal/platform/android/JReactModalHostView.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +class JReactModalHostView + : public facebook::jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = + "Lcom/facebook/react/views/modal/ReactModalHostView;"; + + static Size getDisplayMetrics() { + static auto method = + JReactModalHostView::javaClassStatic()->getStaticMethod( + "getScreenDisplayMetricsWithoutInsets"); + auto result = method(javaClassStatic()); + + // Inspired from yogaMeassureToSize from conversions.h + int32_t wBits = 0xFFFFFFFF & (result >> 32); + int32_t hBits = 0xFFFFFFFF & result; + + auto* measuredWidth = reinterpret_cast(&wBits); + auto* measuredHeight = reinterpret_cast(&hBits); + + return Size{.width = *measuredWidth, .height = *measuredHeight}; + } +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/modal/platform/android/ModalHostViewUtils.cpp b/packages/react-native/ReactCommon/react/renderer/components/modal/platform/android/ModalHostViewUtils.cpp new file mode 100644 index 000000000000..50e2ce89584d --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/modal/platform/android/ModalHostViewUtils.cpp @@ -0,0 +1,18 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include "JReactModalHostView.h" + +namespace facebook::react { + +Size ModalHostViewScreenSize() { + return JReactModalHostView::getDisplayMetrics(); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/modal/platform/cxx/ModalHostViewUtils.cpp b/packages/react-native/ReactCommon/react/renderer/components/modal/platform/cxx/ModalHostViewUtils.cpp new file mode 100644 index 000000000000..a661b73ccfa5 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/modal/platform/cxx/ModalHostViewUtils.cpp @@ -0,0 +1,17 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +namespace facebook::react { + +Size ModalHostViewScreenSize() { + return Size{0, 0}; +} + +} // namespace facebook::react