Skip to content

Commit 6bca7b9

Browse files
committed
Fix Dimensions window values on Android < 15
1 parent c2280e3 commit 6bca7b9

9 files changed

Lines changed: 131 additions & 111 deletions

File tree

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3277,13 +3277,10 @@ public abstract class com/facebook/react/uimanager/BaseViewManagerDelegate : com
32773277

32783278
public final class com/facebook/react/uimanager/DisplayMetricsHolder {
32793279
public static final field INSTANCE Lcom/facebook/react/uimanager/DisplayMetricsHolder;
3280-
public static final fun getDisplayMetricsWritableMap (D)Lcom/facebook/react/bridge/WritableMap;
32813280
public static final fun getScreenDisplayMetrics ()Landroid/util/DisplayMetrics;
3282-
public static final fun getWindowDisplayMetrics ()Landroid/util/DisplayMetrics;
32833281
public static final fun initDisplayMetrics (Landroid/content/Context;)V
32843282
public static final fun initDisplayMetricsIfNotInitialized (Landroid/content/Context;)V
32853283
public static final fun setScreenDisplayMetrics (Landroid/util/DisplayMetrics;)V
3286-
public static final fun setWindowDisplayMetrics (Landroid/util/DisplayMetrics;)V
32873284
}
32883285

32893286
public final class com/facebook/react/uimanager/FloatUtil {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ dependencies {
699699
api(libs.androidx.autofill)
700700
api(libs.androidx.swiperefreshlayout)
701701
api(libs.androidx.tracing)
702+
api(libs.androidx.window)
702703

703704
api(libs.fbjni)
704705
api(libs.fresco)

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import static com.facebook.systrace.Systrace.TRACE_TAG_REACT;
1515

1616
import android.annotation.SuppressLint;
17+
import android.app.Activity;
1718
import android.content.Context;
1819
import android.graphics.BlendMode;
1920
import android.graphics.Canvas;
@@ -37,6 +38,9 @@
3738
import android.widget.FrameLayout;
3839
import androidx.annotation.Nullable;
3940
import androidx.annotation.RequiresApi;
41+
import androidx.core.view.ViewCompat;
42+
import androidx.core.view.WindowInsetsCompat;
43+
import androidx.window.layout.WindowMetricsCalculator;
4044
import com.facebook.common.logging.FLog;
4145
import com.facebook.infer.annotation.Assertions;
4246
import com.facebook.infer.annotation.ThreadConfined;
@@ -72,6 +76,7 @@
7276
import com.facebook.react.uimanager.common.UIManagerType;
7377
import com.facebook.react.uimanager.common.ViewUtil;
7478
import com.facebook.react.uimanager.events.EventDispatcher;
79+
import com.facebook.react.views.view.WindowUtilKt;
7580
import com.facebook.systrace.Systrace;
7681
import java.util.concurrent.atomic.AtomicInteger;
7782

@@ -1020,10 +1025,28 @@ private void checkForKeyboardEventsLegacy() {
10201025
}
10211026
}
10221027
}
1023-
final int heightDiff =
1024-
DisplayMetricsHolder.getWindowDisplayMetrics().heightPixels
1025-
- mVisibleViewArea.bottom
1026-
+ notchHeight;
1028+
1029+
int heightPixels = getContext().getResources().getDisplayMetrics().heightPixels;
1030+
final ReactContext reactContext = getCurrentReactContext();
1031+
final Activity activity = reactContext != null ? reactContext.getCurrentActivity() : null;
1032+
1033+
if (activity != null) {
1034+
heightPixels = WindowMetricsCalculator.getOrCreate()
1035+
.computeCurrentWindowMetrics(activity).getBounds().height();
1036+
1037+
if (!WindowUtilKt.isEdgeToEdgeFeatureFlagOn()) {
1038+
WindowInsetsCompat rootWindowInsets =
1039+
ViewCompat.getRootWindowInsets(activity.getWindow().getDecorView());
1040+
1041+
if (rootWindowInsets != null) {
1042+
androidx.core.graphics.Insets insets =
1043+
rootWindowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
1044+
heightPixels -= (insets.top + insets.bottom);
1045+
}
1046+
}
1047+
}
1048+
1049+
final int heightDiff = heightPixels - mVisibleViewArea.bottom + notchHeight;
10271050

10281051
boolean isKeyboardShowingOrKeyboardHeightChanged =
10291052
mKeyboardHeight != heightDiff && heightDiff > mMinKeyboardHeightDetected;

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

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@
77

