Skip to content

Commit db7566e

Browse files
Bruno Ramosfacebook-github-bot
authored andcommitted
Measure text at the window's density on Android (#59087)
Summary: `DisplayMetricsHolder.initDisplayMetrics()` takes `density` and `densityDpi` from `Display.getRealMetrics()`, the physical display, but `scaledDensity` from the window's configuration. When a window renders at a different density than its display, text is measured at one density and laid out at the other, so it comes out clipped or oversized. With the new `useWindowDensityForDisplayMetricsAndroid` feature flag on, the screen metrics keep the window's `density` and `densityDpi` and take only the screen size from the display. The flag is off by default, so nothing changes for apps that don't turn it on. `initDisplayMetrics` reads the flag only when the window's density differs from the display's, so in the usual case it doesn't touch feature flags, and apps can still set their flags after calling it. **How to use:** return `true` from your app's feature flag overrides, before React Native starts: ``` override fun useWindowDensityForDisplayMetricsAndroid(): Boolean = true ``` Changelog: [Internal] Reviewed By: javache Differential Revision: D123975873
1 parent c89f412 commit db7566e

23 files changed

Lines changed: 208 additions & 24 deletions

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<eb0bafceba60cb439a4be3d68b890725>>
7+
* @generated SignedSource<<cfa7aa53731059247d858fd8fb6e914b>>
88
*/
99

1010
/**
@@ -564,6 +564,12 @@ public object ReactNativeFeatureFlags {
564564
@JvmStatic
565565
public fun useTurboModuleInterop(): Boolean = accessor.useTurboModuleInterop()
566566

567+
/**
568+
* Keeps the window density in the screen display metrics instead of the physical display density, so text is measured at the density the window renders at.
569+
*/
570+
@JvmStatic
571+
public fun useWindowDensityForDisplayMetricsAndroid(): Boolean = accessor.useWindowDensityForDisplayMetricsAndroid()
572+
567573
/**
568574
* Outset the culling context frame with the provided ratio. The culling context frame size will be outset by width * ratio on the left and right, and height * ratio on the top and bottom.
569575
*/

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<0107593110db613063b6f4ceb50d8e58>>
7+
* @generated SignedSource<<71d0554dc45df7d5365f81b7ec8c3d5b>>
88
*/
99

1010
/**
@@ -109,6 +109,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
109109
private var useSharedAnimatedBackendCache: Boolean? = null
110110
private var useTraitHiddenOnAndroidCache: Boolean? = null
111111
private var useTurboModuleInteropCache: Boolean? = null
112+
private var useWindowDensityForDisplayMetricsAndroidCache: Boolean? = null
112113
private var viewCullingOutsetRatioCache: Double? = null
113114
private var viewTransitionEnabledCache: Boolean? = null
114115
private var viewTransitionUseHardwareBitmapAndroidCache: Boolean? = null
@@ -915,6 +916,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
915916
return cached
916917
}
917918

919+
override fun useWindowDensityForDisplayMetricsAndroid(): Boolean {
920+
var cached = useWindowDensityForDisplayMetricsAndroidCache
921+
if (cached == null) {
922+
cached = ReactNativeFeatureFlagsCxxInterop.useWindowDensityForDisplayMetricsAndroid()
923+
useWindowDensityForDisplayMetricsAndroidCache = cached
924+
}
925+
return cached
926+
}
927+
918928
override fun viewCullingOutsetRatio(): Double {
919929
var cached = viewCullingOutsetRatioCache
920930
if (cached == null) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<fa0374808e3cbe7122ca993780c0b627>>
7+
* @generated SignedSource<<312cec99bd429d22e25f4e82d7fc4ee7>>
88
*/
99

1010
/**
@@ -206,6 +206,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
206206

207207
@DoNotStrip @JvmStatic public external fun useTurboModuleInterop(): Boolean
208208

209+
@DoNotStrip @JvmStatic public external fun useWindowDensityForDisplayMetricsAndroid(): Boolean
210+
209211
@DoNotStrip @JvmStatic public external fun viewCullingOutsetRatio(): Double
210212

211213
@DoNotStrip @JvmStatic public external fun viewTransitionEnabled(): Boolean

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<768077a22c110aa98ab02bc96eec695b>>
7+
* @generated SignedSource<<669f16e131d4a5ddc026ffecc6893575>>
88
*/
99

1010
/**
@@ -201,6 +201,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
201201

202202
override fun useTurboModuleInterop(): Boolean = false
203203

204+
override fun useWindowDensityForDisplayMetricsAndroid(): Boolean = false
205+
204206
override fun viewCullingOutsetRatio(): Double = 0.0
205207

206208
override fun viewTransitionEnabled(): Boolean = false

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<86ebc7307e7ebe2023e909a868ed3117>>
7+
* @generated SignedSource<<ee4c0f9c1e7c6af3b0d15948ebc519b3>>
88
*/
99

