[Android][Fabric] Text keeps its pre-change width after Bold text is turned on at runtime: TextMeasureCache key has no font weight adjustment
Maintainer antworten meist innerhalb von 2 Tagen
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Anfängerfreundlichkeit
- 45/100
Rechercherichtung
Read ReactCommon's TextMeasureCache.h and the Android TextLayoutManager cache path, then trace how FabricUIManager.measureText passes the font weight adjustment into measurement. Reproduce with the provided RNTester Playground example and Rubik family, toggling Bold text while the app is open. Done when text measured before the change uses widths appropriate to the adjusted face after returning to the app, while the described cache behavior is covered.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Description
On Android with the New Architecture, when a custom font family has more than one weight registered (each weight its own face) and the user turns on the system Bold text setting while the app is running, Text that was measured before the change keeps its old measured width but draws with the heavier face. Single-line labels are cut: "Create account" renders as "Create", "Sign in" as "Sign", "Forgot password?" as "Forgot".
- It persists through a second activity recreate minutes later, and clears only after a cold start (process death).
- Strings first measured after the change (a screen opened afterwards) are whole.
- With only one face per family name the labels are not cut: the adjusted weight then resolves to a synthetic bold of the same face, which keeps the advance widths, so the stale width still fits.
This looks like a follow-up to #57118, the change for #54931. Since that change, Fabric measurement applies the adjustment, read live at measure time: FabricUIManager.measureText passes ReactTypefaceUtils.getFontWeightAdjustment(mReactApplicationContext) (Configuration.fontWeightAdjustment, 300 with Bold text on) to TextLayoutManager.measureText, and ReactTypefaceUtils.applyFontWeightAdjustment turns weight 400 into Typeface.create(base, 700, italic). With a multi-face family that selects the real 700 face, which is wider.
What the cache key includes (read from the 0.88.0 source; TextMeasureCache.h on main today differs only in its includes):
TextMeasureCacheKeyis{attributedString, paragraphAttributes, layoutConstraints, pointScaleFactor}.LineMeasureCacheKeyis{attributedString, paragraphAttributes, size}.- Fragment equivalence (
areAttributedStringFragmentsEquivalentLayoutWise→areTextAttributesEquivalentLayoutWise, hashed bytextAttributesHashLayoutWise) compares the fragment string plusfontFamily,fontWeight,fontStyle,fontVariant,fontVariationSettings,allowFontScaling,dynamicTypeRamp,alignment,fontSize,fontSizeMultiplier,letterSpacing,lineHeightandmaxFontSizeMultiplier. - Nothing in the key reflects the font weight adjustment: no file under
ReactCommon/mentions it, andTextLayoutContextcarries onlypointScaleFactorandsurfaceId.fontWeightin the key is the style's own weight (unset here, which Android resolves as 400), before the adjustment is added on the Java side. - Nothing invalidates the cache on a configuration change:
SimpleThreadSafeCachehas noclear, and the AndroidTextLayoutManagerownstextMeasureCache_andlineMeasureCache_for its lifetime. - The cache path is on by default:
disableTextLayoutManagerCacheAndroid()andenablePreparedTextLayout()both default tofalseinReactNativeFeatureFlagsDefaults.h, soTextLayoutManager::measuregoes throughtextMeasureCache_.get(...). (WhenenablePreparedTextLayoutis on,PreparedTextCacheKeyhas no adjustment either.)
That matches the observed pattern: a process-lifetime cache keyed without the adjustment returns the pre-change size for any string measured before the change, and misses for new strings. We have not confirmed it by running with disableTextLayoutManagerCacheAndroid set to true.
A possible direction: make the adjustment (or a configuration generation) part of the key, for example through TextLayoutContext, or drop the text caches when the configuration's font weight adjustment changes. The iOS counterpart for a runtime Dynamic Type change is #57512.
Steps to reproduce
RNTester already registers a multi-weight family, Rubik (res/font/rubik.xml has faces at 300, 400, 500 and 700; RNTesterApplication calls ReactFontManager.getInstance().addCustomFont(this, "Rubik", R.font.rubik)), so the reproducer is an edit to packages/rn-tester/js/examples/Playground/RNTesterPlayground.js:
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
import * as React from 'react';
import {StyleSheet, Text, View} from 'react-native';
function Playground() {
return (
<View style={styles.container}>
{/* Row children are sized by their measured text width. */}
<View style={styles.row}>
<Text style={styles.label}>Create account</Text>
</View>
<View style={styles.row}>
<Text style={styles.label}>Forgot password?</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {padding: 10, gap: 12},
row: {flexDirection: 'row'},
label: {fontFamily: 'Rubik', fontSize: 17, backgroundColor: '#eee'},
});
export default {
title: 'Playground',
name: 'playground',
description: 'Test out new features and ideas.',
render: (): React.Node => <Playground />,
} as RNTesterModuleExample;
- With Bold text off (
adb shell settings put secure font_weight_adjustment 0), launch RNTester on an Android 12+ device or emulator and open Playground. Both labels are whole. - Leave the app open and turn Bold text on: Settings → Accessibility → Display size and text → Bold text, or
adb shell settings put secure font_weight_adjustment 300. - Return to the app (the activity is recreated; RNTester's
configChangesdoes not includefontWeightAdjustment). Expected: both labels whole, in the bold face. Actual: the labels draw in the bold face inside their old width and are cut. - Kill the app and start it again with Bold text still on: the labels are whole.
Without RNTester, the same family can be registered in any app with an XML font family under res/font/ (two <font> entries, app:fontWeight="400" and "700") and ReactFontManager.getInstance().addCustomFont(context, "Family", R.font.family) in Application.onCreate, or in an Expo app with expo-font's FontFamilyDefinition (useFonts([{fontFamily: 'Family', fontDefinitions: [{path: regular, weight: 400}, {path: bold, weight: 700}]}])), which builds a multi-face Typeface and registers it through ReactFontManager.addCustomFont(fontFamily, typeface).
We observed this in an app through the expo-font path (Expo SDK 58), with several families each registered as a 400 face and a 700 face. The RNTester form above is that setup reduced to one label; we have not run it separately.
React Native Version
0.88.0
Affected Platforms
Runtime - Android
Areas
Fabric - The New Renderer
Output of npx @react-native-community/cli info
Collected by hand (Expo-managed app; the community CLI is not installed in it):
System:
OS: macOS 27.0.1
Binaries:
Node: 22.23.2
Java: OpenJDK 17.0.19
npmPackages:
react: 19.3.0
react-native: 0.88.0
expo: 58.0.16
expo-font: 58.0.9
Android:
newArchEnabled: true
hermesEnabled: true
compileSdk 37, targetSdk 36 (React Native 0.88.0 defaults)
Device:
Android emulator, Pixel 8, API 35 (Android 15)
Stacktrace or Logs
No crash or log output: a layout-only defect.
MANDATORY Reproducer
Inline above, as an edit to RNTesterPlayground.js (RNTester's bundled Rubik family needs no font files). We can't publish a separate repository for this.
Screenshots and Videos
No screenshots attached. Observed in the app: with Bold text switched on while the app was open, "Create account" showed "Create", "Sign in" showed "Sign" and "Forgot password?" showed "Forgot"; after a cold start with Bold text still on, all three were whole.
- Vorherrschende Sprache
- C++
- Sterne
- 127k
- Forks
- 25.4k
- Ø Merge
- 3 T. 1 Std.
- Gemergte PRs (30 T.)
- 108
Entwicklungsumgebung
- Kein Dockerfile und keine Docker-Compose-Datei
- Hat eine Pull-Request-Vorlage
- Beitragsleitfaden lesen
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus react/react-native
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 70/100
react/react-native#59722 ·
Maintainer antworten meist innerhalb von 2 Tagen
-
Android: new WebSocket() crashes the app for a URL that OkHttp or java.net.URI rejectsEvtl. vergeben @sorinc03 hat das vor 1 Tag übernommen. Offen🌐Networking Needs: Author Feedback Needs: Repro Platform: Android
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
react/react-native#59704 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 2 Tagen
-
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 88/100
react/react-native#59694 ·
Maintainer antworten meist innerhalb von 2 Tagen
-
Image.resolveAssetSource's TypeScript declaration drops the null it returns for an unknown assetEvtl. vergeben @Hassan1305 hat das vor 3 Tagen übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
react/react-native#59648 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 2 Tagen
-
Linking.getInitialURL() returns the task's first link after a link relaunches a reclaimed Android appEvtl. vergeben @YevheniiKotyrlo hat das vor 20 Tagen übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
react/react-native#59006 · 2 Kommentare ·
Maintainer antworten meist innerhalb von 2 Tagen
Alle Issues in react/react-native
Ähnliche Issues
-
`BLMPOP` with COUNT 0 or numkeys 0 hangsEvtl. vergeben @ggyuchive hat das heute übernommen. Offenbug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
Maintainer antworten meist innerhalb von 1 Tag
-
needs triage
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
Maintainer antworten meist innerhalb von 1 Tag
-
gObjectFullbrightAlphaMaskProgram in newview/llviewershadermgr.cpp is defined but never initializedOffenbug triage
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 78/100
secondlife/viewer#6620 ·
Maintainer antworten meist innerhalb von 1 Tag
-
bug core p3
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
RunanywhereAI/runanywhere-sdks#1258 ·
Maintainer antworten meist innerhalb von 1 Tag
-
ripwire-router skill: stale language list (15 vs 26 supported; Elixir/PHP/Kotlin wrongly listed as unsupported)Evtl. vergeben @joyful-ii-V-I hat das heute übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 80/100
redhat-et/ripwire#521 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag