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
This commit is contained in:
Nicola Corti
2025-07-17 16:58:20 +01:00
committed by Rob Hogan
parent c43952ac22
commit 710d499f13
12 changed files with 129 additions and 35 deletions
@@ -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<String, Any> {
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()
@@ -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
}
}
@@ -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
@@ -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
@@ -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})
@@ -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);
@@ -9,15 +9,12 @@
#include <react/renderer/core/graphicsConversions.h>
#include <react/renderer/graphics/Float.h>
#include "ModalHostViewUtils.h"
#ifdef ANDROID
#include <folly/dynamic.h>
#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<const ModalHostViewState>;
#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{};
@@ -7,10 +7,10 @@
#pragma once
#include <react/renderer/core/graphicsConversions.h>
#include <react/renderer/graphics/Size.h>
namespace facebook::react {
Size RCTModalHostViewScreenSize(void);
Size ModalHostViewScreenSize(void);
} // namespace facebook::react
@@ -11,7 +11,7 @@
namespace facebook::react {
Size RCTModalHostViewScreenSize(void)
Size ModalHostViewScreenSize(void)
{
CGSize screenSize = RCTScreenSize();
return {screenSize.width, screenSize.height};
@@ -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 <fbjni/fbjni.h>
#include <react/renderer/graphics/Size.h>
namespace facebook::react {
class JReactModalHostView
: public facebook::jni::JavaClass<JReactModalHostView> {
public:
static auto constexpr kJavaDescriptor =
"Lcom/facebook/react/views/modal/ReactModalHostView;";
static Size getDisplayMetrics() {
static auto method =
JReactModalHostView::javaClassStatic()->getStaticMethod<jlong()>(
"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<float*>(&wBits);
auto* measuredHeight = reinterpret_cast<float*>(&hBits);
return Size{.width = *measuredWidth, .height = *measuredHeight};
}
};
} // namespace facebook::react
@@ -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 <react/renderer/components/modal/ModalHostViewUtils.h>
#include <react/renderer/graphics/Size.h>
#include "JReactModalHostView.h"
namespace facebook::react {
Size ModalHostViewScreenSize() {
return JReactModalHostView::getDisplayMetrics();
}
} // namespace facebook::react
@@ -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 <react/renderer/components/modal/ModalHostViewUtils.h>
#include <react/renderer/graphics/Size.h>
namespace facebook::react {
Size ModalHostViewScreenSize() {
return Size{0, 0};
}
} // namespace facebook::react