Skip to content

Commit ae4fd33

Browse files
NickGerlemanjoemun
authored andcommitted
Fix New Arch handling of inline views when text truncated (facebook#49960)
Summary: Pull Request resolved: facebook#49960 Fixes facebook#49106 RN legacy arch, and web, will clip inline content which appears after elipsized text. This is the correct behavior, compared to new arch, which will put it in a random place depending on the platform. `line-clamp`: https://jsfiddle.net/7xgdke1b/ `text-overflow`: https://jsfiddle.net/7xgdke1b/2/ Fabric renderer does not, funnily enough, having an `isClipped` field on `TextMeasurement::Attachment` that is never used. This change propagates state for whether an attachment is beyond elipsized area to this measurement, then when we see it, we set empty layout results with `DisplayType::None` so that we don't create native views. We don't layout child views either, but this seems to work out okay, even when removing and re-adding `numberOfLines`. Changelog: [General][Fixed] - Fix New Arch handling of inline views when text truncated Reviewed By: mdvacca Differential Revision: D70922174 fbshipit-source-id: 8c1f4aadbf53ff64ce55b44d6c7953d9b2e40bc5
1 parent aac7715 commit ae4fd33

File tree

6 files changed

+87
-40
lines changed

6 files changed

+87
-40
lines changed

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -746,12 +746,16 @@ public static long measureText(
746746
int start = text.getSpanStart(placeholder);
747747
int line = layout.getLineForOffset(start);
748748
boolean isLineTruncated = layout.getEllipsisCount(line) > 0;
749-
// This truncation check works well on recent versions of Android (tested on 5.1.1 and
750-
// 6.0.1) but not on Android 4.4.4. The reason is that getEllipsisCount is buggy on
751-
// Android 4.4.4. Specifically, it incorrectly returns 0 if an inline view is the
752-
// first thing to be truncated.
753-
if (!(isLineTruncated && start >= layout.getLineStart(line) + layout.getEllipsisStart(line))
754-
|| start >= layout.getLineEnd(line)) {
749+
boolean isAttachmentTruncated =
750+
line > calculatedLineCount
751+
|| (isLineTruncated
752+
&& start >= layout.getLineStart(line) + layout.getEllipsisStart(line));
753+
int attachmentPosition = attachmentIndex * 2;
754+
if (isAttachmentTruncated) {
755+
attachmentsPositions[attachmentPosition] = Float.NaN;
756+
attachmentsPositions[attachmentPosition + 1] = Float.NaN;
757+
attachmentIndex++;
758+
} else {
755759
float placeholderWidth = placeholder.getWidth();
756760
float placeholderHeight = placeholder.getHeight();
757761
// Calculate if the direction of the placeholder character is Right-To-Left.
@@ -804,7 +808,6 @@ public static long measureText(
804808
}
805809
// Vertically align the inline view to the baseline of the line of text.
806810
float placeholderTopPosition = layout.getLineBaseline(line) - placeholderHeight;
807-
int attachmentPosition = attachmentIndex * 2;
808811

809812
// The attachment array returns the positions of each of the attachments as
810813
attachmentsPositions[attachmentPosition] =

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,14 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) {
260260

261261
auto& layoutableShadowNode =
262262
dynamic_cast<LayoutableShadowNode&>(*clonedShadowNode);
263+
const auto& attachmentMeasurement = measurement.attachments[i];
264+
if (attachmentMeasurement.isClipped) {
265+
layoutableShadowNode.setLayoutMetrics(
266+
LayoutMetrics{.displayType = DisplayType::None});
267+
continue;
268+
}
263269

264-
auto attachmentFrame = measurement.attachments[i].frame;
270+
auto attachmentFrame = attachmentMeasurement.frame;
265271
attachmentFrame.origin.x += layoutMetrics.contentInsets.left;
266272
attachmentFrame.origin.y += layoutMetrics.contentInsets.top;
267273

packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
#include "TextLayoutManager.h"
99

10+
#include <span>
11+
#include <utility>
12+
1013
#include <react/common/mapbuffer/JReadableMapBuffer.h>
1114
#include <react/jni/ReadableNativeMap.h>
1215
#include <react/renderer/attributedstring/conversions.h>
@@ -117,33 +120,39 @@ TextMeasurement doMeasure(
117120
maximumSize.height,
118121
attachmentPositions);
119122

120-
jfloat* attachmentData =
121-
env->GetFloatArrayElements(attachmentPositions, nullptr);
123+
jfloat* attachmentDataElements =
124+
env->GetFloatArrayElements(attachmentPositions, nullptr /*isCopy*/);
125+
std::span<float> attachmentData{
126+
attachmentDataElements, static_cast<size_t>(attachmentCount * 2)};
122127

123128
auto attachments = TextMeasurement::Attachments{};
124129
if (attachmentCount > 0) {
125-
int attachmentIndex = 0;
126130
for (const auto& fragment : attributedString.getFragments()) {
127131
if (fragment.isAttachment()) {
128-
float top = attachmentData[attachmentIndex * 2];
129-
float left = attachmentData[attachmentIndex * 2 + 1];
130-
float width = fragment.parentShadowView.layoutMetrics.frame.size.width;
131-
float height =
132-
fragment.parentShadowView.layoutMetrics.frame.size.height;
133-
134-
auto rect = facebook::react::Rect{
135-
.origin = {.x = left, .y = top},
136-
.size = facebook::react::Size{.width = width, .height = height}};
137-
attachments.push_back(
138-
TextMeasurement::Attachment{.frame = rect, .isClipped = false});
139-
attachmentIndex++;
132+
float top = attachmentData[attachments.size() * 2];
133+
float left = attachmentData[attachments.size() * 2 + 1];
134+
if (std::isnan(top) || std::isnan(left)) {
135+
attachments.push_back(
136+
TextMeasurement::Attachment{.frame = Rect{}, .isClipped = true});
137+
} else {
138+
float width =
139+
fragment.parentShadowView.layoutMetrics.frame.size.width;
140+
float height =
141+
fragment.parentShadowView.layoutMetrics.frame.size.height;
142+
143+
auto rect = facebook::react::Rect{
144+
.origin = {.x = left, .y = top},
145+
.size = facebook::react::Size{.width = width, .height = height}};
146+
attachments.push_back(
147+
TextMeasurement::Attachment{.frame = rect, .isClipped = false});
148+
}
140149
}
141150
}
142151
}
143152

144153
// Clean up allocated ref
145154
env->ReleaseFloatArrayElements(
146-
attachmentPositions, attachmentData, JNI_ABORT);
155+
attachmentPositions, attachmentDataElements, JNI_ABORT);
147156
env->DeleteLocalRef(attachmentPositions);
148157

149158
return TextMeasurement{.size = size, .attachments = attachments};

packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -375,19 +375,27 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage
375375
return;
376376
}
377377

378-
CGSize attachmentSize = attachment.bounds.size;
379-
CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer];
380-
381-
CGRect frame;
382-
CGFloat baseline = [layoutManager locationForGlyphAtIndex:range.location].y;
383-
384-
frame = {{glyphRect.origin.x, glyphRect.origin.y + baseline - attachmentSize.height}, attachmentSize};
385-
386-
auto rect = facebook::react::Rect{
387-
facebook::react::Point{frame.origin.x, frame.origin.y},
388-
facebook::react::Size{frame.size.width, frame.size.height}};
389-
390-
attachments.push_back(TextMeasurement::Attachment{rect, false});
378+
NSRange attachmentGlyphRange = [layoutManager glyphRangeForCharacterRange:range
379+
actualCharacterRange:NULL];
380+
NSRange truncatedRange =
381+
[layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:attachmentGlyphRange.location];
382+
if (truncatedRange.location != NSNotFound && attachmentGlyphRange.location >= truncatedRange.location) {
383+
attachments.push_back(TextMeasurement::Attachment{.isClipped = true});
384+
} else {
385+
CGSize attachmentSize = attachment.bounds.size;
386+
CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer];
387+
388+
CGRect frame;
389+
CGFloat baseline = [layoutManager locationForGlyphAtIndex:range.location].y;
390+
391+
frame = {{glyphRect.origin.x, glyphRect.origin.y + baseline - attachmentSize.height}, attachmentSize};
392+
393+
auto rect = facebook::react::Rect{
394+
facebook::react::Point{frame.origin.x, frame.origin.y},
395+
facebook::react::Size{frame.size.width, frame.size.height}};
396+
397+
attachments.push_back(TextMeasurement::Attachment{.frame = rect, .isClipped = false});
398+
}
391399
}];
392400

