Skip to content

Commit 5eed872

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Split Java TextLayoutManager Logic (#51066)
Summary: Pull Request resolved: #51066 Splits up some of the `measureText` code in Android TextLayoutManager, ahead of D73970149, which adds `measurePreparedLayout` to reuse most of this logic. Most significant change, is we pull out logic to iterate and retrieve metrics for attachments, since `measurePreparedLayout`, needs to also return dimensions, and fill into ArrayList, instead of received buffer. Changelog: [Internal] Reviewed By: rshest Differential Revision: D74035246 fbshipit-source-id: 2a0f5b171c4343e26ffa5b2538a0018939b06773
1 parent 989b3f6 commit 5eed872

1 file changed

Lines changed: 149 additions & 93 deletions

File tree

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

Lines changed: 149 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import androidx.annotation.NonNull;
2727
import androidx.annotation.Nullable;
2828
import androidx.core.util.Preconditions;
29+
import com.facebook.infer.annotation.Assertions;
2930
import com.facebook.react.bridge.ReactNoCrashSoftException;
3031
import com.facebook.react.bridge.ReactSoftExceptionLogger;
3132
import com.facebook.react.bridge.WritableArray;
@@ -695,22 +696,54 @@ public static long measureText(
695696
width,
696697
height,
697698
reactTextViewManagerCallback);
698-
Spanned text = (Spanned) layout.getText();
699-
700-
if (text == null) {
701-
return 0;
702-
}
703699

704700
int maximumNumberOfLines =
705701
paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES)
706702
? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
707703
: ReactConstants.UNSET;
708704

709-
int calculatedLineCount =
710-
maximumNumberOfLines == ReactConstants.UNSET || maximumNumberOfLines == 0
711-
? layout.getLineCount()
712-
: Math.min(maximumNumberOfLines, layout.getLineCount());
705+
Spanned text = (Spanned) layout.getText();
713706

