Skip to content

Commit 3b185e4

Browse files
zoontekfacebook-github-bot
authored andcommitted
Fix Dimensions window values on Android < 15 (#52738)
Summary: This PR (initially created for edge-to-edge opt-in support, rebased multiple times) fixes the `Dimensions` API `window` values on Android < 15, when edge-to-edge is enabled. Currently the window height doesn't include the status and navigation bar heights (but it does on Android >= 15): <img width="300" alt="Screenshot 2025-06-27 at 16 23 02" src="https://github.com/user-attachments/assets/c7d11334-9298-4f7f-a75c-590df8cc2d8a" /> Using `WindowMetricsCalculator` from AndroidX: <img width="300" alt="Screenshot 2025-06-27 at 16 34 01" src="https://github.com/user-attachments/assets/7a4e3dc7-a83b-421b-8f6d-fd1344f5fe81" /> Fixes #47080 ## Changelog: [Android] [Fixed] Fix `Dimensions` `window` values on Android < 15 when edge-to-edge is enabled Pull Request resolved: #52738 Test Plan: Run the example app on an Android < 15 device. Rollback Plan: Reviewed By: cipolleschi, Abbondanzo Differential Revision: D78738516 Pulled By: alanleedev fbshipit-source-id: fdb22f3cc76b0bda987db426cb015124bcacdc84
1 parent 8b2e309 commit 3b185e4

11 files changed

Lines changed: 95 additions & 34 deletions

File tree

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3348,8 +3348,10 @@ public final class com/facebook/react/uimanager/DisplayMetricsHolder {
33483348
public static final fun getDisplayMetricsWritableMap (D)Lcom/facebook/react/bridge/WritableMap;
33493349
public static final fun getScreenDisplayMetrics ()Landroid/util/DisplayMetrics;
33503350
public static final fun getWindowDisplayMetrics ()Landroid/util/DisplayMetrics;
3351-
public static final fun initDisplayMetrics (Landroid/content/Context;)V
3352-
public static final fun initDisplayMetricsIfNotInitialized (Landroid/content/Context;)V
3351+
public static final fun initScreenDisplayMetrics (Landroid/content/Context;)V
3352+
public static final fun initScreenDisplayMetricsIfNotInitialized (Landroid/content/Context;)V
3353+
public static final fun initWindowDisplayMetrics (Landroid/content/Context;)V
3354+
public static final fun initWindowDisplayMetricsIfNotInitialized (Landroid/content/Context;)V
33533355
public static final fun setScreenDisplayMetrics (Landroid/util/DisplayMetrics;)V
33543356
public static final fun setWindowDisplayMetrics (Landroid/util/DisplayMetrics;)V
33553357
}

packages/react-native/ReactAndroid/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,7 @@ dependencies {
630630
api(libs.androidx.autofill)
631631
api(libs.androidx.swiperefreshlayout)
632632
api(libs.androidx.tracing)
633+
api(libs.androidx.window)
633634

634635
api(libs.fbjni)
635636
api(libs.fresco)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,11 @@ public static ReactInstanceManagerBuilder builder() {
259259
FLog.d(TAG, "ReactInstanceManager.ctor()");
260260
initializeSoLoaderIfNecessary(applicationContext);
261261

262-
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(applicationContext);
262+
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(applicationContext);
263+
264+
if (currentActivity != null) {
265+
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(currentActivity);
266+
}
263267

264268
// See {@code ReactInstanceManagerBuilder} for description of all flags here.
265269
mApplicationContext = applicationContext;
@@ -924,6 +928,13 @@ public void onConfigurationChanged(Context updatedContext, @Nullable Configurati
924928

925929
ReactContext currentReactContext = getCurrentReactContext();
926930
if (currentReactContext != null) {
931+
DisplayMetricsHolder.initScreenDisplayMetrics(currentReactContext);
932+
Activity currentActivity = currentReactContext.getCurrentActivity();
933+
934+
if (currentActivity != null) {
935+
DisplayMetricsHolder.initWindowDisplayMetrics(currentActivity);
936+
}
937+
927938
AppearanceModule appearanceModule =
928939
currentReactContext.getNativeModule(AppearanceModule.class);
929940

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,8 @@ private void init() {
136136
setRootViewTag(ReactRootViewTagGenerator.getNextRootViewTag());
137137
setClipChildren(false);
138138

139-
if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) {
140-
DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext());
141-
}
139+
DisplayMetricsHolder.initScreenDisplayMetrics(getContext());
140+
DisplayMetricsHolder.initWindowDisplayMetrics(getContext());
142141
}
143142

144143
@Override
@@ -883,7 +882,8 @@ private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLay
883882
private int mDeviceRotation = 0;
884883

885884
/* package */ CustomGlobalLayoutListener() {
886-
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(getContext().getApplicationContext());
885+
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(getContext());
886+
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(getContext());
887887
mVisibleViewArea = new Rect();
888888
mMinKeyboardHeightDetected = (int) PixelUtil.toPixelFromDIP(60);
889889
}
@@ -1006,7 +1006,8 @@ private void checkForDeviceOrientationChanges() {
10061006
return;
10071007
}
10081008
mDeviceRotation = rotation;
1009-
DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext());
1009+
DisplayMetricsHolder.initScreenDisplayMetrics(getContext());
1010+
DisplayMetricsHolder.initWindowDisplayMetrics(getContext());
10101011
emitOrientationChanged(rotation);
10111012
}
10121013

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import com.facebook.react.bridge.ReactSoftExceptionLogger
1515
import com.facebook.react.bridge.ReadableMap
1616
import com.facebook.react.module.annotations.ReactModule
1717
import com.facebook.react.uimanager.DisplayMetricsHolder.getDisplayMetricsWritableMap
18-
import com.facebook.react.uimanager.DisplayMetricsHolder.initDisplayMetricsIfNotInitialized
18+
import com.facebook.react.uimanager.DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized
19+
import com.facebook.react.uimanager.DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized
1920
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn
2021

2122
/** Module that exposes Android Constants to JS. */
@@ -26,7 +27,8 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) :
2627
private var previousDisplayMetrics: ReadableMap? = null
2728

