Skip to content

Commit eca3267

Browse files
tomekzawclaude
andcommitted
Keep iOS text line breaks stable while LayoutAnimation resizes a paragraph
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 #59161 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 27e7721 commit eca3267

4 files changed

Lines changed: 33 additions & 2 deletions

File tree

‎packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ - (void)_updateTextViewFrame
188188
CGRect textViewFrame = self.bounds;
189189
CGRect drawingFrame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
190190

191+
// Lay out the text in the size the paragraph has in the shadow tree rather than in the current frame, which
192+
// LayoutAnimation interpolates. Otherwise, line breaks change on every frame and words jump between lines.
193+
// The text view clips the text to the current bounds.
194+
if (_textView.state) {
195+
drawingFrame.size = RCTCGSizeFromSize(_textView.state->getData().layoutSize);
196+
}
197+
191198
if (ReactNativeFeatureFlags::enableIOSCompressedTextFrameAdjustment() && _textView.state &&
192199
drawingFrame.size.height > 0) {
193200
const auto &stateData = _textView.state->getData();

‎packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ void ParagraphShadowNode::updateStateIfNeeded(const Content& content) {
178178
auto& state = getStateData();
179179

180180
react_native_assert(textLayoutManager_);
181+
#ifdef ANDROID
181182
if (state.attributedString == content.attributedString) {
182183
return;
183184
}
@@ -187,6 +188,20 @@ void ParagraphShadowNode::updateStateIfNeeded(const Content& content) {
187188
content.attributedString,
188189
content.paragraphAttributes,
189190
textLayoutManager_});
191+
#else
192+
auto layoutSize = getLayoutMetrics().getContentFrame().size;
193+
if (state.attributedString == content.attributedString &&
194+
state.layoutSize == layoutSize) {
195+
return;
196+
}
197+
198+
setStateData(
199+
ParagraphState{
200+
content.attributedString,
201+
content.paragraphAttributes,
202+
textLayoutManager_,
203+
layoutSize});
204+
#endif
190205
}
191206

192207
MeasuredPreparedTextLayout* ParagraphShadowNode::findUsableLayout() {

‎packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ class ParagraphShadowNode
110110
void updateStateIfNeeded(const Content &content, const MeasuredPreparedTextLayout &layout);
111111

112112
/*
113-
* Creates a `State` object (with `AttributedText` and
114-
* `TextLayoutManager`) if needed.
113+
* Creates a `State` object (with `AttributedText`, `TextLayoutManager` and,
114+
* outside of Android, the size of the content frame) if needed.
115115
*/
116116
void updateStateIfNeeded(const Content &content);
117117

‎packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/ParagraphState.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <react/cxxstableapi/FrameworksGuard.h>
1111

1212
#include <React/Debug.h>
13+
#include <React/Graphics.h>
1314
#include <react/renderer/attributedstring/AttributedString.h>
1415
#include <react/renderer/attributedstring/ParagraphAttributes.h>
1516
#include <react/renderer/textlayoutmanager/TextLayoutManager.h>
@@ -39,6 +40,14 @@ class ParagraphState final {
3940
* `AttributedString`.
4041
*/
4142
std::weak_ptr<const TextLayoutManager> layoutManager;
43+
44+
/*
45+
* Size of the content frame that the paragraph was laid out with.
46+
* The mounted view can temporarily have a different size (e.g. while
47+
* LayoutAnimation interpolates its frame), so it lays out text in this size
48+
* to keep line breaks stable.
49+
*/
50+
Size layoutSize;
4251
};
4352

4453
} // namespace facebook::react

0 commit comments

Comments
 (0)