1010
/**
@@ -113,6 +113,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
113113
private var useSharedAnimatedBackendCache: Boolean? = null
114114
private var useTraitHiddenOnAndroidCache: Boolean? = null
115115
private var useTurboModuleInteropCache: Boolean? = null
116+
private var useWindowDensityForDisplayMetricsAndroidCache: Boolean? = null
116117
private var viewCullingOutsetRatioCache: Double? = null
117118
private var viewTransitionEnabledCache: Boolean? = null
118119
private var viewTransitionUseHardwareBitmapAndroidCache: Boolean? = null
@@ -1008,6 +1009,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
10081009
return cached
10091010
}
10101011

1012+
override fun useWindowDensityForDisplayMetricsAndroid(): Boolean {
1013+
var cached = useWindowDensityForDisplayMetricsAndroidCache
1014+
if (cached == null) {
1015+
cached = currentProvider.useWindowDensityForDisplayMetricsAndroid()
1016+
accessedFeatureFlags.add("useWindowDensityForDisplayMetricsAndroid")
1017+
useWindowDensityForDisplayMetricsAndroidCache = cached
1018+
}
1019+
return cached
1020+
}
1021+
10111022
override fun viewCullingOutsetRatio(): Double {
10121023
var cached = viewCullingOutsetRatioCache
10131024
if (cached == null) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<c83fba10dc6f7d596e3bcd9725c64e6e>>
7+
* @generated SignedSource<<18267103ef4f3eb22f23a15b808528c2>>
88
*/
99

1010
/**
@@ -201,6 +201,8 @@ public interface ReactNativeFeatureFlagsProvider {
201201

202202
@DoNotStrip public fun useTurboModuleInterop(): Boolean
203203

204+
@DoNotStrip public fun useWindowDensityForDisplayMetricsAndroid(): Boolean
205+
204206
@DoNotStrip public fun viewCullingOutsetRatio(): Double
205207

206208
@DoNotStrip public fun viewTransitionEnabled(): Boolean

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ 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.internal.featureflags.ReactNativeFeatureFlags
1718
import com.facebook.react.uimanager.PixelUtil.pxToDp
1819

1920
/**
@@ -61,6 +62,13 @@ public object DisplayMetricsHolder {
6162
// Non-visual contexts (e.g. Application) may throw on API 30+.
6263
// Falls back to resource display metrics copied via setTo() above.
6364
}
65+
if (
66+
ReactNativeFeatureFlags.useWindowDensityForDisplayMetricsAndroid() &&
67+
displayMetrics.densityDpi != screenDisplayMetrics.densityDpi
68+
) {
69+
screenDisplayMetrics.density = displayMetrics.density
70+
screenDisplayMetrics.densityDpi = displayMetrics.densityDpi
71+
}
6472
// Preserve fontScale from the configuration because getRealMetrics() returns
6573
// physical display metrics without the system font scale setting.
6674
// This is needed for proper text scaling when fontScale < 1.0

‎packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<200266835197634d13de07182d80a2c1>>
7+
* @generated SignedSource<<568d6e4175fb51e09bc3d4441ffa9741>>
88
*/
99

1010
/**
@@ -573,6 +573,12 @@ class ReactNativeFeatureFlagsJavaProvider
573573
return method(javaProvider_);
574574
}
575575

576+
bool useWindowDensityForDisplayMetricsAndroid() override {
577+
static const auto method =
578+
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("useWindowDensityForDisplayMetricsAndroid");
579+
return method(javaProvider_);
580+
}
581+
576582
double viewCullingOutsetRatio() override {
577583
static const auto method =
578584
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jdouble()>("viewCullingOutsetRatio");
@@ -1046,6 +1052,11 @@ bool JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop(
10461052
return ReactNativeFeatureFlags::useTurboModuleInterop();
10471053
}
10481054

1055+
bool JReactNativeFeatureFlagsCxxInterop::useWindowDensityForDisplayMetricsAndroid(
1056+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
1057+
return ReactNativeFeatureFlags::useWindowDensityForDisplayMetricsAndroid();
1058+
}
1059+
10491060
double JReactNativeFeatureFlagsCxxInterop::viewCullingOutsetRatio(
10501061
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
10511062
return ReactNativeFeatureFlags::viewCullingOutsetRatio();
@@ -1364,6 +1375,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
13641375
makeNativeMethod(
13651376
"useTurboModuleInterop",
13661377
JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop),
1378+
makeNativeMethod(
1379+
"useWindowDensityForDisplayMetricsAndroid",
1380+
JReactNativeFeatureFlagsCxxInterop::useWindowDensityForDisplayMetricsAndroid),
13671381
makeNativeMethod(
13681382
"viewCullingOutsetRatio",
13691383
JReactNativeFeatureFlagsCxxInterop::viewCullingOutsetRatio),

‎packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<13aa248dae556b06b31c25a28e448460>>
7+
* @generated SignedSource<<2e0f936c003aafc456ab3f960ca4b2f3>>
88
*/
99