707+
int calculatedLineCount = calculateLineCount(layout, maximumNumberOfLines);
708+
float calculatedWidth =
709+
calculateWidth(layout, text, width, widthYogaMeasureMode, calculatedLineCount);
710+
float calculatedHeight =
711+
calculateHeight(layout, text, height, heightYogaMeasureMode, calculatedLineCount);
712+
713+
if (attachmentsPositions != null) {
714+
int attachmentIndex = 0;
715+
int lastAttachmentFoundInSpan;
716+
717+
AttachmentMetrics metrics = new AttachmentMetrics();
718+
for (int i = 0; i < text.length(); i = lastAttachmentFoundInSpan) {
719+
lastAttachmentFoundInSpan =
720+
nextAttachmentMetrics(layout, text, calculatedWidth, calculatedLineCount, i, metrics);
721+
if (metrics.wasFound) {
722+
attachmentsPositions[attachmentIndex] = PixelUtil.toDIPFromPixel(metrics.top);
723+
attachmentsPositions[attachmentIndex + 1] = PixelUtil.toDIPFromPixel(metrics.left);
724+
attachmentIndex += 2;
725+
}
726+
}
727+
}
728+
729+
float widthInSP = PixelUtil.toDIPFromPixel(calculatedWidth);
730+
float heightInSP = PixelUtil.toDIPFromPixel(calculatedHeight);
731+
732+
return YogaMeasureOutput.make(widthInSP, heightInSP);
733+
}
734+
735+
private static int calculateLineCount(Layout layout, int maximumNumberOfLines) {
736+
return maximumNumberOfLines == ReactConstants.UNSET || maximumNumberOfLines == 0
737+
? layout.getLineCount()
738+
: Math.min(maximumNumberOfLines, layout.getLineCount());
739+
}
740+
741+
private static float calculateWidth(
742+
Layout layout,
743+
Spanned text,
744+
float width,
745+
YogaMeasureMode widthYogaMeasureMode,
746+
int calculatedLineCount) {
714747
// Instead of using `layout.getWidth()` (which may yield a significantly larger width for
715748
// text that is wrapping), compute width using the longest line.
716749
float calculatedWidth = 0;
@@ -741,7 +774,15 @@ public static long measureText(
741774
if (android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
742775
calculatedWidth = (float) Math.ceil(calculatedWidth);
743776
}
777+
return calculatedWidth;
778+
}
744779

780+
private static float calculateHeight(
781+
Layout layout,
782+
Spanned text,
783+
float height,
784+
YogaMeasureMode heightYogaMeasureMode,
785+
int calculatedLineCount) {
745786
float calculatedHeight = height;
746787
if (heightYogaMeasureMode != YogaMeasureMode.EXACTLY) {
747788
// StaticLayout only seems to change its height in response to maxLines when ellipsizing, so
@@ -751,98 +792,113 @@ public static long measureText(
751792
calculatedHeight = height;
752793
}
753794
}
795+
return calculatedHeight;
796+
}
754797

798+
private static class AttachmentMetrics {
799+
boolean wasFound;
800+
float top;
801+
float left;
802+
float width;
803+
float height;
804+
}
805+
806+
private static int nextAttachmentMetrics(
807+
Layout layout,
808+
Spanned text,
809+
float calculatedWidth,
810+
int calculatedLineCount,
811+
int i,
812+
AttachmentMetrics metrics) {
755813
// Calculate the positions of the attachments (views) that will be rendered inside the
756814
// Spanned Text. The following logic is only executed when a text contains views inside.
757815
// This follows a similar logic than used in pre-fabric (see ReactTextView.onLayout method).
758-
int attachmentIndex = 0;
759-
int lastAttachmentFoundInSpan;
760-
for (int i = 0; i < text.length(); i = lastAttachmentFoundInSpan) {
761-
lastAttachmentFoundInSpan =
762-
text.nextSpanTransition(i, text.length(), TextInlineViewPlaceholderSpan.class);
763-
TextInlineViewPlaceholderSpan[] placeholders =
764-
text.getSpans(i, lastAttachmentFoundInSpan, TextInlineViewPlaceholderSpan.class);
765-
for (TextInlineViewPlaceholderSpan placeholder : placeholders) {
766-
int start = text.getSpanStart(placeholder);
767-
int line = layout.getLineForOffset(start);
768-
boolean isLineTruncated = layout.getEllipsisCount(line) > 0;
769-
boolean isAttachmentTruncated =
770-
line > calculatedLineCount
771-
|| (isLineTruncated
772-
&& start >= layout.getLineStart(line) + layout.getEllipsisStart(line));
773-
int attachmentPosition = attachmentIndex * 2;
774-
if (isAttachmentTruncated) {
775-
attachmentsPositions[attachmentPosition] = Float.NaN;
776-
attachmentsPositions[attachmentPosition + 1] = Float.NaN;
777-
attachmentIndex++;
778-
} else {
779-
float placeholderWidth = placeholder.getWidth();
780-
float placeholderHeight = placeholder.getHeight();
781-
// Calculate if the direction of the placeholder character is Right-To-Left.
782-
boolean isRtlChar = layout.isRtlCharAt(start);
783-
boolean isRtlParagraph = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT;
784-
float placeholderLeftPosition;
785-
// There's a bug on Samsung devices where calling getPrimaryHorizontal on
786-
// the last offset in the layout will result in an endless loop. Work around
787-
// this bug by avoiding getPrimaryHorizontal in that case.
788-
if (start == text.length() - 1) {
789-
boolean endsWithNewLine =
790-
text.length() > 0 && text.charAt(layout.getLineEnd(line) - 1) == '\n';
791-
float lineWidth = endsWithNewLine ? layout.getLineMax(line) : layout.getLineWidth(line);
792-
placeholderLeftPosition =
793-
isRtlParagraph
794-
// Equivalent to `layout.getLineLeft(line)` but `getLineLeft` returns
795-
// incorrect
796-
// values when the paragraph is RTL and `setSingleLine(true)`.
797-
? calculatedWidth - lineWidth
798-
: layout.getLineRight(line) - placeholderWidth;
799-
} else {
800-
// The direction of the paragraph may not be exactly the direction the string is
801-
// heading
802-
// in at the
803-
// position of the placeholder. So, if the direction of the character is the same
804-
// as the
805-
// paragraph
806-
// use primary, secondary otherwise.
807-
boolean characterAndParagraphDirectionMatch = isRtlParagraph == isRtlChar;
808-
placeholderLeftPosition =
809-
characterAndParagraphDirectionMatch
810-
? layout.getPrimaryHorizontal(start)
811-
: layout.getSecondaryHorizontal(start);
812-
if (isRtlParagraph && !isRtlChar) {
813-
// Adjust `placeholderLeftPosition` to work around an Android bug.
814-
// The bug is when the paragraph is RTL and `setSingleLine(true)`, some layout
815-
// methods such as `getPrimaryHorizontal`, `getSecondaryHorizontal`, and
816-
// `getLineRight` return incorrect values. Their return values seem to be off
817-
// by the same number of pixels so subtracting these values cancels out the
818-
// error.
819-
//
820-
// The result is equivalent to bugless versions of
821-
// `getPrimaryHorizontal`/`getSecondaryHorizontal`.
822-
placeholderLeftPosition =
823-
calculatedWidth - (layout.getLineRight(line) - placeholderLeftPosition);
824-
}
825-
if (isRtlChar) {
826-
placeholderLeftPosition -= placeholderWidth;
827-
}
828-
}
829-
// Vertically align the inline view to the baseline of the line of text.
830-
float placeholderTopPosition = layout.getLineBaseline(line) - placeholderHeight;
831-
832-
// The attachment array returns the positions of each of the attachments as
833-
attachmentsPositions[attachmentPosition] =
834-
PixelUtil.toDIPFromPixel(placeholderTopPosition);
835-
attachmentsPositions[attachmentPosition + 1] =
836-
PixelUtil.toDIPFromPixel(placeholderLeftPosition);
837-
attachmentIndex++;
816+
int lastAttachmentFoundInSpan =
817+
text.nextSpanTransition(i, text.length(), TextInlineViewPlaceholderSpan.class);
818+
TextInlineViewPlaceholderSpan[] placeholders =
819+
text.getSpans(i, lastAttachmentFoundInSpan, TextInlineViewPlaceholderSpan.class);
820+
821+
if (placeholders.length == 0) {
822+
metrics.wasFound = false;
823+
return lastAttachmentFoundInSpan;
824+
}
825+
826+
Assertions.assertCondition(placeholders.length == 1);
827+
TextInlineViewPlaceholderSpan placeholder = placeholders[0];
828+
829+
int start = text.getSpanStart(placeholder);
830+
int line = layout.getLineForOffset(start);
831+
boolean isLineTruncated = layout.getEllipsisCount(line) > 0;
832+
boolean isAttachmentTruncated =
833+
line > calculatedLineCount
834+
|| (isLineTruncated
835+
&& start >= layout.getLineStart(line) + layout.getEllipsisStart(line));
836+
if (isAttachmentTruncated) {
837+
metrics.top = Float.NaN;
838+
metrics.left = Float.NaN;
839+
} else {
840+
float placeholderWidth = placeholder.getWidth();
841+
float placeholderHeight = placeholder.getHeight();
842+
// Calculate if the direction of the placeholder character is Right-To-Left.
843+
boolean isRtlChar = layout.isRtlCharAt(start);
844+
boolean isRtlParagraph = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT;
845+
float placeholderLeftPosition;
846+
// There's a bug on Samsung devices where calling getPrimaryHorizontal on
847+
// the last offset in the layout will result in an endless loop. Work around
848+
// this bug by avoiding getPrimaryHorizontal in that case.
849+
if (start == text.length() - 1) {
850+
boolean endsWithNewLine =
851+
text.length() > 0 && text.charAt(layout.getLineEnd(line) - 1) == '\n';
852+
float lineWidth = endsWithNewLine ? layout.getLineMax(line) : layout.getLineWidth(line);
853+
placeholderLeftPosition =
854+
isRtlParagraph
855+
// Equivalent to `layout.getLineLeft(line)` but `getLineLeft` returns
856+
// incorrect
857+
// values when the paragraph is RTL and `setSingleLine(true)`.
858+
? calculatedWidth - lineWidth
859+
: layout.getLineRight(line) - placeholderWidth;
860+
} else {
861+
// The direction of the paragraph may not be exactly the direction the string is
862+
// heading
863+
// in at the
864+
// position of the placeholder. So, if the direction of the character is the same
865+
// as the
866+
// paragraph
867+
// use primary, secondary otherwise.
868+
boolean characterAndParagraphDirectionMatch = isRtlParagraph == isRtlChar;
869+
placeholderLeftPosition =
870+
characterAndParagraphDirectionMatch
871+
? layout.getPrimaryHorizontal(start)
872+
: layout.getSecondaryHorizontal(start);
873+
if (isRtlParagraph && !isRtlChar) {
874+
// Adjust `placeholderLeftPosition` to work around an Android bug.
875+
// The bug is when the paragraph is RTL and `setSingleLine(true)`, some layout
876+
// methods such as `getPrimaryHorizontal`, `getSecondaryHorizontal`, and
877+
// `getLineRight` return incorrect values. Their return values seem to be off
878+
// by the same number of pixels so subtracting these values cancels out the
879+
// error.
880+
//
881+
// The result is equivalent to bugless versions of
882+
// `getPrimaryHorizontal`/`getSecondaryHorizontal`.
883+
placeholderLeftPosition =
884+
calculatedWidth - (layout.getLineRight(line) - placeholderLeftPosition);
885+
}
886+
if (isRtlChar) {
887+
placeholderLeftPosition -= placeholderWidth;
838888
}
839889
}
840-
}
890+
// Vertically align the inline view to the baseline of the line of text.
891+
float placeholderTopPosition = layout.getLineBaseline(line) - placeholderHeight;
841892

842-
float widthInSP = PixelUtil.toDIPFromPixel(calculatedWidth);
843-
float heightInSP = PixelUtil.toDIPFromPixel(calculatedHeight);
893+
// The attachment array returns the positions of each of the attachments as
894+
metrics.top = placeholderTopPosition;
895+
metrics.left = placeholderLeftPosition;
896+
}
844897

845-
return YogaMeasureOutput.make(widthInSP, heightInSP);
898+
metrics.wasFound = true;
899+
metrics.width = placeholder.getWidth();
900+
metrics.height = placeholder.getHeight();
901+
return lastAttachmentFoundInSpan;
846902
}
847903

848904
public static WritableArray measureLines(

0 commit comments

Comments
 (0)