2829
init {
29-
initDisplayMetricsIfNotInitialized(reactContext)
30+
initScreenDisplayMetricsIfNotInitialized(reactContext)
31+
reactContext.currentActivity?.let { initWindowDisplayMetricsIfNotInitialized(it) }
3032
reactContext.addLifecycleEventListener(this)
3133
}
3234

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -625,9 +625,8 @@ public class ReactHostImpl(
625625
override fun onConfigurationChanged(context: Context) {
626626
val currentReactContext = this.currentReactContext
627627
if (currentReactContext != null) {
628-
if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) {
629-
DisplayMetricsHolder.initDisplayMetrics(currentReactContext)
630-
}
628+
DisplayMetricsHolder.initScreenDisplayMetrics(currentReactContext)
629+
currentReactContext.currentActivity?.let { DisplayMetricsHolder.initWindowDisplayMetrics(it) }
631630

632631
val appearanceModule = currentReactContext.getNativeModule(AppearanceModule::class.java)
633632
appearanceModule?.onConfigurationChanged(context)
@@ -918,6 +917,7 @@ public class ReactHostImpl(
918917
val instance =
919918
ReactInstance(
920919
reactContext,
920+
currentActivity,
921921
reactHostDelegate,
922922
componentFactory,
923923
devSupportManager,

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
package com.facebook.react.runtime
99

10+
import android.app.Activity
1011
import android.content.res.AssetManager
1112
import android.view.View
1213
import com.facebook.common.logging.FLog
@@ -88,6 +89,7 @@ import kotlin.jvm.JvmStatic
8889
@UnstableReactNativeAPI
8990
internal class ReactInstance(
9091
private val context: BridgelessReactContext,
92+
private val activity: Activity?,
9193
delegate: ReactHostDelegate,
9294
componentFactory: ComponentFactory,
9395
devSupportManager: DevSupportManager,
@@ -240,7 +242,8 @@ internal class ReactInstance(
240242
FabricUIManager(context, ViewManagerRegistry(viewManagerResolver), eventBeatManager)
241243

242244
// Misc initialization that needs to be done before Fabric init
243-
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(context)
245+
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(context)
246+
activity?.let { DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(it) }
244247

245248
val binding = FabricUIManagerBinding()
246249
binding.register(

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.kt

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,28 @@ import android.util.DisplayMetrics
1313
import android.view.WindowManager
1414
import androidx.core.view.ViewCompat
1515
import androidx.core.view.WindowInsetsCompat
16+
import androidx.window.layout.WindowMetricsCalculator
1617
import com.facebook.react.bridge.WritableMap
1718
import com.facebook.react.bridge.WritableNativeMap
19+
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn
1820

1921
/**
2022
* Holds an instance of the current DisplayMetrics so we don't have to thread it through all the
2123
* classes that need it.
2224
*/
2325
public object DisplayMetricsHolder {
24-
private const val INITIALIZATION_MISSING_MESSAGE =
25-
"DisplayMetricsHolder must be initialized with initDisplayMetricsIfNotInitialized or initDisplayMetrics"
26+
private const val SCREEN_INITIALIZATION_MISSING_MESSAGE =
27+
"DisplayMetricsHolder must be initialized with initScreenDisplayMetricsIfNotInitialized or initScreenDisplayMetrics"
28+
private const val WINDOW_INITIALIZATION_MISSING_MESSAGE =
29+
"DisplayMetricsHolder must be initialized with initWindowDisplayMetricsIfNotInitialized or initWindowDisplayMetrics"
2630

2731
@JvmStatic private var windowDisplayMetrics: DisplayMetrics? = null
2832
@JvmStatic private var screenDisplayMetrics: DisplayMetrics? = null
2933

3034
/** The metrics of the window associated to the Context used to initialize ReactNative */
3135
@JvmStatic
3236
public fun getWindowDisplayMetrics(): DisplayMetrics {
33-
checkNotNull(windowDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
37+
checkNotNull(windowDisplayMetrics) { WINDOW_INITIALIZATION_MISSING_MESSAGE }
3438
return windowDisplayMetrics as DisplayMetrics
3539
}
3640

@@ -42,7 +46,7 @@ public object DisplayMetricsHolder {
4246
/** Screen metrics returns the metrics of the default screen on the device. */
4347
@JvmStatic
4448
public fun getScreenDisplayMetrics(): DisplayMetrics {
45-
checkNotNull(screenDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
49+
checkNotNull(screenDisplayMetrics) { SCREEN_INITIALIZATION_MISSING_MESSAGE }
4650
return screenDisplayMetrics as DisplayMetrics
4751
}
4852

@@ -52,33 +56,58 @@ public object DisplayMetricsHolder {
5256
}
5357

5458
@JvmStatic
55-
public fun initDisplayMetricsIfNotInitialized(context: Context) {
56-
if (screenDisplayMetrics != null) {
57-
return
59+
public fun initScreenDisplayMetricsIfNotInitialized(context: Context) {
60+
if (screenDisplayMetrics == null) {
61+
initScreenDisplayMetrics(context)
5862
}
59-
initDisplayMetrics(context)
6063
}
6164

6265
@JvmStatic
63-
public fun initDisplayMetrics(context: Context) {
64-
val displayMetrics = context.resources.displayMetrics
65-
windowDisplayMetrics = displayMetrics
66-
val screenDisplayMetrics = DisplayMetrics()
67-
screenDisplayMetrics.setTo(displayMetrics)
66+
public fun initWindowDisplayMetricsIfNotInitialized(context: Context) {
67+
if (windowDisplayMetrics == null) {
68+
initWindowDisplayMetrics(context)
69+
}
70+
}
71+
72+
@JvmStatic
73+
public fun initScreenDisplayMetrics(context: Context) {
74+
val displayMetrics = DisplayMetrics()
75+
displayMetrics.setTo(context.resources.displayMetrics)
76+
6877
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
6978
// Get the real display metrics if we are using API level 17 or higher.
7079
// The real metrics include system decor elements (e.g. soft menu bar).
7180
//
7281
// See:
7382
// http://developer.android.com/reference/android/view/Display.html#getRealMetrics(android.util.DisplayMetrics)
74-
@Suppress("DEPRECATION") wm.defaultDisplay.getRealMetrics(screenDisplayMetrics)
75-
DisplayMetricsHolder.screenDisplayMetrics = screenDisplayMetrics
83+
@Suppress("DEPRECATION") wm.defaultDisplay.getRealMetrics(displayMetrics)
84+
screenDisplayMetrics = displayMetrics
85+
}
86+
87+
/*
88+
* NOTE: Unlike [initScreenDisplayMetrics], this method needs a UiContext (Activity of
89+
* InputMethodService) else WindowMetircsCalculator will throw an exception.
90+
*/
91+
@JvmStatic
92+
public fun initWindowDisplayMetrics(context: Context) {
93+
val displayMetrics = DisplayMetrics()
94+
displayMetrics.setTo(context.resources.displayMetrics)
95+
96+
if (isEdgeToEdgeFeatureFlagOn) {
97+
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context).let { windowMetrics
98+
->
99+
displayMetrics.widthPixels = windowMetrics.bounds.width()
100+
displayMetrics.heightPixels = windowMetrics.bounds.height()
101+
}
102+
}
103+
104+
windowDisplayMetrics = displayMetrics
76105
}
77106

78107
@JvmStatic
79108
public fun getDisplayMetricsWritableMap(fontScale: Double): WritableMap {
80-
checkNotNull(windowDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
81-
checkNotNull(screenDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
109+
checkNotNull(windowDisplayMetrics) { WINDOW_INITIALIZATION_MISSING_MESSAGE }
110+
checkNotNull(screenDisplayMetrics) { SCREEN_INITIALIZATION_MISSING_MESSAGE }
82111

83112
return WritableNativeMap().apply {
84113
putMap(

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import static com.facebook.react.uimanager.common.UIManagerType.FABRIC;
1313
import static com.facebook.react.uimanager.common.UIManagerType.LEGACY;
1414

15+
import android.app.Activity;
1516
import android.content.ComponentCallbacks2;
1617
import android.content.res.Configuration;
1718
import android.view.View;
@@ -126,7 +127,11 @@ public UIManagerModule(
126127
ViewManagerResolver viewManagerResolver,
127128
int minTimeLeftInFrameForNonBatchedOperationMs) {
128129
super(reactContext);
129-
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(reactContext);
130+
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(reactContext);
131+
Activity currentActivity = reactContext.getCurrentActivity();
132+
if (currentActivity != null) {
133+
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(currentActivity);
134+
}
130135
mEventDispatcher = new EventDispatcherImpl(reactContext);
131136
mModuleConstants = createConstants(viewManagerResolver);
132137
mCustomDirectEvents = UIManagerModuleConstants.directEventTypeConstants;
@@ -146,7 +151,11 @@ public UIManagerModule(
146151
List<ViewManager> viewManagersList,
147152
int minTimeLeftInFrameForNonBatchedOperationMs) {
148153
super(reactContext);
149-
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(reactContext);
154+
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(reactContext);
155+
Activity currentActivity = reactContext.getCurrentActivity();
156+
if (currentActivity != null) {
157+
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(currentActivity);
158+
}
150159
mEventDispatcher = new EventDispatcherImpl(reactContext);
151160
mCustomDirectEvents = MapBuilder.newHashMap();
152161
mModuleConstants = createConstants(viewManagersList, null, mCustomDirectEvents);

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/RootViewTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ class RootViewTest {
7171
reactContext = spy(BridgeReactContext(RuntimeEnvironment.getApplication()))
7272
reactContext.initializeWithInstance(catalystInstanceMock)
7373

74-
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(reactContext)
74+
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(reactContext)
75+
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(reactContext)
7576
val uiManagerModuleMock: UIManagerModule = mock()
7677
whenever(catalystInstanceMock.getNativeModule(UIManagerModule::class.java))
7778
.thenReturn(uiManagerModuleMock)

0 commit comments

Comments
 (0)