1010
/**
@@ -297,6 +297,9 @@ class JReactNativeFeatureFlagsCxxInterop
297297
static bool useTurboModuleInterop(
298298
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
299299

300+
static bool useWindowDensityForDisplayMetricsAndroid(
301+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
302+
300303
static double viewCullingOutsetRatio(
301304
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
302305

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/DisplayMetricsHolderTest.kt‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import android.view.View
1717
import android.view.Window
1818
import android.view.WindowInsets
1919
import androidx.annotation.RequiresApi
20+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
21+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults
22+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
2023
import com.facebook.testutils.shadows.ShadowNativeLoader
2124
import com.facebook.testutils.shadows.ShadowNativeMap
2225
import com.facebook.testutils.shadows.ShadowReadableNativeMap
@@ -52,6 +55,7 @@ class DisplayMetricsHolderTest {
5255

5356
@Before
5457
fun setUp() {
58+
ReactNativeFeatureFlagsForTests.setUp()
5559
context = RuntimeEnvironment.getApplication()
5660
displayMetrics = context.resources.displayMetrics
5761
DisplayMetricsHolder.setScreenDisplayMetrics(null)
@@ -60,6 +64,7 @@ class DisplayMetricsHolderTest {
6064
@After
6165
fun tearDown() {
6266
DisplayMetricsHolder.setScreenDisplayMetrics(null)
67+
ReactNativeFeatureFlags.dangerouslyReset()
6368
}
6469

6570
@Test(expected = IllegalStateException::class)
@@ -142,6 +147,41 @@ class DisplayMetricsHolderTest {
142147
assertThat(screenMetrics.scaledDensity).isEqualTo(customScaledDensity)
143148
}
144149

150+
@Test
151+
fun initDisplayMetrics_withWindowDensityFlag_usesTheWindowDensity() {
152+
overrideWindowDensityFlag(true)
153+
setWindowDensityTo400Dpi()
154+
155+
DisplayMetricsHolder.initDisplayMetrics(context)
156+
157+
val screenMetrics = DisplayMetricsHolder.getScreenDisplayMetrics()
158+
assertThat(screenMetrics.density).isEqualTo(2.5f)
159+
assertThat(screenMetrics.densityDpi).isEqualTo(DisplayMetrics.DENSITY_400)
160+
assertThat(PixelUtil.toDIPFromPixel(PixelUtil.toPixelFromSP(16f))).isEqualTo(16f)
161+
}
162+
163+
@Test
164+
fun initDisplayMetrics_withoutWindowDensityFlag_usesTheDisplayDensity() {
165+
DisplayMetricsHolder.initDisplayMetrics(context)
166+
val displayDensityDpi = DisplayMetricsHolder.getScreenDisplayMetrics().densityDpi
167+
overrideWindowDensityFlag(false)
168+
setWindowDensityTo400Dpi()
169+
170+
DisplayMetricsHolder.initDisplayMetrics(context)
171+
172+
assertThat(DisplayMetricsHolder.getScreenDisplayMetrics().densityDpi)
173+
.isEqualTo(displayDensityDpi)
174+
.isNotEqualTo(DisplayMetrics.DENSITY_400)
175+
}
176+
177+
@Test
178+
fun initDisplayMetrics_withMatchingDensities_doesNotReadFeatureFlags() {
179+
DisplayMetricsHolder.initDisplayMetrics(context)
180+
overrideWindowDensityFlag(true)
181+
182+
assertThat(ReactNativeFeatureFlags.useWindowDensityForDisplayMetricsAndroid()).isTrue()
183+
}
184+
145185
@Test
146186
fun initDisplayMetrics_doesNotCrashWithNonVisualContext() {
147187
val mockContext: Context = mock()
@@ -164,4 +204,19 @@ class DisplayMetricsHolderTest {
164204
// Metrics should still be set from resource display metrics
165205
assertThat(DisplayMetricsHolder.getScreenDisplayMetrics()).isNotNull()
166206
}
207+
208+
private fun overrideWindowDensityFlag(enabled: Boolean) {
209+
ReactNativeFeatureFlags.override(
210+
object : ReactNativeFeatureFlagsDefaults() {
211+
override fun useWindowDensityForDisplayMetricsAndroid(): Boolean = enabled
212+
},
213+
)
214+
}
215+
216+
private fun setWindowDensityTo400Dpi() {
217+
val resourceMetrics = context.resources.displayMetrics
218+
resourceMetrics.density = 2.5f
219+
resourceMetrics.densityDpi = DisplayMetrics.DENSITY_400
220+
resourceMetrics.scaledDensity = 2.5f
221+
}
167222
}

0 commit comments

Comments
 (0)