Skip to content

Commit 642f086

Browse files
kdwkrmeta-codesync[bot]
authored andcommitted
Fix text not scaling down when system fontScale < 1.0 (#54238)
Summary: Fixes #54168 In React Native 0.82.0, text on Android does not scale down when the system font scale is set to less than 1.0 (e.g., 85%). This regression was introduced when `PixelUtil.toPixelFromSP()` was changed to use `DisplayMetricsHolder.getScreenDisplayMetrics()` instead of `getWindowDisplayMetrics()` in commit [1ad2ec0](1ad2ec0). The issue occurs because: 1. **windowDisplayMetrics** is obtained from `context.resources.displayMetrics` and includes the system font scale from `Configuration` 2. **screenDisplayMetrics** is populated by `Display.getRealMetrics()`, which returns physical display metrics **without** the system font scale setting ([Reference](https://developer.android.com/reference/android/view/Display#getRealMetrics(android.util.DisplayMetrics))) When `getRealMetrics()` is called, it overwrites the `scaledDensity` value (which is `density * fontScale`), effectively resetting it to just `density` and losing the user's font scale preference. ## Changelog: <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [ANDROID] [FIXED] - Fix text not scaling down when system fontScale < 1.0 Pull Request resolved: #54238 Test Plan: https://github.com/kdwkr/rn-0.82.0-android-layout-scale Tested with this reproducer --- Reviewed By: javache Differential Revision: D85350263 Pulled By: alanleedev fbshipit-source-id: ff646cf0405f689ff2a9166a1474fdb8b1b85fd6
1 parent 851d59a commit 642f086

File tree

3 files changed

+212
-1
lines changed

3 files changed

+212
-1
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public object DisplayMetricsHolder {
6161
}
6262

6363
@JvmStatic
64+
@Suppress("DEPRECATION")
6465
public fun initDisplayMetrics(context: Context) {
6566
val displayMetrics = context.resources.displayMetrics
6667
windowDisplayMetrics = displayMetrics
@@ -72,7 +73,11 @@ public object DisplayMetricsHolder {
7273
//
7374
// See:
7475
// http://developer.android.com/reference/android/view/Display.html#getRealMetrics(android.util.DisplayMetrics)
75-
@Suppress("DEPRECATION") wm.defaultDisplay.getRealMetrics(screenDisplayMetrics)
76+
wm.defaultDisplay.getRealMetrics(screenDisplayMetrics)
77+
// Preserve fontScale from the configuration because getRealMetrics() returns
78+
// physical display metrics without the system font scale setting.
79+
// This is needed for proper text scaling when fontScale < 1.0
80+
screenDisplayMetrics.scaledDensity = displayMetrics.scaledDensity
7681
DisplayMetricsHolder.screenDisplayMetrics = screenDisplayMetrics
7782
}
7883

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,18 @@ class DisplayMetricsHolderTest {
166166
assertThat(decodedWidth).isEqualTo(width)
167167
assertThat(decodedHeight).isEqualTo(height)
168168
}
169+
170+
@Test
171+
fun initDisplayMetrics_preservesScaledDensityForFontScale() {
172+
val originalMetrics = context.resources.displayMetrics
173+
val customScaledDensity = originalMetrics.density * 0.85f // fontScale = 0.85
174+
175+
originalMetrics.scaledDensity = customScaledDensity
176+
177+
DisplayMetricsHolder.initDisplayMetrics(context)
178+
179+
val screenMetrics = DisplayMetricsHolder.getScreenDisplayMetrics()
180+
181+
assertThat(screenMetrics.scaledDensity).isEqualTo(customScaledDensity)
182+
}
169183
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager
9+
10+
import android.content.Context
11+
import android.content.res.Configuration
12+
import android.content.res.Resources
13+
import android.util.DisplayMetrics
14+
import com.facebook.testutils.shadows.ShadowNativeLoader
15+
import com.facebook.testutils.shadows.ShadowSoLoader
16+
import kotlin.math.abs
17+
import org.assertj.core.api.Assertions.assertThat
18+
import org.junit.After
19+
import org.junit.Before
20+
import org.junit.Test
21+
import org.junit.runner.RunWith
22+
import org.mockito.kotlin.mock
23+
import org.mockito.kotlin.whenever
24+
import org.robolectric.RobolectricTestRunner
25+
import org.robolectric.RuntimeEnvironment
26+
import org.robolectric.annotation.Config
27+
28+
@RunWith(RobolectricTestRunner::class)
29+
@Config(shadows = [ShadowSoLoader::class, ShadowNativeLoader::class])
30+
class PixelUtilTest {
31+
32+
private lateinit var context: Context
33+
34+
@Before
35+
fun setUp() {
36+
context = RuntimeEnvironment.getApplication()
37+
DisplayMetricsHolder.setWindowDisplayMetrics(null)
38+
DisplayMetricsHolder.setScreenDisplayMetrics(null)
39+
}
40+
41+
@After
42+
fun tearDown() {
43+
DisplayMetricsHolder.setWindowDisplayMetrics(null)
44+
DisplayMetricsHolder.setScreenDisplayMetrics(null)
45+
}
46+
47+
@Test
48+
fun toPixelFromSP_respectsFontScaleLessThanOne() {
49+
// Setup display metrics with fontScale < 1.0
50+
val displayMetrics = DisplayMetrics()
51+
displayMetrics.density = 3.0f
52+
displayMetrics.scaledDensity = 3.0f * 0.85f // fontScale = 0.85
53+
displayMetrics.widthPixels = 1080
54+
displayMetrics.heightPixels = 1920
55+
displayMetrics.densityDpi = DisplayMetrics.DENSITY_XXHIGH
56+
57+
DisplayMetricsHolder.setWindowDisplayMetrics(displayMetrics)
58+
DisplayMetricsHolder.setScreenDisplayMetrics(displayMetrics)
59+
60+
// Test that toPixelFromSP respects fontScale < 1.0
61+
val fontSize = 16f // 16sp
62+
val result = PixelUtil.toPixelFromSP(fontSize)
63+
64+
// Expected: 16sp * 3.0 (density) * 0.85 (fontScale) = 40.8px
65+
val expected = fontSize * displayMetrics.scaledDensity
66+
67+
assertThat(abs(result - expected)).isLessThan(0.1f)
68+
}
69+
70+
@Test
71+
fun toPixelFromSP_respectsFontScaleGreaterThanOne() {
72+
// Setup display metrics with fontScale > 1.0
73+
val displayMetrics = DisplayMetrics()
74+
displayMetrics.density = 3.0f
75+
displayMetrics.scaledDensity = 3.0f * 1.3f // fontScale = 1.3
76+
displayMetrics.widthPixels = 1080
77+
displayMetrics.heightPixels = 1920
78+
displayMetrics.densityDpi = DisplayMetrics.DENSITY_XXHIGH
79+
80+
DisplayMetricsHolder.setWindowDisplayMetrics(displayMetrics)
81+
DisplayMetricsHolder.setScreenDisplayMetrics(displayMetrics)
82+
83+
// Test that toPixelFromSP respects fontScale > 1.0
84+
val fontSize = 16f // 16sp
85+
val result = PixelUtil.toPixelFromSP(fontSize)
86+
87+
// Expected: 16sp * 3.0 (density) * 1.3 (fontScale) = 62.4px
88+
val expected = fontSize * displayMetrics.scaledDensity
89+
90+
assertThat(abs(result - expected)).isLessThan(0.1f)
91+
}
92+
93+
@Test
94+
fun toPixelFromSP_respectsMaxFontScale() {
95+
// Setup display metrics with high fontScale
96+
val displayMetrics = DisplayMetrics()
97+
displayMetrics.density = 3.0f
98+
displayMetrics.scaledDensity = 3.0f * 2.0f // fontScale = 2.0
99+
displayMetrics.widthPixels = 1080
100+
displayMetrics.heightPixels = 1920
101+
displayMetrics.densityDpi = DisplayMetrics.DENSITY_XXHIGH
102+
103+
DisplayMetricsHolder.setWindowDisplayMetrics(displayMetrics)
104+
DisplayMetricsHolder.setScreenDisplayMetrics(displayMetrics)
105+
106+
// Test that maxFontScale limits the scaling
107+
val fontSize = 16f // 16sp
108+
val maxFontScale = 1.5f
109+
val result = PixelUtil.toPixelFromSP(fontSize, maxFontScale)
110+
111+
// With fontScale = 2.0, scaledValue would be 16 * 3.0 * 2.0 = 96px
112+
// But maxFontScale = 1.5 limits it to 16 * 3.0 * 1.5 = 72px
113+
val expected = fontSize * displayMetrics.density * maxFontScale
114+
115+
assertThat(abs(result - expected)).isLessThan(0.1f)
116+
}
117+
118+
@Test
119+
fun toPixelFromSP_doesNotApplyMaxFontScaleWhenFontScaleIsLess() {
120+
// Setup display metrics with low fontScale
121+
val displayMetrics = DisplayMetrics()
122+
displayMetrics.density = 3.0f
123+
displayMetrics.scaledDensity = 3.0f * 0.8f // fontScale = 0.8
124+
displayMetrics.widthPixels = 1080
125+
displayMetrics.heightPixels = 1920
126+
displayMetrics.densityDpi = DisplayMetrics.DENSITY_XXHIGH
127+
128+
DisplayMetricsHolder.setWindowDisplayMetrics(displayMetrics)
129+
DisplayMetricsHolder.setScreenDisplayMetrics(displayMetrics)
130+
131+
// Test that maxFontScale doesn't prevent scaling down
132+
val fontSize = 16f // 16sp
133+
val maxFontScale = 1.5f
134+
val result = PixelUtil.toPixelFromSP(fontSize, maxFontScale)
135+
136+
// With fontScale = 0.8, scaledValue is 16 * 3.0 * 0.8 = 38.4px
137+
// maxFontScale limit would be 16 * 3.0 * 1.5 = 72px
138+
// min(38.4, 72) = 38.4px, so fontScale is respected
139+
val expected = fontSize * displayMetrics.scaledDensity
140+
141+
assertThat(abs(result - expected)).isLessThan(0.1f)
142+
}
143+
144+
@Test
145+
fun toPixelFromDIP_convertsCorrectly() {
146+
val displayMetrics = DisplayMetrics()
147+
displayMetrics.density = 3.0f
148+
displayMetrics.widthPixels = 1080
149+
displayMetrics.heightPixels = 1920
150+
displayMetrics.densityDpi = DisplayMetrics.DENSITY_XXHIGH
151+
152+
DisplayMetricsHolder.setScreenDisplayMetrics(displayMetrics)
153+
154+
val dipValue = 16f
155+
val result = PixelUtil.toPixelFromDIP(dipValue)
156+
157+
// Expected: 16dp * 3.0 (density) = 48px
158+
val expected = dipValue * displayMetrics.density
159+
160+
assertThat(abs(result - expected)).isLessThan(0.1f)
161+
}
162+
163+
@Test
164+
fun initDisplayMetrics_preservesFontScale() {
165+
// Create a context with custom configuration
166+
val mockContext = mock<Context>()
167+
val mockResources = mock<Resources>()
168+
val configuration = Configuration()
169+
configuration.fontScale = 0.85f
170+
171+
val displayMetrics = DisplayMetrics()
172+
displayMetrics.density = 3.0f
173+
displayMetrics.scaledDensity = 3.0f * 0.85f // fontScale = 0.85
174+
displayMetrics.widthPixels = 1080
175+
displayMetrics.heightPixels = 1920
176+
displayMetrics.densityDpi = DisplayMetrics.DENSITY_XXHIGH
177+
178+
whenever(mockContext.resources).thenReturn(mockResources)
179+
whenever(mockResources.displayMetrics).thenReturn(displayMetrics)
180+
whenever(mockResources.configuration).thenReturn(configuration)
181+
whenever(mockContext.getSystemService(Context.WINDOW_SERVICE))
182+
.thenReturn(context.getSystemService(Context.WINDOW_SERVICE))
183+
184+
// Initialize display metrics
185+
DisplayMetricsHolder.initDisplayMetrics(mockContext)
186+
187+
val screenMetrics = DisplayMetricsHolder.getScreenDisplayMetrics()
188+
189+
// Verify that scaledDensity (which includes fontScale) is preserved
190+
assertThat(abs(screenMetrics.scaledDensity - displayMetrics.scaledDensity)).isLessThan(0.01f)
191+
}
192+
}

0 commit comments

Comments
 (0)