From 710d499f132fc03b4a0e010408558229de419043 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Thu, 19 Jun 2025 06:21:16 -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 fbshipit-source-id: 055c12aa62d70acc1e4c5a2a5c4ea0c5608e22c7 --- .../modules/statusbar/StatusBarModule.kt | 19 ++-------- .../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 +++++++++ 12 files changed, 129 insertions(+), 35 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/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 c5547ca8d3a5..ca491603af6f 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,29 +33,17 @@ public class StatusBarModule(reactContext: ReactApplicationContext?) : @Suppress("DEPRECATION") override fun getTypedExportedConstants(): Map { + val currentActivity = reactApplicationContext.currentActivity val statusBarColor = 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, ) } - @Suppress("DEPRECATION") - private fun getStatusBarHeightPx(): Float { - val windowInsets = - currentActivity?.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 d23014cb04bf..75573be1aadd 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 @@ -99,4 +102,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 0894abf74e1f..46e13042b103 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 @@ -37,6 +37,8 @@ import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.common.ReactConstants import com.facebook.react.common.annotations.VisibleForTesting 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 @@ -49,6 +51,7 @@ 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.yoga.annotations.DoNotStrip import java.util.Objects /** @@ -119,6 +122,7 @@ public class ReactModalHostView(context: ThemedReactContext) : private var createNewDialog = false init { + initStatusBarHeight(context) dialogRootViewGroup = DialogRootViewGroup(context) } @@ -409,6 +413,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()) } /** @@ -424,6 +448,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 d8ff12b5d411..b7bd1a2eb114 100644 --- a/packages/react-native/ReactCommon/React-FabricComponents.podspec +++ b/packages/react-native/ReactCommon/React-FabricComponents.podspec @@ -112,7 +112,7 @@ Pod::Spec.new do |s| ss.subspec "modal" do |sss| sss.dependency folly_dep_name, folly_version sss.compiler_flags = folly_compiler_flags - 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 dd1a291061ba..a84728b6b281 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/modal/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/components/modal/CMakeLists.txt @@ -14,7 +14,10 @@ add_compile_options( -Wpedantic -DLOG_TAG=\"Fabric\") -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 2b713926dd5e..75e305d1fb17 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 ANDROID #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 ANDROID @@ -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