Skip to content

Commit 0581e88

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Avoid scratch TextPaint for Facsimile Layouts (#51827)
Summary: Pull Request resolved: #51827 TextLayoutManager has an optimization today, where it reuses a scratch TextPaint throughout layouts. This is not safe in Facsimile, since the paint is included as part of `Layout`, and the `Layout` escapes the TextLayoutManager, to later be drawn. I.e. every `PreparedLayoutTextView` right now is sharing this same scratch Paint. This change makes it so that we only ever use a scratch paint for the purpose of measurements, where the layout is short-lived. A simpler approach could be to just abandon the scratch TextPaint, since they do not seem wildly expensive at a glance (though not trivial). Changelog: [Internal] Reviewed By: rshest Differential Revision: D75987605 fbshipit-source-id: 3fe3519c6164828a25cb4e2b0ee6eded73695a95
1 parent 7f8cf06 commit 0581e88

3 files changed

Lines changed: 69 additions & 34 deletions

File tree

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,15 @@ internal object FontMetricsUtil {
2222
private const val AMPLIFICATION_FACTOR = 100f
2323

2424
@JvmStatic
25-
fun getFontMetrics(
26-
text: CharSequence,
27-
layout: Layout,
28-
paint: TextPaint,
29-
context: Context
30-
): WritableArray {
25+
fun getFontMetrics(text: CharSequence, layout: Layout, context: Context): WritableArray {
3126
val dm = context.resources.displayMetrics
3227
val lines = Arguments.createArray()
3328

3429
// To calculate xHeight and capHeight we have to render an "x" and "T" and manually measure
3530
// their height. In order to get more precision than Android offers, we blow up the text size by
3631
// 100 and
3732
// measure it. Luckily, text size affects rendering linearly, so we can do this trick.
38-
val paintCopy = TextPaint(paint).apply { textSize *= AMPLIFICATION_FACTOR }
33+
val paintCopy = TextPaint(layout.paint).apply { textSize *= AMPLIFICATION_FACTOR }
3934

4035
val capHeightBounds = Rect()
4136
paintCopy.getTextBounds(

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,7 @@ public long measure(
112112

113113
if (mShouldNotifyOnTextLayout) {
114114
ThemedReactContext themedReactContext = getThemedContext();
115-
WritableArray lines =
116-
FontMetricsUtil.getFontMetrics(
117-
text, layout, sTextPaintInstance, themedReactContext);
115+
WritableArray lines = FontMetricsUtil.getFontMetrics(text, layout, themedReactContext);
118116
WritableMap event = Arguments.createMap();
119117
event.putArray("lines", lines);
120118
if (themedReactContext.hasActiveReactInstance()) {

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

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ private static Spannable createSpannableFromAttributedString(
373373

374374
private static Layout createLayout(
375375
Spannable text,
376-
BoringLayout.Metrics boring,
376+
@Nullable BoringLayout.Metrics boring,
377377
float width,
378378
YogaMeasureMode widthYogaMeasureMode,
379379
boolean includeFontPadding,
@@ -417,7 +417,7 @@ private static Layout createLayout(
417417

418418
private static Layout createLayoutWithCorrectRounding(
419419
Spannable text,
420-
BoringLayout.Metrics boring,
420+
@Nullable BoringLayout.Metrics boring,
421421
float width,
422422
YogaMeasureMode widthYogaMeasureMode,
423423
boolean includeFontPadding,
@@ -475,7 +475,7 @@ private static Layout createLayoutWithCorrectRounding(
475475

476476
private static Layout createLayoutWithBuggedRounding(
477477
Spannable text,
478-
BoringLayout.Metrics boring,
478+
@Nullable BoringLayout.Metrics boring,
479479
float width,
480480
YogaMeasureMode widthYogaMeasureMode,
481481
boolean includeFontPadding,
@@ -571,14 +571,12 @@ private static Layout createLayoutWithBuggedRounding(
571571
return layout;
572572
}
573573

574+
/**
575+
* Sets attributes on the TextPaint, used for content outside the Spannable text, like for empty
576+
* strings, or newlines after the last trailing character
577+
*/
574578
private static void updateTextPaint(
575579
TextPaint paint, TextAttributeProps baseTextAttributes, Context context) {
576-
// TextPaint attributes will be used for content outside the Spannable, like for the
577-
// hypothetical height of a new line after a trailing newline character (considered part of the
578-
// previous line).
579-
paint.reset();
580-
paint.setAntiAlias(true);
581-
582580
if (baseTextAttributes.getEffectiveFontSize() != ReactConstants.UNSET) {
583581
paint.setTextSize(baseTextAttributes.getEffectiveFontSize());
584582
}
@@ -602,13 +600,33 @@ private static void updateTextPaint(
602600
paint.setFakeBoldText((missingStyle & Typeface.BOLD) != 0);
603601
paint.setTextSkewX((missingStyle & Typeface.ITALIC) != 0 ? -0.25f : 0);
604602
}
605-
} else {
606-
paint.setTypeface(null);
607603
}
608604
}
609605

610-
private static Layout createLayout(
611-
@NonNull Context context,
606+
/**
607+
* WARNING: This paint should not be used for any layouts which may escape TextLayoutManager, as
608+
* they may need to be drawn later, and may not safely be reused
609+
*/
610+
private static TextPaint scratchPaintWithAttributes(
611+
TextAttributeProps baseTextAttributes, Context context) {
612+
TextPaint paint = Preconditions.checkNotNull(sTextPaintInstance.get());
613+
paint.setTypeface(null);
614+
paint.setTextSize(12);
615+
paint.setFakeBoldText(false);
616+
paint.setTextSkewX(0);
617+
updateTextPaint(paint, baseTextAttributes, context);
618+
return paint;
619+
}
620+
621+
private static TextPaint newPaintWithAttributes(
622+
TextAttributeProps baseTextAttributes, Context context) {
623+
TextPaint paint = new TextPaint();
624+
updateTextPaint(paint, baseTextAttributes, context);
625+
return paint;
626+
}
627+
628+
private static Layout createLayoutForMeasurement(
629+
Context context,
612630
MapBuffer attributedString,
613631
MapBuffer paragraphAttributes,
614632
float width,
@@ -625,10 +643,29 @@ private static Layout createLayout(
625643
} else {
626644
TextAttributeProps baseTextAttributes =
627645
TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES));
628-
paint = Preconditions.checkNotNull(sTextPaintInstance.get());
629-
updateTextPaint(paint, baseTextAttributes, context);
646+
paint = scratchPaintWithAttributes(baseTextAttributes, context);
630647
}
631648

649+
return createLayout(
650+
text,
651+
paint,
652+
attributedString,
653+
paragraphAttributes,
654+
width,
655+
widthYogaMeasureMode,
656+
height,
657+
heightYogaMeasureMode);
658+
}
659+
660+
private static Layout createLayout(
661+
Spannable text,
662+
TextPaint paint,
663+
MapBuffer attributedString,
664+
MapBuffer paragraphAttributes,
665+
float width,
666+
YogaMeasureMode widthYogaMeasureMode,
667+
float height,
668+
YogaMeasureMode heightYogaMeasureMode) {
632669
BoringLayout.Metrics boring = isBoring(text, paint);
633670

634671
int textBreakStrategy =
@@ -656,6 +693,7 @@ private static Layout createLayout(
656693
paragraphAttributes.getString(PA_KEY_ELLIPSIZE_MODE))
657694
: null;
658695

696+
// T226571629: textAlign should be moved to ParagraphAttributes
659697
@Nullable String alignmentAttr = getTextAlignmentAttr(attributedString);
660698
Layout.Alignment alignment = getTextAlignment(attributedString, text, alignmentAttr);
661699
int justificationMode = getTextJustificationMode(alignmentAttr);
@@ -699,24 +737,28 @@ private static Layout createLayout(
699737

700738
@UnstableReactNativeAPI
701739
public static PreparedLayout createPreparedLayout(
702-
@NonNull Context context,
740+
Context context,
703741
ReadableMapBuffer attributedString,
704742
ReadableMapBuffer paragraphAttributes,
705743
float width,
706744
YogaMeasureMode widthYogaMeasureMode,
707745
float height,
708746
YogaMeasureMode heightYogaMeasureMode,
709747
@Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) {
748+
Spannable text =
749+
getOrCreateSpannableForText(context, attributedString, reactTextViewManagerCallback);
750+
TextAttributeProps baseTextAttributes =
751+
TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES));
710752
Layout layout =
711753
TextLayoutManager.createLayout(
712-
Preconditions.checkNotNull(context),
754+
text,
755+
newPaintWithAttributes(baseTextAttributes, context),
713756
attributedString,
714757
paragraphAttributes,
715758
width,
716759
widthYogaMeasureMode,
717760
height,
718-
heightYogaMeasureMode,
719-
reactTextViewManagerCallback);
761+
heightYogaMeasureMode);
720762

721763
int maximumNumberOfLines =
722764
paragraphAttributes.contains(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
@@ -828,7 +870,7 @@ public static long measureText(
828870
@Nullable float[] attachmentsPositions) {
829871
// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
830872
Layout layout =
831-
createLayout(
873+
createLayoutForMeasurement(
832874
context,
833875
attributedString,
834876
paragraphAttributes,
@@ -1151,23 +1193,23 @@ private static int nextAttachmentMetrics(
11511193
}
11521194

11531195
public static WritableArray measureLines(
1154-
@NonNull Context context,
1196+
Context context,
11551197
MapBuffer attributedString,
11561198
MapBuffer paragraphAttributes,
11571199
float width,
11581200
float height) {
11591201
Layout layout =
1160-
createLayout(
1202+
createLayoutForMeasurement(
11611203
context,
11621204
attributedString,
11631205
paragraphAttributes,
11641206
width,
11651207
YogaMeasureMode.EXACTLY,
11661208
height,
11671209
YogaMeasureMode.EXACTLY,
1210+
// TODO T226571550: Fix measureLines with ReactTextViewManagerCallback
11681211
null);
1169-
return FontMetricsUtil.getFontMetrics(
1170-
layout.getText(), layout, Preconditions.checkNotNull(sTextPaintInstance.get()), context);
1212+
return FontMetricsUtil.getFontMetrics(layout.getText(), layout, context);
11711213
}
11721214

11731215
private static @Nullable BoringLayout.Metrics isBoring(Spannable text, TextPaint paint) {

0 commit comments

Comments
 (0)