From 9044e09bb74b8181949d7943b595bf6ef94c3f1b Mon Sep 17 00:00:00 2001 From: Bruno Ramos Date: Wed, 14 Oct 2026 11:53:26 -0700 Subject: [PATCH] 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 --- .../featureflags/ReactNativeFeatureFlags.kt | 8 ++- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 +++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 ++++- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../react/uimanager/DisplayMetricsHolder.kt | 8 +++ .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +++++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../uimanager/DisplayMetricsHolderTest.kt | 55 +++++++++++++++++++ .../facebook/react/uimanager/PixelUtilTest.kt | 4 ++ .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 ++- .../ReactNativeFeatureFlagsAccessor.cpp | 28 ++++++++-- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +++- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 ++- .../NativeReactNativeFeatureFlags.h | 4 +- .../ReactNativeFeatureFlags.config.js | 11 ++++ .../featureflags/ReactNativeFeatureFlags.js | 7 ++- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 23 files changed, 208 insertions(+), 24 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 0342cf267f2f..0c879d6415f9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -564,6 +564,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun useTurboModuleInterop(): Boolean = accessor.useTurboModuleInterop() + /** + * 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. + */ + @JvmStatic + public fun useWindowDensityForDisplayMetricsAndroid(): Boolean = accessor.useWindowDensityForDisplayMetricsAndroid() + /** * 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. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index bfc64b6a1339..28329721fab8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0107593110db613063b6f4ceb50d8e58>> + * @generated SignedSource<<71d0554dc45df7d5365f81b7ec8c3d5b>> */ /** @@ -109,6 +109,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var useSharedAnimatedBackendCache: Boolean? = null private var useTraitHiddenOnAndroidCache: Boolean? = null private var useTurboModuleInteropCache: Boolean? = null + private var useWindowDensityForDisplayMetricsAndroidCache: Boolean? = null private var viewCullingOutsetRatioCache: Double? = null private var viewTransitionEnabledCache: Boolean? = null private var viewTransitionUseHardwareBitmapAndroidCache: Boolean? = null @@ -915,6 +916,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun useWindowDensityForDisplayMetricsAndroid(): Boolean { + var cached = useWindowDensityForDisplayMetricsAndroidCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.useWindowDensityForDisplayMetricsAndroid() + useWindowDensityForDisplayMetricsAndroidCache = cached + } + return cached + } + override fun viewCullingOutsetRatio(): Double { var cached = viewCullingOutsetRatioCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 93e688128426..6f83f1cc3dbd 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<312cec99bd429d22e25f4e82d7fc4ee7>> */ /** @@ -206,6 +206,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun useTurboModuleInterop(): Boolean + @DoNotStrip @JvmStatic public external fun useWindowDensityForDisplayMetricsAndroid(): Boolean + @DoNotStrip @JvmStatic public external fun viewCullingOutsetRatio(): Double @DoNotStrip @JvmStatic public external fun viewTransitionEnabled(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index c593a036b4e7..f4998cb8b416 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<768077a22c110aa98ab02bc96eec695b>> + * @generated SignedSource<<669f16e131d4a5ddc026ffecc6893575>> */ /** @@ -201,6 +201,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun useTurboModuleInterop(): Boolean = false + override fun useWindowDensityForDisplayMetricsAndroid(): Boolean = false + override fun viewCullingOutsetRatio(): Double = 0.0 override fun viewTransitionEnabled(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 961e8c94ddc7..536ee78905d7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<86ebc7307e7ebe2023e909a868ed3117>> + * @generated SignedSource<> */ /** @@ -113,6 +113,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var useSharedAnimatedBackendCache: Boolean? = null private var useTraitHiddenOnAndroidCache: Boolean? = null private var useTurboModuleInteropCache: Boolean? = null + private var useWindowDensityForDisplayMetricsAndroidCache: Boolean? = null private var viewCullingOutsetRatioCache: Double? = null private var viewTransitionEnabledCache: Boolean? = null private var viewTransitionUseHardwareBitmapAndroidCache: Boolean? = null @@ -1008,6 +1009,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun useWindowDensityForDisplayMetricsAndroid(): Boolean { + var cached = useWindowDensityForDisplayMetricsAndroidCache + if (cached == null) { + cached = currentProvider.useWindowDensityForDisplayMetricsAndroid() + accessedFeatureFlags.add("useWindowDensityForDisplayMetricsAndroid") + useWindowDensityForDisplayMetricsAndroidCache = cached + } + return cached + } + override fun viewCullingOutsetRatio(): Double { var cached = viewCullingOutsetRatioCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 73121e261bb3..0143013e3ccd 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<18267103ef4f3eb22f23a15b808528c2>> */ /** @@ -201,6 +201,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun useTurboModuleInterop(): Boolean + @DoNotStrip public fun useWindowDensityForDisplayMetricsAndroid(): Boolean + @DoNotStrip public fun viewCullingOutsetRatio(): Double @DoNotStrip public fun viewTransitionEnabled(): Boolean 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 7dd31eb758d5..76827dec40fa 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 @@ -14,6 +14,7 @@ import android.util.DisplayMetrics import android.view.WindowManager import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.uimanager.PixelUtil.pxToDp /** @@ -61,6 +62,13 @@ public object DisplayMetricsHolder { // Non-visual contexts (e.g. Application) may throw on API 30+. // Falls back to resource display metrics copied via setTo() above. } + if ( + displayMetrics.densityDpi != screenDisplayMetrics.densityDpi && + ReactNativeFeatureFlags.useWindowDensityForDisplayMetricsAndroid() + ) { + screenDisplayMetrics.density = displayMetrics.density + screenDisplayMetrics.densityDpi = displayMetrics.densityDpi + } // Preserve fontScale from the configuration because getRealMetrics() returns // physical display metrics without the system font scale setting. // This is needed for proper text scaling when fontScale < 1.0 diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 6bff0ece1398..526398c18c11 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<200266835197634d13de07182d80a2c1>> + * @generated SignedSource<<568d6e4175fb51e09bc3d4441ffa9741>> */ /** @@ -573,6 +573,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool useWindowDensityForDisplayMetricsAndroid() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("useWindowDensityForDisplayMetricsAndroid"); + return method(javaProvider_); + } + double viewCullingOutsetRatio() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("viewCullingOutsetRatio"); @@ -1046,6 +1052,11 @@ bool JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop( return ReactNativeFeatureFlags::useTurboModuleInterop(); } +bool JReactNativeFeatureFlagsCxxInterop::useWindowDensityForDisplayMetricsAndroid( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::useWindowDensityForDisplayMetricsAndroid(); +} + double JReactNativeFeatureFlagsCxxInterop::viewCullingOutsetRatio( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::viewCullingOutsetRatio(); @@ -1364,6 +1375,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "useTurboModuleInterop", JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop), + makeNativeMethod( + "useWindowDensityForDisplayMetricsAndroid", + JReactNativeFeatureFlagsCxxInterop::useWindowDensityForDisplayMetricsAndroid), makeNativeMethod( "viewCullingOutsetRatio", JReactNativeFeatureFlagsCxxInterop::viewCullingOutsetRatio), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index e87b24228769..5f3fdda386cd 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<13aa248dae556b06b31c25a28e448460>> + * @generated SignedSource<<2e0f936c003aafc456ab3f960ca4b2f3>> */ /** @@ -297,6 +297,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool useTurboModuleInterop( facebook::jni::alias_ref); + static bool useWindowDensityForDisplayMetricsAndroid( + facebook::jni::alias_ref); + static double viewCullingOutsetRatio( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/DisplayMetricsHolderTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/DisplayMetricsHolderTest.kt index 54bc46108f8a..8b409f185d70 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/DisplayMetricsHolderTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/DisplayMetricsHolderTest.kt @@ -17,6 +17,9 @@ import android.view.View import android.view.Window import android.view.WindowInsets import androidx.annotation.RequiresApi +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.testutils.shadows.ShadowNativeLoader import com.facebook.testutils.shadows.ShadowNativeMap import com.facebook.testutils.shadows.ShadowReadableNativeMap @@ -52,6 +55,7 @@ class DisplayMetricsHolderTest { @Before fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() context = RuntimeEnvironment.getApplication() displayMetrics = context.resources.displayMetrics DisplayMetricsHolder.setScreenDisplayMetrics(null) @@ -60,6 +64,7 @@ class DisplayMetricsHolderTest { @After fun tearDown() { DisplayMetricsHolder.setScreenDisplayMetrics(null) + ReactNativeFeatureFlags.dangerouslyReset() } @Test(expected = IllegalStateException::class) @@ -142,6 +147,41 @@ class DisplayMetricsHolderTest { assertThat(screenMetrics.scaledDensity).isEqualTo(customScaledDensity) } + @Test + fun initDisplayMetrics_withWindowDensityFlag_usesTheWindowDensity() { + overrideWindowDensityFlag(true) + setWindowDensityTo400Dpi() + + DisplayMetricsHolder.initDisplayMetrics(context) + + val screenMetrics = DisplayMetricsHolder.getScreenDisplayMetrics() + assertThat(screenMetrics.density).isEqualTo(2.5f) + assertThat(screenMetrics.densityDpi).isEqualTo(DisplayMetrics.DENSITY_400) + assertThat(PixelUtil.toDIPFromPixel(PixelUtil.toPixelFromSP(16f))).isEqualTo(16f) + } + + @Test + fun initDisplayMetrics_withoutWindowDensityFlag_usesTheDisplayDensity() { + DisplayMetricsHolder.initDisplayMetrics(context) + val displayDensityDpi = DisplayMetricsHolder.getScreenDisplayMetrics().densityDpi + overrideWindowDensityFlag(false) + setWindowDensityTo400Dpi() + + DisplayMetricsHolder.initDisplayMetrics(context) + + assertThat(DisplayMetricsHolder.getScreenDisplayMetrics().densityDpi) + .isEqualTo(displayDensityDpi) + .isNotEqualTo(DisplayMetrics.DENSITY_400) + } + + @Test + fun initDisplayMetrics_withMatchingDensities_doesNotReadFeatureFlags() { + DisplayMetricsHolder.initDisplayMetrics(context) + overrideWindowDensityFlag(true) + + assertThat(ReactNativeFeatureFlags.useWindowDensityForDisplayMetricsAndroid()).isTrue() + } + @Test fun initDisplayMetrics_doesNotCrashWithNonVisualContext() { val mockContext: Context = mock() @@ -164,4 +204,19 @@ class DisplayMetricsHolderTest { // Metrics should still be set from resource display metrics assertThat(DisplayMetricsHolder.getScreenDisplayMetrics()).isNotNull() } + + private fun overrideWindowDensityFlag(enabled: Boolean) { + ReactNativeFeatureFlags.override( + object : ReactNativeFeatureFlagsDefaults() { + override fun useWindowDensityForDisplayMetricsAndroid(): Boolean = enabled + }, + ) + } + + private fun setWindowDensityTo400Dpi() { + val resourceMetrics = context.resources.displayMetrics + resourceMetrics.density = 2.5f + resourceMetrics.densityDpi = DisplayMetrics.DENSITY_400 + resourceMetrics.scaledDensity = 2.5f + } } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/PixelUtilTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/PixelUtilTest.kt index 46ae613601d3..766392383397 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/PixelUtilTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/PixelUtilTest.kt @@ -14,6 +14,8 @@ import android.content.Context import android.content.res.Configuration import android.content.res.Resources import android.util.DisplayMetrics +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.testutils.shadows.ShadowNativeLoader import com.facebook.testutils.shadows.ShadowSoLoader import kotlin.math.abs @@ -36,6 +38,7 @@ class PixelUtilTest { @Before fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() context = RuntimeEnvironment.getApplication() DisplayMetricsHolder.setScreenDisplayMetrics(null) } @@ -43,6 +46,7 @@ class PixelUtilTest { @After fun tearDown() { DisplayMetricsHolder.setScreenDisplayMetrics(null) + ReactNativeFeatureFlags.dangerouslyReset() } @Test diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 1744ea854459..adc3531518ef 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -382,6 +382,10 @@ bool ReactNativeFeatureFlags::useTurboModuleInterop() { return getAccessor().useTurboModuleInterop(); } +bool ReactNativeFeatureFlags::useWindowDensityForDisplayMetricsAndroid() { + return getAccessor().useWindowDensityForDisplayMetricsAndroid(); +} + double ReactNativeFeatureFlags::viewCullingOutsetRatio() { return getAccessor().viewCullingOutsetRatio(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index c5195f19bd59..cd4d40533d5f 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1a9a8a587b477b6aa0d3aa57e30272c2>> + * @generated SignedSource<<62dbb584d6aaf595314636a81346e3e5>> */ /** @@ -486,6 +486,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool useTurboModuleInterop(); + /** + * 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. + */ + RN_EXPORT static bool useWindowDensityForDisplayMetricsAndroid(); + /** * 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. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 3d332f2976f7..93ef1cc62ace 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -1631,6 +1631,24 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::useWindowDensityForDisplayMetricsAndroid() { + auto flagValue = useWindowDensityForDisplayMetricsAndroid_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(89, "useWindowDensityForDisplayMetricsAndroid"); + + flagValue = currentProvider_->useWindowDensityForDisplayMetricsAndroid(); + useWindowDensityForDisplayMetricsAndroid_ = flagValue; + } + + return flagValue.value(); +} + double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { auto flagValue = viewCullingOutsetRatio_.load(); @@ -1640,7 +1658,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "viewCullingOutsetRatio"); + markFlagAsAccessed(90, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1658,7 +1676,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(90, "viewTransitionEnabled"); + markFlagAsAccessed(91, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1676,7 +1694,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(91, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(92, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1694,7 +1712,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(92, "virtualViewPrerenderRatio"); + markFlagAsAccessed(93, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index fc7efe0b5c7e..5c05c3bc5427 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<486b4aac1c83d8bced6a3f5393a03756>> + * @generated SignedSource<<074e6ad1d773f289f93ce1d16656ec6e>> */ /** @@ -123,6 +123,7 @@ class ReactNativeFeatureFlagsAccessor { bool useSharedAnimatedBackend(); bool useTraitHiddenOnAndroid(); bool useTurboModuleInterop(); + bool useWindowDensityForDisplayMetricsAndroid(); double viewCullingOutsetRatio(); bool viewTransitionEnabled(); bool viewTransitionUseHardwareBitmapAndroid(); @@ -138,7 +139,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 93> accessedFeatureFlags_; + std::array, 94> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -229,6 +230,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> useSharedAnimatedBackend_; std::atomic> useTraitHiddenOnAndroid_; std::atomic> useTurboModuleInterop_; + std::atomic> useWindowDensityForDisplayMetricsAndroid_; std::atomic> viewCullingOutsetRatio_; std::atomic> viewTransitionEnabled_; std::atomic> viewTransitionUseHardwareBitmapAndroid_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 92e1157fefbf..82208e3861b9 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<52db1adfed5fd99e4ca145b50dc413a0>> */ /** @@ -385,6 +385,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool useWindowDensityForDisplayMetricsAndroid() override { + return false; + } + double viewCullingOutsetRatio() override { return 0.0; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index a49d8e7e919a..4d40ac107fc4 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2a5ad585617d77035a43b255d3e30eb3>> + * @generated SignedSource<<8d27c5f3feb4c8000922012882701b7e>> */ /** @@ -848,6 +848,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::useTurboModuleInterop(); } + bool useWindowDensityForDisplayMetricsAndroid() override { + auto value = values_["useWindowDensityForDisplayMetricsAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useWindowDensityForDisplayMetricsAndroid(); + } + double viewCullingOutsetRatio() override { auto value = values_["viewCullingOutsetRatio"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index eed6e5e8a51c..3ba8405c14ff 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8daa3cebd554caa27139d96604fcc368>> + * @generated SignedSource<<7e5839223cfce81a42708f876c4c77ee>> */ /** @@ -116,6 +116,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool useSharedAnimatedBackend() = 0; virtual bool useTraitHiddenOnAndroid() = 0; virtual bool useTurboModuleInterop() = 0; + virtual bool useWindowDensityForDisplayMetricsAndroid() = 0; virtual double viewCullingOutsetRatio() = 0; virtual bool viewTransitionEnabled() = 0; virtual bool viewTransitionUseHardwareBitmapAndroid() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 55a535f5be9d..f2d4210f8d23 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<00cccf7098052c51f5855949375ea0f9>> + * @generated SignedSource<> */ /** @@ -489,6 +489,11 @@ bool NativeReactNativeFeatureFlags::useTurboModuleInterop( return ReactNativeFeatureFlags::useTurboModuleInterop(); } +bool NativeReactNativeFeatureFlags::useWindowDensityForDisplayMetricsAndroid( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::useWindowDensityForDisplayMetricsAndroid(); +} + double NativeReactNativeFeatureFlags::viewCullingOutsetRatio( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::viewCullingOutsetRatio(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 459619f34276..4aafb51f00fc 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<84b944e162fc53799326c584a5edd17d>> */ /** @@ -216,6 +216,8 @@ class NativeReactNativeFeatureFlags bool useTurboModuleInterop(jsi::Runtime& runtime); + bool useWindowDensityForDisplayMetricsAndroid(jsi::Runtime& runtime); + double viewCullingOutsetRatio(jsi::Runtime& runtime); bool viewTransitionEnabled(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 08f37bd4f127..0a76242a2c75 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -1005,6 +1005,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'canary', }, + useWindowDensityForDisplayMetricsAndroid: { + defaultValue: false, + metadata: { + dateAdded: '2026-10-11', + description: + '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.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, viewCullingOutsetRatio: { defaultValue: 0, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index d8626a635e6b..1f474567697e 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<4eb26b4190d023d26a50f54b6e3ea5eb>> * @flow strict * @noformat */ @@ -139,6 +139,7 @@ export type ReactNativeFeatureFlags = Readonly<{ useSharedAnimatedBackend: Getter, useTraitHiddenOnAndroid: Getter, useTurboModuleInterop: Getter, + useWindowDensityForDisplayMetricsAndroid: Getter, viewCullingOutsetRatio: Getter, viewTransitionEnabled: Getter, viewTransitionUseHardwareBitmapAndroid: Getter, @@ -580,6 +581,10 @@ export const useTraitHiddenOnAndroid: Getter = createNativeFlagGetter(' * In Bridgeless mode, should legacy NativeModules use the TurboModule system? */ export const useTurboModuleInterop: Getter = createNativeFlagGetter('useTurboModuleInterop', false); +/** + * 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. + */ +export const useWindowDensityForDisplayMetricsAndroid: Getter = createNativeFlagGetter('useWindowDensityForDisplayMetricsAndroid', false); /** * 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. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index b73154a74852..673dc0ed3bf8 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3972c873d92371d3dc67eaa3a6217427>> + * @generated SignedSource<<40cc7b87d7658fb5aa3d33f468eff3fc>> * @flow strict * @noformat */ @@ -114,6 +114,7 @@ export interface Spec extends TurboModule { readonly useSharedAnimatedBackend?: () => boolean; readonly useTraitHiddenOnAndroid?: () => boolean; readonly useTurboModuleInterop?: () => boolean; + readonly useWindowDensityForDisplayMetricsAndroid?: () => boolean; readonly viewCullingOutsetRatio?: () => number; readonly viewTransitionEnabled?: () => boolean; readonly viewTransitionUseHardwareBitmapAndroid?: () => boolean;