[Android] `maxFontSizeMultiplier` doesn't cap line height on Android (Fabric) — height keeps growing with device font scale even when JS pre-caps the value
还没有人认领这个 Issue。
- 主要语言
- C++
- 星标
- 127k
- 派生
- 25.3k
- 平均合并
- 1 天 23 小时
- 30 天内合并 PR
- 4
描述
Description
On Android with the New Architecture (Fabric) enabled, maxFontSizeMultiplier does not fully cap rendered text size. Even when an explicit, already-capped lineHeight value is passed from JS (a fixed number that does not depend on the device's actual font scale), the rendered layout height still changes depending on the device's real system font scale setting — as if the native layer re-applies the raw (uncapped) Configuration.fontScale on top of the value React Native was given.
fontSize itself appears to be capped correctly (min(fontScale, maxFontSizeMultiplier), matching TextAttributeProps.getEffectiveFontSize() / PixelUtil.toPixelFromSP()), but the line height / leading used for layout does not seem to go through the same capped calculation, and continues to track the device's raw font scale.
Expected Behavior
With maxFontSizeMultiplier set to a fixed value, the effective scaling multiplier applied to a <Text> (including its line height / layout height) should never exceed that value, regardless of how much larger the device's actual system font scale is — matching the documented behavior ("Specifies the largest possible scale a font can reach when allowFontScaling is enabled").
Actual Behavior
Rendered text height keeps growing with the device's real font scale even when maxFontSizeMultiplier is fixed and even when an already-capped lineHeight is explicitly supplied from JS. Only fully disabling allowFontScaling and computing sizes manually produces a stable, correctly-capped result.
Steps to reproduce
1. Minimal repro — fixed maxFontSizeMultiplier, size still varies by device font scale
import { PixelRatio, Text, View } from 'react-native';
export default function FontScaleTest() {
const fontScale = PixelRatio.getFontScale();
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.5}
style={{ fontSize: 20, backgroundColor: 'yellow' }}
onLayout={e =>
console.log(
'fontScale=',
fontScale,
'height=',
e.nativeEvent.layout.height,
)
}
>
가나다
</Text>
</View>
);
}
Set the device's system font size so that PixelRatio.getFontScale() reports 1.5, force-quit and cold-start the app, and note the logged height. Then set the device font size so getFontScale() reports 2.0, force-quit and cold-start again, and compare.
Expected: identical height in both cases, since maxFontSizeMultiplier={1.5} should cap the effective multiplier to 1.5 in both (min(1.5, 1.5) = 1.5 and min(2.0, 1.5) = 1.5).
Actual:
| Device fontScale | height |
|---|---|
| 1.5 | 34.844444 |
| 2.0 | 39.822224 |
A ~14.3% difference, reproduced consistently across cold starts.
2. Isolating the cause — explicit pre-capped lineHeight still leaks
To rule out "the automatic default line-height calculation ignores the cap," we passed an explicit lineHeight computed in JS so it is numerically identical regardless of the device's font scale (since maxFontSizeMultiplier should cap both cases to the same 1.5):
<Text
allowFontScaling
maxFontSizeMultiplier={1.5}
numberOfLines={1}
style={{
fontSize: 20,
lineHeight: 20 * 1.5 * 1.2 /* = 36, same value in both cases */,
}}
onLayout={e =>
console.log('fontScale=', fontScale, 'height=', e.nativeEvent.layout.height)
}
>
Aa가나다
</Text>
| fontScale=1.5 | fontScale=2.0 | delta | |
|---|---|---|---|
| Auto lineHeight | 34.844444 | 39.822224 | +14.3% |
| Explicit fixed lineHeight (36 in both cases) | 36.266666 | 43.377773 | +19.6% |
Even though the exact same JS-computed number (36) was passed to lineHeight in both cases, the rendered height still differs by ~19.6% depending on the device's real font scale. This rules out "default line-height calculation doesn't check the cap" as the sole explanation — something in the native layer appears to reintroduce the raw, uncapped fontScale/scaledDensity even when the JS side has already fully accounted for the cap.
3. Confirmed workaround — disabling allowFontScaling entirely and computing everything manually removes the discrepancy
const cappedScale = Math.min(fontScale, 1.5);
const fontSize = 20 * cappedScale; // 30 in both cases
const lineHeight = fontSize * 1.1615; // 34.845 in both cases
<Text allowFontScaling={false} numberOfLines={1} style={{ fontSize, lineHeight }} onLayout={...}>
Aa가나다
</Text>
| fontScale=1.5 | fontScale=2.0 | |
|---|---|---|
| height | 35.200001 | 35.200001 |
Identical. This strongly suggests the leak only happens through the allowFontScaling={true} / SP-based native scaling path, and is avoided entirely by using allowFontScaling={false} with manual DIP-based sizing.
Additional Context
-
We initially suspected this could be the previously-fixed issue in #47499 (Fabric ignoring
maxFontSizeMultiplierentirely), fixed by #47614 and shipped in 0.78 — but that fix predates both our original 0.83.1 install and the 0.87.0 re-verification below, and the magnitude of the discrepancy (~14–20%) doesn't match "completely ignored" (which would produce a difference closer to the raw fontScale ratio,2.0 / 1.5 ≈ 33%). -
We also ruled out the New Architecture "layout not recalculated after returning from background without a full restart" issue (#51768) by reproducing with a full cold start (force-quit + relaunch) between each font-scale change.
-
The pattern (fontSize appears correctly capped; line height / layout height does not) points at a discrepancy between the code path that sizes glyphs and the code path that determines line height/leading for layout purposes, but we were not able to pin down the exact native function responsible.
-
Minimal repro repository (local RN CLI project,
react-native@0.87.0, New Architecture enabled, matches the environment above exactly): https://github.com/silverj0805/RNFontScaleRepro.src/App.tsxbundles all three cases above into one screen with tab switching and on-screenPixelRatio.getFontScale()/onLayoutheight readouts (see the screenshots above), so results can be checked without a console attached. -
This bug was originally reproduced and reported against
react-native@0.83.1. After the first version of this issue was flagged as targeting an unsupported version, the repro project was upgraded end-to-end to 0.87.0 (the latest stable release at time of writing) and re-verified on an Android emulator (Pixel 8 Pro, API 35,font_scaleset viaadb shell settings put system font_scale <value>, cold-started between changes) — the bug reproduces identically on 0.87.0, withonLayoutheight values matching to the sub-pixel:Test fontScale=1.5 fontScale=2.0 1 (auto lineHeight) 35.000000 40.333344 2 (explicit fixed lineHeight=36) 36.000000 43.333313 3 (workaround: allowFontScaling=false+ manual math)35.000000 35.000000 Same shape as the physical-device numbers: Test 1 and 2 keep growing with the device's raw
fontScalepast the1.5cap, Test 3 stays exactly fixed — on a stock Pixel emulator, independent of the Galaxy S21/One UI hardware above, so this isn't Samsung-specific either. (These emulator numbers were measured withABCas the sample text rather than the Hangul text shown in the physical-device numbers and code snippets above — the pattern is identical either way, since the leak is in the line-height calculation, not the glyphs.)
React Native Version
0.87.0
Affected Platforms
Runtime - Android
Areas
Fabric - The New Renderer
Output of npx @react-native-community/cli info
System:
OS: macOS 26.4.1
CPU: (8) arm64 Apple M1 Pro
Memory: 192.83 MB / 16.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 22.17.0
path: /opt/homebrew/opt/node@22/bin/node
Yarn:
version: 3.6.4
path: /opt/homebrew/opt/node@22/bin/yarn
npm:
version: 10.9.2
path: /opt/homebrew/opt/node@22/bin/npm
Watchman:
version: 2025.06.30.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.15.2
path: /Users/choieunjeong/.rbenv/shims/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 25.4
- iOS 26.4
- macOS 26.4
- tvOS 26.4
- visionOS 26.4
- watchOS 26.4
Android SDK:
API Levels:
- "31"
- "33"
- "34"
- "35"
- "36"
- "37"
Build Tools:
- 30.0.2
- 30.0.3
- 33.0.0
- 33.0.1
- 34.0.0
- 35.0.0
- 36.0.0
- 37.0.0
System Images:
- android-35 | Google Play ARM 64 v8a
- android-36.1 | Google APIs ARM 64 v8a
- android-36 | Google APIs ARM 64 v8a
- android-36 | Google Play ARM 64 v8a
Android NDK: Not Found
IDEs:
Android Studio: 2025.2 AI-252.28238.7.2523.14688667
Xcode:
version: 26.4/17E192
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.11
path: /usr/bin/javac
Ruby:
version: 2.7.6
path: /Users/choieunjeong/.rbenv/shims/ruby
npmPackages:
"@react-native-community/cli":
installed: 20.2.0
wanted: 20.2.0
react:
installed: 19.2.3
wanted: 19.2.3
react-native:
installed: 0.87.0
wanted: 0.87.0
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
Stacktrace or Logs
Not applicable — this is not a crash and produces no error, warning, or stack trace. The bug is a numeric layout discrepancy, demonstrated instead by the `onLayout` height measurements and screenshots above (same JS input, different rendered `height` depending on the device's raw font scale).
MANDATORY Reproducer
https://github.com/silverj0805/RNFontScaleRepro
Screenshots and Videos
From the linked reproducer, same maxFontSizeMultiplier={1.5} cap in every screenshot, only the device's actual font scale differs (1.5 vs 2.0):
The problem — case 1 (automatic lineHeight):
| Device fontScale = 1.5 | Device fontScale = 2.0 |
|---|---|
![]() |
![]() |
The problem — case 2 (explicit, pre-capped lineHeight):
| Device fontScale = 1.5 | Device fontScale = 2.0 |
|---|---|
![]() |
![]() |
The workaround — case 3 (allowFontScaling={false} + manual math), for comparison:
| Device fontScale = 1.5 | Device fontScale = 2.0 |
|---|---|
![]() |
![]() |
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
从链接的复现代码 src/App.tsx 开始,比较设备字体缩放受限和不受限时的 onLayout 高度。跟踪 Android Fabric 文本布局路径中对 line-height 的缩放处理,并使用报告的 maxFontSizeMultiplier 和显式 lineHeight 情况作为回归场景。完成标准是:当设备字体缩放超过 1.5 时,渲染高度仍保持受限,并且有一个覆盖这两种情况的回归测试。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- android, react-native
- 领域
- mobile-dev
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 活跃
- 描述清晰度
- 基本清楚
- 新手友好度
- 55/100