393401
return TextMeasurement{{size.width, size.height}, attachments};

packages/rn-tester/js/examples/Text/TextExample.android.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import type {RNTesterModule} from '../../types/RNTesterTypes';
1414

15+
import hotdog from '../../assets/hotdog.jpg';
1516
import RNTesterText from '../../components/RNTesterText';
1617
import TextLegend from '../../components/TextLegend';
1718
import TextAdjustsDynamicLayoutExample from './TextAdjustsDynamicLayoutExample';
@@ -20,6 +21,7 @@ import TextInlineViewsExample from './TextInlineViewsExample';
2021
const TextInlineView = require('../../components/TextInlineView');
2122
const React = require('react');
2223
const {
24+
Image,
2325
LayoutAnimation,
2426
StyleSheet,
2527
Text,
@@ -515,7 +517,7 @@ function MaxFontSizeMultiplierExample(props: {}): React.Node {
515517

516518
function NumberOfLinesExample(props: {}): React.Node {
517519
return (
518-
<>
520+
<View testID="number-of-lines">
519521
<RNTesterText numberOfLines={1} style={styles.wrappedText}>
520522
Maximum of one line no matter now much I write here. If I keep writing
521523
it{"'"}ll just truncate after one line
@@ -532,11 +534,19 @@ function NumberOfLinesExample(props: {}): React.Node {
532534
RNTesterText of two lines no matter now much I write here. If I keep
533535
writing it{"'"}ll just truncate after two lines
534536
</RNTesterText>
537+
<RNTesterText numberOfLines={1} style={{marginTop: 20}}>
538+
The hotdog should be truncated. The hotdog should be truncated. The
539+
hotdog should be truncated. The hotdog should be truncated. The hotdog
540+
should be truncated. The hotdog should be truncated. The hotdog should
541+
be truncated. The hotdog should be truncated. The hotdog should be
542+
truncated. The hotdog should be truncated.
543+
<Image source={hotdog} style={{height: 12}} />
544+
</RNTesterText>
535545
<RNTesterText style={[{marginTop: 20}, styles.wrappedText]}>
536546
No maximum lines specified no matter now much I write here. If I keep
537547
writing it{"'"}ll just keep going and going
538548
</RNTesterText>
539-
</>
549+
</View>
540550
);
541551
}
542552

packages/rn-tester/js/examples/Text/TextExample.ios.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import type {RNTesterModule} from '../../types/RNTesterTypes';
1414

15+
import hotdog from '../../assets/hotdog.jpg';
1516
import RNTesterText from '../../components/RNTesterText';
1617
import TextLegend from '../../components/TextLegend';
1718
import TextInlineViewsExample from './TextInlineViewsExample';
@@ -20,6 +21,7 @@ const TextInlineView = require('../../components/TextInlineView');
2021
const React = require('react');
2122
const {
2223
Button,
24+
Image,
2325
LayoutAnimation,
2426
Platform,
2527
Text,
@@ -1102,9 +1104,10 @@ const examples = [
11021104
},
11031105
{
11041106
title: 'numberOfLines attribute',
1107+
name: 'numberOfLines',
11051108
render: function (): React.Node {
11061109
return (
1107-
<View>
1110+
<View testID="number-of-lines">
11081111
<Text numberOfLines={1}>
11091112
Maximum of one line, no matter how much I write here. If I keep
11101113
writing, it{"'"}ll just truncate after one line.
@@ -1113,6 +1116,14 @@ const examples = [
11131116
Maximum of two lines, no matter how much I write here. If I keep
11141117
writing, it{"'"}ll just truncate after two lines.
11151118
</Text>
1119+
<Text numberOfLines={1} style={{marginTop: 20}}>
1120+
The hotdog should be truncated. The hotdog should be truncated. The
1121+
hotdog should be truncated. The hotdog should be truncated. The
1122+
hotdog should be truncated. The hotdog should be truncated. The
1123+
hotdog should be truncated. The hotdog should be truncated. The
1124+
hotdog should be truncated. The hotdog should be truncated.
1125+
<Image source={hotdog} style={{height: 12}} />
1126+
</Text>
11161127
<Text style={{marginTop: 20}}>
11171128
No maximum lines specified, no matter how much I write here. If I
11181129
keep writing, it{"'"}ll just keep going and going.

0 commit comments

Comments
 (0)