Fix Android text measurement to use the rendering TextView default typeface - #58036
pangziqiang wants to merge 1 commit into
Conversation
|
Hi @pangziqiang! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
The CLA is signed now — the reproducer PR #58783 already picked up the |
…peface Auto-width <Text> can clip its last glyph(s) when the system default font is replaced/bolded at the theme level (e.g. a bold system font or a font replacement app). Measurement resets the TextPaint typeface to Typeface.DEFAULT, while ReactTextView renders with the TextView default typeface that inherits the system font configuration. When those diverge, the measured width is narrower than what is drawn, so trailing glyphs get pushed out of the view. Update updateTextPaint so that, when no explicit font family/weight/style is set and there is no font weight adjustment, the measurement paint uses the same default typeface the rendering TextView uses. The typeface is resolved from a single cached TextView (application context, no per-measure allocation) and re-resolved when the Configuration changes.
dbf37c3 to
da6a2b2
Compare
|
@sbaiahmed1 — this is the patched-build validation you asked for on #57950. You said "the change itself is small; the risk is entirely in verification", so here is the verification. What was tested RN 0.78.3 still ships Devices Both affected, both HyperOS with a theme-level font replacement (MI Lan Pro VF 小米兰亭 Pro at the heaviest weight, identical theme font file on both, md5
Result — native line metrics from
Every patched value equals the 0.73.11 old-architecture reference exactly (6/6 samples on the phone, 5/5 on the tablet). Descender goes from ~0.08 em back to ~0.29 em, the line box from ~1.02 em back to ~1.36 em. No Reproducer — #58783 adds a minimal RNTester Playground reproducer that needs no bundled font asset, plus the exact numbers it prints on affected / patched / stock devices. The full write-up (shared measurement-paint instrumentation, the Please take a look when you have time. |
Summary
Auto-width
<Text>can clip its last glyph(s) on devices where the system default font is replaced/bolded at the theme level (e.g. Xiaomi HyperOS "全加粗" / a font replacement app). Measurement uses a bareTextPaintwhose default typeface isTypeface.DEFAULT(the static normal font), whileReactTextViewrenders with theTextViewdefault typeface that inherits the system font configuration. When those diverge, the measured width is narrower than what is actually drawn, so the trailing glyph(s) are pushed out of the view.Fixes #57950.
Changelog:
Root cause
Fabric measures every
<Text>with a single shared, thread-localTextPaint. For text without anexplicit font family/weight/style,
updateTextPaint()restores the default withpaint.reset(); paint.setTypeface(null). That does not reliably restore the typeface:Paint.reset()does not clear the resolved typeface, andPaint.setTypeface(null)is a no-op when the paint's Java-level typeface is alreadynull(it short-circuits inside
Paint.setTypeface).So the paint keeps the last typeface that was set explicitly. Instrumented inside the app process
on an affected device (48px, same
Paintinstance,measureText("A")in px):wA36.0wA30.0setTypeface(<icon font>)wA33.0wA29.0reset()setTypeface(null)Any app that sets an explicit font on some
<Text>nodes then poisons the measurement of everyplain
<Text>. The most common trigger is react-native-vector-icons: its bundledicomoon.ttfhas a1.000 em line box and a 0.0625 em descender, so measured text gets a ~1.02 em box while
ReactTextViewdraws with the system font at ~1.33 em — the last glyphs / descenders are clipped.Setting an explicit
lineHeighthides it.This also explains the partial reporting: nodes with explicit font attributes take the other branch of
updateTextPaint()and measure correctly, so a singlefontSizecould produce two different lineboxes on one screen (48px text: 49.7/23.7/73.3 px and 50.3/13.7/64.0 px on an affected device before
the fix).
Note that on stock Android the accessibility "bold text" setting (
fontWeightAdjustment) updates theprocess-wide
Typeface.DEFAULT, so the bare measurement paint picks up the same metrics andmeasurement/rendering agree there — which is why this cannot be reproduced on stock emulators.
Change
When no explicit font family/weight/style is set and there is no font weight adjustment, resolve the default typeface from the same source the rendering
TextViewuses:getSystemDefaultTypeface()reads the typeface of a single cachedTextViewcreated from the application context (no per-measure allocation, no activity leak) and re-resolves it whenever theConfigurationchanges (fontScale / fontWeightAdjustment / locale). AContextis threaded fromFabricUIManager(which already threadsassetsandfontWeightAdjustment) down toupdateTextPaint.On unmodified systems the
TextViewdefault typeface is the same normal face, so behavior and measurements are unchanged.Test plan
TextLayoutManagerFontWeightAdjustmentTestto assert that, without font weight adjustment, the measurement typeface equals the renderingTextViewdefault typeface.Englishmeasures 57dp == drawn 56.67dp, all 7 glyphs present; time/CJK text unaffected; layouts remain correct.Device verification (unpatched vs patched vs old architecture)
Two affected devices, RN 0.78.3 with this change backported (
TextLayoutManageris still Java on the0.78 branch). Native line metrics from
onTextLayout(dp: ascender/descender/height):Every patched value equals the 0.73.11 old-architecture reference exactly (6/6 and 5/5 samples).
Descender goes from ~0.08 em back to ~0.29 em and the line box from ~1.02 em back to ~1.36 em.
A third device with an unmodified ROM (AOSP 16) does not reproduce and the change is a behavioural
no-op there.
A minimal reproducer (no bundled font required) is in
#57950 (comment)
Notes for reviewers
TextViewon the affected devices. Devices that apply theme-dependent default fonts per-Activity may need the themed context instead.Configurationchange; a live font-family replacement that does not changeConfigurationwould still need an app restart to re-apply, which is the normal case.