Repository navigation
Conversation
…graph LayoutAnimation interpolates a paragraph's frame on every frame, and RCTParagraphComponentView laid out its text in that intermediate size. With an unlimited number of lines the text container clips its last line instead of wrapping it, so the last visible line showed words from the next line, cut off at the edge, and they jumped down once the next line was revealed. ParagraphState (outside of Android) now stores the size of the content frame the paragraph was laid out with, and the view lays out text in that size and clips it to its current bounds, like the old architecture did. LayoutAnimation keeps the final state while it interpolates the frame, so this needs no animation-specific code. Fixes react#59161 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
Fixes #59161.
When
LayoutAnimationchanges the height of a<Text>on iOS (for example a "read more" toggle fromnumberOfLines={2}to no limit), words jump between lines during the animation instead of the text unrolling with its final line breaks.Current behavior. LayoutAnimation interpolates the paragraph's
LayoutMetricson every frame.RCTParagraphComponentViewuses that intermediate content frame as its drawing frame, sodrawRect:lays the text out again in a container of the in-between size. With no line limit,RCTTextLayoutManagersets the container'slineBreakModetoNSLineBreakByClipping, which applies to the last line that fits: that line isn't word-wrapped, it takes the rest of the text and clips it at the right edge. So on every frame the last visible line shows words that belong to the next line, cut off at the edge, and they jump down once the next line is revealed. Lines also appear only once they fit completely. Width animations have the same problem sideways: the text re-wraps on every frame.The old architecture didn't have this problem.
RCTTextShadowViewlaid the text out once, at the final frame size, and the animation only changed the view's frame. Android (withenableLayoutAnimationsOnAndroid) also unrolls height changes correctly.With this change. The cxx
ParagraphState(used outside of Android) stores the size of the content frame the paragraph was laid out with.ParagraphShadowNodesets it inlayout(), where the layout metrics are final.RCTParagraphComponentViewlays the text out in that size and lets the text view clip it to its current bounds. LayoutAnimation already builds every intermediate view from the final one and never interpolates state ("keep the mounting layer consistent with the final state"), so this needs no animation-specific code.When nothing is animating, the stored size equals the current content frame size: layout metrics are only set together with a
layout()call (YogaLayoutableShadowNode::layout,layoutTree). Static rendering is therefore unchanged, including text whose frame is shorter than its content.Android keeps its current state handling (
#ifdef ANDROID): its views don't use the size, and a state update there goes through JNI and resets the text.I also tried the one-line alternative of using
NSLineBreakByWordWrappingfor unlimited lines. It stops the words from jumping, but lines still pop in only once they fit, and it changes the static rendering of text whose frame is shorter than its content.Cost. A paragraph whose size changes without a content change now gets a new
ParagraphStateon iOS. Such a size change already re-measures and redraws the text, and on currentmainthe attributed string comparison inupdateStateIfNeededfails after layout anyway (see #59063), so iOS already creates a new state on most layouts.Changelog:
[IOS] [FIXED] - Fix words jumping between lines while
LayoutAnimationresizes aTextTest Plan:
Before (left) and after (right), RNTester Playground, iPhone Air simulator (iOS 27.0), 3-second linear
LayoutAnimation.Expand from
numberOfLines={2}to no limit. Before: the last visible line is clipped at the edge and there is a gap below it. After: the final lines are revealed, and the partially visible line is clipped mid-line.Width 50% → 100%. Before: the text re-wraps on every frame. After: it keeps its final line breaks and is uncovered from the right.
Videos of both are at the end of the test plan.
Repro used for the issue and the videos:
heightshorter than the text,numberOfLines={2}with ellipsis, padding + border +textAlign: 'center'+lineHeight, nested styles with an inline view, andflexShrinkin a fixed-height container.yarn fantom packages/react-native/Libraries/Text/__tests__/Text-itest.js packages/react-native/Libraries/LayoutAnimation/__tests__/LayoutAnimation-itest.jsgives 157 passed. Fantom builds the same non-AndroidParagraphStatepath as iOS. To build the tester locally I had to add-Wno-error=deprecated-literal-operator, because the bundlednlohmann/json.hppfails with Xcode 27's clang; that's unrelated to this change../gradlew :packages:rn-tester:android:app:installDebug -PreactNativeArchitectures=arm64-v8abuilds. WithenableLayoutAnimationsOnAndroidforced on locally, the expand animation unrolls correctly before and after this change. I noticed separately that Android re-wraps text during width animations, becauseSurfaceMountingManager.updateLayoutcallsmeasure()on every frame andTextViewrebuilds its layout there; the old architecture doesn't. This PR doesn't address that.--dry-run -Werror) passes on the changed files.Expand, before (left) and after (right):
ios-expand-before-after.mp4
Width animation, before (left) and after (right):
ios-width-before-after.mp4
🤖 Generated with Claude Code