88
package com.facebook.react.modules.deviceinfo
99

10+
import android.util.DisplayMetrics
11+
import androidx.core.view.ViewCompat
12+
import androidx.core.view.WindowInsetsCompat
13+
import androidx.window.layout.WindowMetricsCalculator
1014
import com.facebook.fbreact.specs.NativeDeviceInfoSpec
1115
import com.facebook.react.bridge.LifecycleEventListener
1216
import com.facebook.react.bridge.ReactApplicationContext
1317
import com.facebook.react.bridge.ReactNoCrashSoftException
1418
import com.facebook.react.bridge.ReactSoftExceptionLogger
1519
import com.facebook.react.bridge.ReadableMap
20+
import com.facebook.react.bridge.WritableMap
21+
import com.facebook.react.bridge.WritableNativeMap
1622
import com.facebook.react.module.annotations.ReactModule
17-
import com.facebook.react.uimanager.DisplayMetricsHolder.getDisplayMetricsWritableMap
23+
import com.facebook.react.uimanager.DisplayMetricsHolder.getScreenDisplayMetrics
1824
import com.facebook.react.uimanager.DisplayMetricsHolder.initDisplayMetricsIfNotInitialized
1925
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn
2026

@@ -30,8 +36,55 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) :
3036
reactContext.addLifecycleEventListener(this)
3137
}
3238

