Skip to content

Commit 674163d

Browse files
Fix text wrapping in absolutely positioned elements on Android (facebook#56651)
Summary: ## Context On Android TV, text inside absolutely-positioned Views wraps unexpectedly when the computed width from Yoga is fractional. This manifests in Instagram TV as "Keep watching" breaking to two lines in the exit dialog when the button text is focused (rendered via an absolutely-positioned overlay for color animation). The root cause is a rounding mismatch in React Native's Android `TextLayoutManager`: `desiredWidth` (the text's needed width) uses `ceil()` (rounds up), but `layoutWidth` in `EXACTLY` mode uses `floor()` (rounds down). When Yoga passes a fractional width (e.g. 258.5px), the container gets `floor(258.5) = 258px` but the text needs `ceil(258.3) = 259px`, causing a 1px shortfall that triggers wrapping. ## Fix In `TextLayoutManager.createLayout()`, change `floor(width).toInt()` to `ceil(width).toInt()` for `YogaMeasureMode.EXACTLY` in **both** layout paths so the behavior is consistent regardless of which `Layout` class is chosen: - BoringLayout (single-line text that fits) - StaticLayout (multi-line or complex text) `YogaMeasureMode.AT_MOST` is intentionally left as `floor(width).toInt()`. `AT_MOST` is a constraint contract from Yoga ("do not exceed this width"), so flooring remains the correct conservative behavior — ceiling there could violate the constraint by up to 1px. The BoringLayout entry guard (`boring.width <= floor(width)`) is also left unchanged. If a boring text fails the guard, it falls through to the StaticLayout path, which now also ceils for `EXACTLY`, so no truncation results — the only effect is a slightly less optimal layout class choice in a narrow edge case. ## Why ceiling `EXACTLY` is safe `EXACTLY` means Yoga has guaranteed this width — the container has been allocated at least the full fractional width upstream. Ceiling the local layout width by 1px cannot exceed what Yoga has reserved, while flooring it produced a 1px shortfall that mismatched `desiredWidth`'s ceiling and triggered wrapping. The compensating mechanism in `calculateWidth()` — which returns the raw fractional width to Yoga for `EXACTLY` mode rather than the floored `layout.width` — is preserved, so Yoga's upstream allocation reasoning is unchanged. This was the subpixel-safety property introduced in D74685353; only the local pixel rounding inside `createLayout` changes from "round down 1px and risk wrapping" to "round up 1px and match `desiredWidth`". ## Notes This was confirmed as a problem with an absolutely positioned style with `left:0, right:0` applied. Width sizing was confirmed to be the issue when `left:-1, right:-1` resolved the issue. Further investigation found this fix in text sizing. Only `EXACTLY` is needed to fix the observed Instagram TV bug. `AT_MOST` is left untouched because the constraint semantics differ. ## Changelog: [Android] [Fixed] - Fix 1px text wrapping in absolutely positioned elements caused by fractional Yoga widths Differential Revision: D102920508
1 parent 795d902 commit 674163d

1 file changed

Lines changed: 2 additions & 3 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -619,8 +619,7 @@ internal object TextLayoutManager {
619619
(widthYogaMeasureMode == YogaMeasureMode.UNDEFINED || boring.width <= floor(width))
620620
) {
621621
val layoutWidth =
622-
if (widthYogaMeasureMode == YogaMeasureMode.EXACTLY) floor(width).toInt()
623-
else boring.width
622+
if (widthYogaMeasureMode == YogaMeasureMode.EXACTLY) ceil(width).toInt() else boring.width
624623
return BoringLayout.make(
625624
text,
626625
paint,
@@ -637,7 +636,7 @@ internal object TextLayoutManager {
637636

638637
val layoutWidth =
639638
when (widthYogaMeasureMode) {
640-
YogaMeasureMode.EXACTLY -> floor(width).toInt()
639+
YogaMeasureMode.EXACTLY -> ceil(width).toInt()
641640
YogaMeasureMode.AT_MOST -> min(desiredWidth, floor(width).toInt())
642641
else -> desiredWidth
643642
}

0 commit comments

Comments
 (0)