2626import androidx .annotation .NonNull ;
2727import androidx .annotation .Nullable ;
2828import androidx .core .util .Preconditions ;
29+ import com .facebook .infer .annotation .Assertions ;
2930import com .facebook .react .bridge .ReactNoCrashSoftException ;
3031import com .facebook .react .bridge .ReactSoftExceptionLogger ;
3132import 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