diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index da6658cc1319..6e5fd181563a 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -188,6 +188,13 @@ - (void)_updateTextViewFrame CGRect textViewFrame = self.bounds; CGRect drawingFrame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); + // Lay out the text in the size the paragraph has in the shadow tree rather than in the current frame, which + // LayoutAnimation interpolates. Otherwise, line breaks change on every frame and words jump between lines. + // The text view clips the text to the current bounds. + if (_textView.state) { + drawingFrame.size = RCTCGSizeFromSize(_textView.state->getData().layoutSize); + } + if (ReactNativeFeatureFlags::enableIOSCompressedTextFrameAdjustment() && _textView.state && drawingFrame.size.height > 0) { const auto &stateData = _textView.state->getData(); diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp index 7602b616e798..bad770b36246 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp @@ -178,6 +178,7 @@ void ParagraphShadowNode::updateStateIfNeeded(const Content& content) { auto& state = getStateData(); react_native_assert(textLayoutManager_); +#ifdef ANDROID if (state.attributedString == content.attributedString) { return; } @@ -187,6 +188,20 @@ void ParagraphShadowNode::updateStateIfNeeded(const Content& content) { content.attributedString, content.paragraphAttributes, textLayoutManager_}); +#else + auto layoutSize = getLayoutMetrics().getContentFrame().size; + if (state.attributedString == content.attributedString && + state.layoutSize == layoutSize) { + return; + } + + setStateData( + ParagraphState{ + content.attributedString, + content.paragraphAttributes, + textLayoutManager_, + layoutSize}); +#endif } MeasuredPreparedTextLayout* ParagraphShadowNode::findUsableLayout() { diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h index 5d0537d363df..f569067717bb 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h @@ -110,8 +110,8 @@ class ParagraphShadowNode void updateStateIfNeeded(const Content &content, const MeasuredPreparedTextLayout &layout); /* - * Creates a `State` object (with `AttributedText` and - * `TextLayoutManager`) if needed. + * Creates a `State` object (with `AttributedText`, `TextLayoutManager` and, + * outside of Android, the size of the content frame) if needed. */ void updateStateIfNeeded(const Content &content); diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/ParagraphState.h b/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/ParagraphState.h index 07783b401366..bf3ca15d2182 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/ParagraphState.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/platform/cxx/react/renderer/components/text/ParagraphState.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -39,6 +40,14 @@ class ParagraphState final { * `AttributedString`. */ std::weak_ptr layoutManager; + + /* + * Size of the content frame that the paragraph was laid out with. + * The mounted view can temporarily have a different size (e.g. while + * LayoutAnimation interpolates its frame), so it lays out text in this size + * to keep line breaks stable. + */ + Size layoutSize; }; } // namespace facebook::react