2626import androidx .annotation .NonNull ;
2727import androidx .annotation .Nullable ;
2828import androidx .core .util .Preconditions ;
29+ import com .facebook .common .logging .FLog ;
2930import com .facebook .infer .annotation .Assertions ;
3031import com .facebook .react .bridge .ReactNoCrashSoftException ;
3132import com .facebook .react .bridge .ReactSoftExceptionLogger ;
3233import com .facebook .react .bridge .WritableArray ;
3334import com .facebook .react .common .ReactConstants ;
3435import com .facebook .react .common .annotations .UnstableReactNativeAPI ;
3536import com .facebook .react .common .mapbuffer .MapBuffer ;
37+ import com .facebook .react .common .mapbuffer .ReadableMapBuffer ;
3638import com .facebook .react .internal .featureflags .ReactNativeFeatureFlags ;
3739import com .facebook .react .uimanager .PixelUtil ;
3840import com .facebook .react .uimanager .ReactAccessibilityDelegate .AccessibilityRole ;
@@ -85,6 +87,7 @@ public class TextLayoutManager {
8587 public static final short PA_KEY_HYPHENATION_FREQUENCY = 5 ;
8688 public static final short PA_KEY_MINIMUM_FONT_SIZE = 6 ;
8789 public static final short PA_KEY_MAXIMUM_FONT_SIZE = 7 ;
90+ public static final short PA_KEY_TEXT_ALIGN_VERTICAL = 8 ;
8891
8992 private static final String TAG = TextLayoutManager .class .getSimpleName ();
9093
@@ -604,8 +607,7 @@ private static void updateTextPaint(
604607 }
605608 }
606609
607- @ UnstableReactNativeAPI
608- public static Layout createLayout (
610+ private static Layout createLayout (
609611 @ NonNull Context context ,
610612 MapBuffer attributedString ,
611613 MapBuffer paragraphAttributes ,
@@ -695,6 +697,38 @@ public static Layout createLayout(
695697 paint );
696698 }
697699
700+ @ UnstableReactNativeAPI
701+ public static PreparedLayout createPreparedLayout (
702+ @ NonNull Context context ,
703+ ReadableMapBuffer attributedString ,
704+ ReadableMapBuffer paragraphAttributes ,
705+ float width ,
706+ YogaMeasureMode widthYogaMeasureMode ,
707+ float height ,
708+ YogaMeasureMode heightYogaMeasureMode ) {
709+ Layout layout =
710+ TextLayoutManager .createLayout (
711+ Preconditions .checkNotNull (context ),
712+ attributedString ,
713+ paragraphAttributes ,
714+ width ,
715+ widthYogaMeasureMode ,
716+ height ,
717+ heightYogaMeasureMode ,
718+ null /* T219881133: Migrate away from ReactTextViewManagerCallback */ );
719+
720+ int maximumNumberOfLines =
721+ paragraphAttributes .contains (TextLayoutManager .PA_KEY_MAX_NUMBER_OF_LINES )
722+ ? paragraphAttributes .getInt (TextLayoutManager .PA_KEY_MAX_NUMBER_OF_LINES )
723+ : ReactConstants .UNSET ;
724+
725+ float verticalOffset =
726+ getVerticalOffset (
727+ layout , paragraphAttributes , height , heightYogaMeasureMode , maximumNumberOfLines );
728+
729+ return new PreparedLayout (layout , maximumNumberOfLines , verticalOffset );
730+ }
731+
698732 /*package*/ static void adjustSpannableFontToFit (
699733 Spannable text ,
700734 float width ,
@@ -814,7 +848,7 @@ public static long measureText(
814848 float calculatedWidth =
815849 calculateWidth (layout , text , width , widthYogaMeasureMode , calculatedLineCount );
816850 float calculatedHeight =
817- calculateHeight (layout , text , height , heightYogaMeasureMode , calculatedLineCount );
851+ calculateHeight (layout , height , heightYogaMeasureMode , calculatedLineCount );
818852
819853 if (attachmentsPositions != null ) {
820854 int attachmentIndex = 0 ;
@@ -823,7 +857,8 @@ public static long measureText(
823857 AttachmentMetrics metrics = new AttachmentMetrics ();
824858 for (int i = 0 ; i < text .length (); i = lastAttachmentFoundInSpan ) {
825859 lastAttachmentFoundInSpan =
826- nextAttachmentMetrics (layout , text , calculatedWidth , calculatedLineCount , i , metrics );
860+ nextAttachmentMetrics (
861+ layout , text , calculatedWidth , calculatedLineCount , i , 0 , metrics );
827862 if (metrics .wasFound ) {
828863 attachmentsPositions [attachmentIndex ] = PixelUtil .toDIPFromPixel (metrics .top );
829864 attachmentsPositions [attachmentIndex + 1 ] = PixelUtil .toDIPFromPixel (metrics .left );
@@ -853,7 +888,7 @@ public static float[] measurePreparedLayout(
853888 float calculatedWidth =
854889 calculateWidth (layout , text , width , widthYogaMeasureMode , calculatedLineCount );
855890 float calculatedHeight =
856- calculateHeight (layout , text , height , heightYogaMeasureMode , calculatedLineCount );
891+ calculateHeight (layout , height , heightYogaMeasureMode , calculatedLineCount );
857892
858893 ArrayList <Float > retList = new ArrayList <>();
859894 retList .add (PixelUtil .toDIPFromPixel (calculatedWidth ));
@@ -863,7 +898,14 @@ public static float[] measurePreparedLayout(
863898 int lastAttachmentFoundInSpan ;
864899 for (int i = 0 ; i < text .length (); i = lastAttachmentFoundInSpan ) {
865900 lastAttachmentFoundInSpan =
866- nextAttachmentMetrics (layout , text , calculatedWidth , calculatedLineCount , i , metrics );
901+ nextAttachmentMetrics (
902+ layout ,
903+ text ,
904+ calculatedWidth ,
905+ calculatedLineCount ,
906+ i ,
907+ preparedLayout .getVerticalOffset (),
908+ metrics );
867909 if (metrics .wasFound ) {
868910 retList .add (PixelUtil .toDIPFromPixel (metrics .top ));
869911 retList .add (PixelUtil .toDIPFromPixel (metrics .left ));
@@ -879,6 +921,44 @@ public static float[] measurePreparedLayout(
879921 return ret ;
880922 }
881923
924+ private static float getVerticalOffset (
925+ Layout layout ,
926+ ReadableMapBuffer paragraphAttributes ,
927+ float height ,
928+ YogaMeasureMode heightMeasureMode ,
929+ int maximumNumberOfLines ) {
930+ @ Nullable
931+ String textAlignVertical =
932+ paragraphAttributes .contains (TextLayoutManager .PA_KEY_TEXT_ALIGN_VERTICAL )
933+ ? paragraphAttributes .getString (TextLayoutManager .PA_KEY_TEXT_ALIGN_VERTICAL )
934+ : null ;
935+
936+ if (textAlignVertical == null ) {
937+ return 0 ;
938+ }
939+
940+ int textHeight = layout .getHeight ();
941+ int calculatedLineCount = calculateLineCount (layout , maximumNumberOfLines );
942+ float boxHeight = calculateHeight (layout , height , heightMeasureMode , calculatedLineCount );
943+
944+ if (textHeight > boxHeight ) {
945+ return 0 ;
946+ }
947+
948+ switch (textAlignVertical ) {
949+ case "auto" :
950+ case "top" :
951+ return 0 ;
952+ case "center" :
953+ return (boxHeight - textHeight ) / 2f ;
954+ case "bottom" :
955+ return boxHeight - textHeight ;
956+ default :
957+ FLog .w (ReactConstants .TAG , "Invalid textAlignVertical: " + textAlignVertical );
958+ return 0 ;
959+ }
960+ }
961+
882962 private static int calculateLineCount (Layout layout , int maximumNumberOfLines ) {
883963 return maximumNumberOfLines == ReactConstants .UNSET || maximumNumberOfLines == 0
884964 ? layout .getLineCount ()
@@ -945,11 +1025,7 @@ private static float calculateWidth(
9451025 }
9461026
9471027 private static float calculateHeight (
948- Layout layout ,
949- Spanned text ,
950- float height ,
951- YogaMeasureMode heightYogaMeasureMode ,
952- int calculatedLineCount ) {
1028+ Layout layout , float height , YogaMeasureMode heightYogaMeasureMode , int calculatedLineCount ) {
9531029 float calculatedHeight = height ;
9541030 if (heightYogaMeasureMode != YogaMeasureMode .EXACTLY ) {
9551031 // StaticLayout only seems to change its height in response to maxLines when ellipsizing, so
@@ -976,6 +1052,7 @@ private static int nextAttachmentMetrics(
9761052 float calculatedWidth ,
9771053 int calculatedLineCount ,
9781054 int i ,
1055+ float verticalOffset ,
9791056 AttachmentMetrics metrics ) {
9801057 // Calculate the positions of the attachments (views) that will be rendered inside the
9811058 // Spanned Text. The following logic is only executed when a text contains views inside.
@@ -1062,6 +1139,10 @@ private static int nextAttachmentMetrics(
10621139 metrics .left = placeholderLeftPosition ;
10631140 }
10641141
1142+ // The text may be vertically aligned to the top, center, or bottom of the container. This is
1143+ // not captured in the Layout, but rather applied separately. We need to account for this here.
1144+ metrics .top += verticalOffset ;
1145+
10651146 metrics .wasFound = true ;
10661147 metrics .width = placeholder .getWidth ();
10671148 metrics .height = placeholder .getHeight ();
0 commit comments