39+
private fun getWindowDisplayMetrics(): DisplayMetrics {
40+
val windowDisplayMetrics = DisplayMetrics()
41+
windowDisplayMetrics.setTo(reactApplicationContext.resources.displayMetrics)
42+
43+
val activity = reactApplicationContext.currentActivity ?: return windowDisplayMetrics
44+
val bounds = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity).bounds
45+
var width = bounds.width()
46+
var height = bounds.height()
47+
48+
// WindowMetrics bounds include system bars. When edge-to-edge is not enabled, we subtract them
49+
// so that window dimensions reflect the usable content area.
50+
if (!isEdgeToEdgeFeatureFlagOn) {
51+
ViewCompat.getRootWindowInsets(activity.window.decorView)?.let {
52+
val insets = it.getInsets(WindowInsetsCompat.Type.systemBars())
53+
width -= (insets.left + insets.right)
54+
height -= (insets.top + insets.bottom)
55+
}
56+
}
57+
58+
windowDisplayMetrics.widthPixels = width
59+
windowDisplayMetrics.heightPixels = height
60+
return windowDisplayMetrics
61+
}
62+
63+
fun getDisplayMetricsWritableMap(): WritableMap =
64+
WritableNativeMap().apply {
65+
putMap(
66+
"windowPhysicalPixels",
67+
getPhysicalPixelsWritableMap(getWindowDisplayMetrics()),
68+
)
69+
putMap(
70+
"screenPhysicalPixels",
71+
getPhysicalPixelsWritableMap(getScreenDisplayMetrics()),
72+
)
73+
}
74+
75+
private fun getPhysicalPixelsWritableMap(
76+
displayMetrics: DisplayMetrics,
77+
): WritableMap =
78+
WritableNativeMap().apply {
79+
putInt("width", displayMetrics.widthPixels)
80+
putInt("height", displayMetrics.heightPixels)
81+
putDouble("scale", displayMetrics.density.toDouble())
82+
putDouble("fontScale", fontScale.toDouble())
83+
putDouble("densityDpi", displayMetrics.densityDpi.toDouble())
84+
}
85+
3386
public override fun getTypedExportedConstants(): Map<String, Any> {
34-
val displayMetrics = getDisplayMetricsWritableMap(fontScale.toDouble())
87+
val displayMetrics = getDisplayMetricsWritableMap()
3588

3689
// Cache the initial dimensions for later comparison in emitUpdateDimensionsEvent
3790
previousDisplayMetrics = displayMetrics.copy()
@@ -58,7 +111,7 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) :
58111
reactApplicationContext.let { context ->
59112
if (context.hasActiveReactInstance()) {
60113
// Don't emit an event to JS if the dimensions haven't changed
61-
val displayMetrics = getDisplayMetricsWritableMap(fontScale.toDouble())
114+
val displayMetrics = getDisplayMetricsWritableMap()
62115
if (previousDisplayMetrics == null) {
63116
previousDisplayMetrics = displayMetrics.copy()
64117
} else if (displayMetrics != previousDisplayMetrics) {

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

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import android.util.DisplayMetrics
1414
import android.view.WindowManager
1515
import androidx.core.view.ViewCompat
1616
import androidx.core.view.WindowInsetsCompat
17-
import com.facebook.react.bridge.WritableMap
18-
import com.facebook.react.bridge.WritableNativeMap
1917
import com.facebook.react.uimanager.PixelUtil.pxToDp
2018

2119
/**
@@ -26,21 +24,8 @@ public object DisplayMetricsHolder {
2624
private const val INITIALIZATION_MISSING_MESSAGE =
2725
"DisplayMetricsHolder must be initialized with initDisplayMetricsIfNotInitialized or initDisplayMetrics"
2826

29-
@JvmStatic private var windowDisplayMetrics: DisplayMetrics? = null
3027
@JvmStatic private var screenDisplayMetrics: DisplayMetrics? = null
3128

32-
/** The metrics of the window associated to the Context used to initialize ReactNative */
33-
@JvmStatic
34-
public fun getWindowDisplayMetrics(): DisplayMetrics {
35-
checkNotNull(windowDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
36-
return windowDisplayMetrics as DisplayMetrics
37-
}
38-
39-
@JvmStatic
40-
public fun setWindowDisplayMetrics(displayMetrics: DisplayMetrics?) {
41-
windowDisplayMetrics = displayMetrics
42-
}
43-
4429
/** Screen metrics returns the metrics of the default screen on the device. */
4530
@JvmStatic
4631
public fun getScreenDisplayMetrics(): DisplayMetrics {
@@ -62,11 +47,10 @@ public object DisplayMetricsHolder {
6247
}
6348

6449
@JvmStatic
65-
@SuppressLint("DeprecatedMethod") // for Andriod Lint
50+
@SuppressLint("DeprecatedMethod") // for Android Lint
6651
@Suppress("DEPRECATION") // for Kotlin compiler
6752
public fun initDisplayMetrics(context: Context) {
6853
val displayMetrics = context.resources.displayMetrics
69-
windowDisplayMetrics = displayMetrics
7054
val screenDisplayMetrics = DisplayMetrics()
7155
screenDisplayMetrics.setTo(displayMetrics)
7256
try {
@@ -84,35 +68,6 @@ public object DisplayMetricsHolder {
8468
DisplayMetricsHolder.screenDisplayMetrics = screenDisplayMetrics
8569
}
8670

87-
@JvmStatic
88-
public fun getDisplayMetricsWritableMap(fontScale: Double): WritableMap {
89-
checkNotNull(windowDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
90-
checkNotNull(screenDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
91-
92-
return WritableNativeMap().apply {
93-
putMap(
94-
"windowPhysicalPixels",
95-
getPhysicalPixelsWritableMap(windowDisplayMetrics as DisplayMetrics, fontScale),
96-
)
97-
putMap(
98-
"screenPhysicalPixels",
99-
getPhysicalPixelsWritableMap(screenDisplayMetrics as DisplayMetrics, fontScale),
100-
)
101-
}
102-
}
103-
104-
private fun getPhysicalPixelsWritableMap(
105-
displayMetrics: DisplayMetrics,
106-
fontScale: Double,
107-
): WritableMap =
108-
WritableNativeMap().apply {
109-
putInt("width", displayMetrics.widthPixels)
110-
putInt("height", displayMetrics.heightPixels)
111-
putDouble("scale", displayMetrics.density.toDouble())
112-
putDouble("fontScale", fontScale)
113-
putDouble("densityDpi", displayMetrics.densityDpi.toDouble())
114-
}
115-
11671
internal fun getStatusBarHeightPx(activity: Activity?): Int {
11772
val windowInsets = activity?.window?.decorView?.let(ViewCompat::getRootWindowInsets) ?: return 0
11873
return windowInsets

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/deviceinfo/DeviceInfoModuleTest.kt

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@
99

1010
package com.facebook.react.modules.deviceinfo
1111

12+
import android.util.DisplayMetrics
1213
import com.facebook.react.bridge.BridgeReactContext
1314
import com.facebook.react.bridge.JavaOnlyMap
1415
import com.facebook.react.bridge.ReactContext
1516
import com.facebook.react.bridge.ReactTestHelper
1617
import com.facebook.react.bridge.WritableMap
1718
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
1819
import com.facebook.react.uimanager.DisplayMetricsHolder
20+
import com.facebook.testutils.shadows.ShadowNativeLoader
21+
import com.facebook.testutils.shadows.ShadowNativeMap
22+
import com.facebook.testutils.shadows.ShadowReadableNativeMap
23+
import com.facebook.testutils.shadows.ShadowSoLoader
24+
import com.facebook.testutils.shadows.ShadowWritableNativeMap
1925
import junit.framework.TestCase
20-
import org.assertj.core.api.Assertions
26+
import org.assertj.core.api.Assertions.assertThat
2127
import org.junit.After
2228
import org.junit.Before
2329
import org.junit.Test
@@ -26,13 +32,26 @@ import org.mockito.ArgumentCaptor
2632
import org.mockito.ArgumentMatchers
2733
import org.mockito.MockedStatic
2834
import org.mockito.Mockito.mockStatic
35+
import org.mockito.kotlin.doReturn
2936
import org.mockito.kotlin.spy
3037
import org.mockito.kotlin.times
3138
import org.mockito.kotlin.verify
39+
import org.mockito.kotlin.whenever
3240
import org.robolectric.RobolectricTestRunner
3341
import org.robolectric.RuntimeEnvironment
42+
import org.robolectric.annotation.Config
3443

3544
@RunWith(RobolectricTestRunner::class)
45+
@Config(
46+
shadows =
47+
[
48+
ShadowSoLoader::class,
49+
ShadowNativeLoader::class,
50+
ShadowNativeMap::class,
51+
ShadowWritableNativeMap::class,
52+
ShadowReadableNativeMap::class,
53+
]
54+
)
3655
class DeviceInfoModuleTest : TestCase() {
3756

3857
private lateinit var deviceInfoModule: DeviceInfoModule
@@ -55,7 +74,7 @@ class DeviceInfoModuleTest : TestCase() {
5574
reactContext = spy(BridgeReactContext(RuntimeEnvironment.getApplication()))
5675
val catalystInstanceMock = ReactTestHelper.createMockCatalystInstance()
5776
reactContext.initializeWithInstance(catalystInstanceMock)
58-
deviceInfoModule = DeviceInfoModule(reactContext)
77+
deviceInfoModule = spy(DeviceInfoModule(reactContext))
5978
}
6079

6180
@After
@@ -110,10 +129,29 @@ class DeviceInfoModuleTest : TestCase() {
110129
)
111130
}
112131

113-
private fun givenDisplayMetricsHolderContains(fakeDisplayMetrics: WritableMap?) {
132+
@Test
133+
fun getDisplayMetricsWritableMap_returnsCorrectMap() {
114134
displayMetricsHolder
115-
.`when`<WritableMap> { DisplayMetricsHolder.getDisplayMetricsWritableMap(1.0) }
116-
.thenAnswer { fakeDisplayMetrics }
135+
.`when`<DisplayMetrics> { DisplayMetricsHolder.getScreenDisplayMetrics() }
136+
.thenAnswer { reactContext.resources.displayMetrics }
137+
138+
// Use the official initialization method to ensure both metrics are set
139+
val map: WritableMap = deviceInfoModule.getDisplayMetricsWritableMap()
140+
assertThat(map.hasKey("windowPhysicalPixels")).isTrue()
141+
assertThat(map.hasKey("screenPhysicalPixels")).isTrue()
142+
val windowMap = map.getMap("windowPhysicalPixels")
143+
val screenMap = map.getMap("screenPhysicalPixels")
144+
checkNotNull(windowMap)
145+
checkNotNull(screenMap)
146+
assertThat(windowMap.hasKey("width")).isTrue()
147+
assertThat(windowMap.hasKey("height")).isTrue()
148+
assertThat(windowMap.hasKey("scale")).isTrue()
149+
assertThat(windowMap.hasKey("fontScale")).isTrue()
150+
assertThat(windowMap.hasKey("densityDpi")).isTrue()
151+
}
152+
153+
private fun givenDisplayMetricsHolderContains(fakeDisplayMetrics: WritableMap?) {
154+
doReturn(fakeDisplayMetrics).whenever(deviceInfoModule).getDisplayMetricsWritableMap()
117155
}
118156

119157
companion object {
@@ -126,7 +164,7 @@ class DeviceInfoModuleTest : TestCase() {
126164
verify(context, times(expectedEventList.size))
127165
?.emitDeviceEvent(ArgumentMatchers.eq("didUpdateDimensions"), captor.capture())
128166
val actualEvents = captor.allValues
129-
Assertions.assertThat(actualEvents).isEqualTo(expectedEventList)
167+
assertThat(actualEvents).isEqualTo(expectedEventList)
130168
}
131169
}
132170
}

0 commit comments

Comments
 (0)