fix(android): keep ReactTextView line breaking advance-based on Android 15+ so the last line is not clipped (#58280)
Summary:
On Android 15+ (API 35), an app that targets API 35+ gets bounds-based line breaking in every `TextView` by default — the platform compat change `TextView#USE_BOUNDS_FOR_WIDTH`:
```java
// frameworks/base/core/java/android/widget/TextView.java
ChangeId
EnabledSince(targetSdkVersion = VERSION_CODES.VANILLA_ICE_CREAM)
public static final long USE_BOUNDS_FOR_WIDTH = 63938206;
…
if (!hasUseBoundForWidthValue) {
mUseBoundsForWidth = CompatChanges.isChangeEnabled(USE_BOUNDS_FOR_WIDTH);
}
```
React Native measures `<Text>` in `TextLayoutManager` with a `StaticLayout` that breaks lines on glyph **advances** (`buildLayout` never sets `setUseBoundsForWidth`). With `enablePreparedTextLayout` off (the default), the pixels on screen come from `ReactTextView`'s own `TextView` layout — `ReactTextView.setText()` hands the Spannable to `TextView` and `onDraw()` defers to `super.onDraw()`. That layout breaks lines on glyph **bounds**.
So measurement and painting disagree on where lines break. For any font whose ink overhangs its advance (script/cursive fonts, several OEM system fonts, emoji fallbacks), a line that fits at measure time can wrap at draw time. The extra line lands outside the Yoga-measured height and is simply never seen: **the last word of a `<Text>` disappears**, while the view is sized as if it were there.
This is the mechanism behind https://github.com/react/react-native/issues/56402 / https://github.com/react/react-native/issues/53286 (and the shape of https://github.com/react/react-native/issues/57957: content-sized parent, last line gone). It is independent of `lineHeight`, and it affects both shrink-wrapped single-line text and width-constrained wrapped paragraphs.
## The fix
Opt `ReactTextView` out of bounds-based breaking so the drawn layout uses the same advance-based line breaking as measurement. Applied in the constructor and again in `recycleView()` so recycled views cannot drift. The call is resolved reflectively, following the existing `setUseBoundsForWidth` pattern in `TextLayoutManager`, because some internal targets compile against an SDK older than 35 (see `AndroidVersion`).
This keeps the final layout on the advance-based behavior React Native has always had — the same principle https://github.com/react/react-native/issues/57117 states for the layouts it builds — but applies it where the pixels actually come from. It is complementary to https://github.com/react/react-native/issues/57117: that PR widens the *desired* width for `AT_MOST`/`UNDEFINED` measurement, which does not reach a width-constrained paragraph whose lines are re-broken by the `TextView` at draw time; this change makes both paths agree regardless of constraint mode.
Trade-off: React Native forgoes Android 15's automatic reservation of overhang space at the edges of a line (glyph ink may be clipped at the view edge as it was before Android 15). That is the pre-existing behavior on every prior Android version, and strictly better than losing whole words. A follow-up could make *measurement* bounds-aware instead (platform parity), but that changes wrapping app-wide and was the direction of the reverted https://github.com/react/react-native/issues/54721/#54871.
Fixes https://github.com/react/react-native/issues/56402
Related: https://github.com/react/react-native/issues/53286, https://github.com/react/react-native/issues/57957, https://github.com/react/react-native/issues/57117, https://github.com/react/react-native/issues/56864
## Changelog:
[ANDROID] [FIXED] - Text: the last line no longer disappears on Android 15+ when a font's glyphs overhang their advance (ReactTextView now breaks lines on advances, matching measurement)
Pull Request resolved: https://github.com/react/react-native/pull/58280
Test Plan:
### Deterministic repro (stock emulator, no custom font)
API 35/36 AVD, app targeting API 35+. Android's generic `cursive` family (Dancing Script) overhangs heavily. Inside a shrink-wrapping container:
```tsx
<View style={{ alignSelf: 'flex-start' }}>
<Text style={{ fontFamily: 'cursive', fontSize: 18, lineHeight: 27 }} allowFontScaling={false}>
Enjoy your coffee<Text style={{ color: 'green' }}> f</Text>
</Text>
</View>
```
**Before:** the green `f` is not painted. The view is sized for it (measure), but the `TextView` breaks the line on bounds, wraps the `f` to a second line, and that line is outside the measured height. Which strings trip it depends on where the bounds-based break falls relative to the advance-based one — in the rn-tester example below two of the four cursive rows lose the `f` — while a control row with a non-overhanging font (Roboto) always keeps it.
**After:** the `f` is painted on the first line.
**Before** (rn-tester `Text` example, API 36 emulator — the cursive column loses its `f` on two of the four rows; the default-font control column keeps every one):

**After** (same example, this branch):

### rn-tester
`Text` → **"Android 15+ glyph overhang (last line must not disappear)"** — the rows above, cursive on the left with a default-font control on the right. Every row must show its green `f`.
### Unit tests
`ReactTextViewTest`:
- `breaksLinesOnAdvancesLikeMeasurementOnApi35` — a freshly constructed `ReactTextView` reports `useBoundsForWidth == false` on API 35.
- `recyclingRestoresAdvanceBasedLineBreaking` — after `useBoundsForWidth = true`, `recycleView()` restores `false`.
Below API 35 the reflective lookup returns null and the view is untouched.
### Origin
Reported in production by a user on a Samsung SM-A566B (Android 16, One UI system font): trailing words vanished from chat messages while the message bubble was sized for the full text. Pinning a bundled font (Alef) in the app made it stop — consistent with the mechanism above — and the same symptom then reproduced on an AOSP emulator with the `cursive` family as shown here.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed By: christophpurrer
Differential Revision: D118632937
Pulled By: javache
fbshipit-source-id: bb60e5549132c22d635c7ef96a9dba940e61b8e1 I
Ido Yana committed
dcdb52bc80873c3d6697d930a28add09c542d073
Parent: 025a862
Committed by meta-codesync[bot] <215208954+meta-codesync[bot]@users.noreply.github.com>
on 9/11/2026, 10:53:55 AM