Skip to content

Commit 5cda7f4

Browse files
joevilchesfacebook-github-bot
authored andcommitted
Implement textVerticalAlign (facebook#51680)
Summary: Pull Request resolved: facebook#51680 Right now there are 2 flavors of vertical text alignment: `verticalAlign` and `textVerticalAlign`. Both do the same thing currently. For Facsimile, we actually want to "properly" implement `verticalAlign` so that it matches the web version, while leaving `textVerticalAlign` the same. That will take some time, however, so for now we are just going to implement the way it currently works, while fixing some issues with inline View's Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D75475915
1 parent bca2a1b commit 5cda7f4

File tree

5 files changed

+109
-32
lines changed

5 files changed

+109
-32
lines changed

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6500,6 +6500,7 @@ public class com/facebook/react/views/text/TextLayoutManager {
65006500
public static final field PA_KEY_MAXIMUM_FONT_SIZE S
65016501
public static final field PA_KEY_MAX_NUMBER_OF_LINES S
65026502
public static final field PA_KEY_MINIMUM_FONT_SIZE S
6503+
public static final field PA_KEY_TEXT_ALIGN_VERTICAL S
65036504
public static final field PA_KEY_TEXT_BREAK_STRATEGY S
65046505
public fun <init> ()V
65056506
public static fun deleteCachedSpannableForTag (I)V

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import android.content.Context;
2323
import android.graphics.Point;
2424
import android.os.SystemClock;
25-
import android.text.Layout;
2625
import android.view.View;
2726
import android.view.accessibility.AccessibilityEvent;
2827
import androidx.annotation.AnyThread;
@@ -51,7 +50,6 @@
5150
import com.facebook.react.bridge.UIManagerListener;
5251
import com.facebook.react.bridge.UiThreadUtil;
5352
import com.facebook.react.bridge.WritableMap;
54-
import com.facebook.react.common.ReactConstants;
5553
import com.facebook.react.common.annotations.UnstableReactNativeAPI;
5654
import com.facebook.react.common.build.ReactBuildConfig;
5755
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
@@ -671,23 +669,15 @@ public PreparedLayout prepareLayout(
671669
float maxHeight) {
672670
SurfaceMountingManager surfaceMountingManager =
673671
mMountingManager.getSurfaceManagerEnforced(surfaceId, "prepareLayout");
674-
Layout layout =
675-
TextLayoutManager.createLayout(
676-
Preconditions.checkNotNull(surfaceMountingManager.getContext()),
677-
attributedString,
678-
paragraphAttributes,
679-
getYogaSize(minWidth, maxWidth),
680-
getYogaMeasureMode(minWidth, maxWidth),
681-
getYogaSize(minHeight, maxHeight),
682-
getYogaMeasureMode(minHeight, maxHeight),
683-
null /* T219881133: Migrate away from ReactTextViewManagerCallback */);
684-
685-
int maximumNumberOfLines =
686-
paragraphAttributes.contains(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
687-
? paragraphAttributes.getInt(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
688-
: ReactConstants.UNSET;
689-
690-
return new PreparedLayout(layout, maximumNumberOfLines);
672+
673+
return TextLayoutManager.createPreparedLayout(
674+
Preconditions.checkNotNull(surfaceMountingManager.getContext()),
675+
attributedString,
676+
paragraphAttributes,
677+
getYogaSize(minWidth, maxWidth),
678+
getYogaMeasureMode(minWidth, maxWidth),
679+
getYogaSize(minHeight, maxHeight),
680+
getYogaMeasureMode(minHeight, maxHeight));
691681
}
692682

693683
@AnyThread

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ import com.facebook.proguard.annotations.DoNotStrip
1515
* it.
1616
*/
1717
@DoNotStrip
18-
internal class PreparedLayout(public val layout: Layout, public val maximumNumberOfLines: Int)
18+
internal class PreparedLayout(
19+
public val layout: Layout,
20+
public val maximumNumberOfLines: Int,
21+
public val verticalOffset: Float
22+
)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re
100100
}
101101

102102
super.onDraw(canvas)
103-
canvas.translate(paddingLeft.toFloat(), paddingTop.toFloat())
103+
canvas.translate(
104+
paddingLeft.toFloat(), paddingTop.toFloat() + (preparedLayout?.verticalOffset ?: 0f))
104105

105106
val layout = preparedLayout?.layout
106107
if (layout != null) {

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

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@
2626
import androidx.annotation.NonNull;
2727
import androidx.annotation.Nullable;
2828
import androidx.core.util.Preconditions;
29+
import com.facebook.common.logging.FLog;
2930
import com.facebook.infer.annotation.Assertions;
3031
import com.facebook.react.bridge.ReactNoCrashSoftException;
3132
import com.facebook.react.bridge.ReactSoftExceptionLogger;
3233
import com.facebook.react.bridge.WritableArray;
3334
import com.facebook.react.common.ReactConstants;
3435
import com.facebook.react.common.annotations.UnstableReactNativeAPI;
3536
import com.facebook.react.common.mapbuffer.MapBuffer;
37+
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
3638
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
3739
import com.facebook.react.uimanager.PixelUtil;
3840
import 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

Comments
 (0)