Skip to content

Commit 747a96b

Browse files
j-piaseckifacebook-github-bot
authored andcommitted
Add implementation of adjustsFontSizeToFit on the new architecture on Android (facebook#44075)
Summary: `adjustsFontSizeToFit` prop is exposed on both platforms but is missing implementation on the new arch on Android. This Pr adds it. Fixes facebook#43104 ## Changelog: [ANDROID] [FIXED] - Fixed `adjustsFontSizeToFit` not working on Android when using the new architecture Pull Request resolved: facebook#44075 Test Plan: Tested on the RN Tester app using the `AdjustingFontSize` test: | Old architecture | New architecture | |------------------|------------------| | <video src="https://github.com/facebook/react-native/assets/21055725/9243317c-1917-4ddb-9b8a-e9e99638409d"> | <video src="https://github.com/facebook/react-native/assets/21055725/39a7a9f2-21e4-4ba7-9ceb-dfec4ca6f643"> | Reviewed By: javache Differential Revision: D56134348 Pulled By: cortinico fbshipit-source-id: b8339e3bec08e307abb0c6e4bd3f5fe143303014
1 parent 4636686 commit 747a96b

8 files changed

Lines changed: 205 additions & 13 deletions

File tree

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7311,6 +7311,7 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi
73117311
public fun invalidateDrawable (Landroid/graphics/drawable/Drawable;)V
73127312
public fun onAttachedToWindow ()V
73137313
public fun onDetachedFromWindow ()V
7314+
protected fun onDraw (Landroid/graphics/Canvas;)V
73147315
public fun onFinishTemporaryDetach ()V
73157316
protected fun onLayout (ZIIII)V
73167317
public fun onStartTemporaryDetach ()V
@@ -7322,10 +7323,14 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi
73227323
public fun setBorderRadius (FI)V
73237324
public fun setBorderStyle (Ljava/lang/String;)V
73247325
public fun setBorderWidth (IF)V
7326+
public fun setBreakStrategy (I)V
73257327
public fun setEllipsizeLocation (Landroid/text/TextUtils$TruncateAt;)V
73267328
public fun setFontSize (F)V
7329+
public fun setHyphenationFrequency (I)V
7330+
public fun setIncludeFontPadding (Z)V
73277331
public fun setLetterSpacing (F)V
73287332
public fun setLinkifyMask (I)V
7333+
public fun setMinimumFontSize (F)V
73297334
public fun setNotifyOnInlineViewLayout (Z)V
73307335
public fun setNumberOfLines (I)V
73317336
public fun setSpanned (Landroid/text/Spannable;)V
@@ -7530,7 +7535,7 @@ public class com/facebook/react/views/text/TextLayoutManager {
75307535
public static fun isRTL (Lcom/facebook/react/bridge/ReadableMap;)Z
75317536
public static fun measureLines (Landroid/content/Context;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;F)Lcom/facebook/react/bridge/WritableArray;
75327537
public static fun measureText (Landroid/content/Context;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;FLcom/facebook/yoga/YogaMeasureMode;FLcom/facebook/yoga/YogaMeasureMode;Lcom/facebook/react/views/text/ReactTextViewManagerCallback;[F)J
7533-
public static fun setCachedSpannabledForTag (ILandroid/text/Spannable;)V
7538+
public static fun setCachedSpannableForTag (ILandroid/text/Spannable;)V
75347539
}
75357540

75367541
public class com/facebook/react/views/text/TextLayoutManagerMapBuffer {
@@ -7548,15 +7553,18 @@ public class com/facebook/react/views/text/TextLayoutManagerMapBuffer {
75487553
public static final field PA_KEY_ELLIPSIZE_MODE S
75497554
public static final field PA_KEY_HYPHENATION_FREQUENCY S
75507555
public static final field PA_KEY_INCLUDE_FONT_PADDING S
7556+
public static final field PA_KEY_MAXIMUM_FONT_SIZE S
75517557
public static final field PA_KEY_MAX_NUMBER_OF_LINES S
7558+
public static final field PA_KEY_MINIMUM_FONT_SIZE S
75527559
public static final field PA_KEY_TEXT_BREAK_STRATEGY S
75537560
public fun <init> ()V
7561+
public static fun adjustSpannableFontToFit (Landroid/text/Spannable;FLcom/facebook/yoga/YogaMeasureMode;FLcom/facebook/yoga/YogaMeasureMode;DIZII)V
75547562
public static fun deleteCachedSpannableForTag (I)V
75557563
public static fun getOrCreateSpannableForText (Landroid/content/Context;Lcom/facebook/react/common/mapbuffer/MapBuffer;Lcom/facebook/react/views/text/ReactTextViewManagerCallback;)Landroid/text/Spannable;
75567564
public static fun isRTL (Lcom/facebook/react/common/mapbuffer/MapBuffer;)Z
7557-
public static fun measureLines (Landroid/content/Context;Lcom/facebook/react/common/mapbuffer/MapBuffer;Lcom/facebook/react/common/mapbuffer/MapBuffer;F)Lcom/facebook/react/bridge/WritableArray;
7565+
public static fun measureLines (Landroid/content/Context;Lcom/facebook/react/common/mapbuffer/MapBuffer;Lcom/facebook/react/common/mapbuffer/MapBuffer;FF)Lcom/facebook/react/bridge/WritableArray;
75587566
public static fun measureText (Landroid/content/Context;Lcom/facebook/react/common/mapbuffer/MapBuffer;Lcom/facebook/react/common/mapbuffer/MapBuffer;FLcom/facebook/yoga/YogaMeasureMode;FLcom/facebook/yoga/YogaMeasureMode;Lcom/facebook/react/views/text/ReactTextViewManagerCallback;[F)J
7559-
public static fun setCachedSpannabledForTag (ILandroid/text/Spannable;)V
7567+
public static fun setCachedSpannableForTag (ILandroid/text/Spannable;)V
75607568
}
75617569

75627570
public final class com/facebook/react/views/text/TextTransform : java/lang/Enum {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,8 @@ private NativeArray measureLinesMapBuffer(
513513
mReactApplicationContext,
514514
attributedString,
515515
paragraphAttributes,
516-
PixelUtil.toPixelFromDIP(width));
516+
PixelUtil.toPixelFromDIP(width),
517+
PixelUtil.toPixelFromDIP(height));
517518
}
518519

519520
@SuppressWarnings("unused")

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package com.facebook.react.views.text;
99

1010
import android.content.Context;
11+
import android.graphics.Canvas;
1112
import android.graphics.drawable.Drawable;
1213
import android.os.Build;
1314
import android.text.Layout;
@@ -44,6 +45,7 @@
4445
import com.facebook.react.views.text.internal.span.TextInlineImageSpan;
4546
import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan;
4647
import com.facebook.react.views.view.ReactViewBackgroundManager;
48+
import com.facebook.yoga.YogaMeasureMode;
4749
import java.util.ArrayList;
4850
import java.util.Collections;
4951
import java.util.Comparator;
@@ -61,10 +63,12 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
6163
private TextUtils.TruncateAt mEllipsizeLocation;
6264
private boolean mAdjustsFontSizeToFit;
6365
private float mFontSize;
66+
private float mMinimumFontSize;
6467
private float mLetterSpacing;
6568
private int mLinkifyMaskType;
6669
private boolean mNotifyOnInlineViewLayout;
6770
private boolean mTextIsSelectable;
71+
private boolean mShouldAdjustSpannableFontSize;
6872

6973
private ReactViewBackgroundManager mReactBackgroundManager;
7074
private Spannable mSpanned;
@@ -92,8 +96,10 @@ private void initView() {
9296
mLinkifyMaskType = 0;
9397
mNotifyOnInlineViewLayout = false;
9498
mTextIsSelectable = false;
99+
mShouldAdjustSpannableFontSize = false;
95100
mEllipsizeLocation = TextUtils.TruncateAt.END;
96101
mFontSize = Float.NaN;
102+
mMinimumFontSize = Float.NaN;
97103
mLetterSpacing = 0.f;
98104

99105
mSpanned = null;
@@ -355,6 +361,27 @@ public int compare(Object o1, Object o2) {
355361
}
356362
}
357363

364+
@Override
365+
protected void onDraw(Canvas canvas) {
366+
if (mAdjustsFontSizeToFit && getSpanned() != null && mShouldAdjustSpannableFontSize) {
367+
mShouldAdjustSpannableFontSize = false;
368+
TextLayoutManagerMapBuffer.adjustSpannableFontToFit(
369+
getSpanned(),
370+
getWidth(),
371+
YogaMeasureMode.EXACTLY,
372+
getHeight(),
373+
YogaMeasureMode.EXACTLY,
374+
mMinimumFontSize,
375+
mNumberOfLines,
376+
getIncludeFontPadding(),
377+
getBreakStrategy(),
378+
getHyphenationFrequency());
379+
setText(getSpanned());
380+
}
381+
382+
super.onDraw(canvas);
383+
}
384+
358385
public void setText(ReactTextUpdate update) {
359386
mContainsImages = update.containsImages();
360387
// Android's TextView crashes when it tries to relayout if LayoutParams are
@@ -575,6 +602,7 @@ public boolean hasOverlappingRendering() {
575602
public void setNumberOfLines(int numberOfLines) {
576603
mNumberOfLines = numberOfLines == 0 ? ViewDefaults.NUMBER_OF_LINES : numberOfLines;
577604
setMaxLines(mNumberOfLines);
605+
mShouldAdjustSpannableFontSize = true;
578606
}
579607

580608
public void setAdjustFontSizeToFit(boolean adjustsFontSizeToFit) {
@@ -590,6 +618,29 @@ public void setFontSize(float fontSize) {
590618
applyTextAttributes();
591619
}
592620

621+
public void setMinimumFontSize(float minimumFontSize) {
622+
mMinimumFontSize = minimumFontSize;
623+
mShouldAdjustSpannableFontSize = true;
624+
}
625+
626+
@Override
627+
public void setIncludeFontPadding(boolean includepad) {
628+
super.setIncludeFontPadding(includepad);
629+
mShouldAdjustSpannableFontSize = true;
630+
}
631+
632+
@Override
633+
public void setBreakStrategy(int breakStrategy) {
634+
super.setBreakStrategy(breakStrategy);
635+
mShouldAdjustSpannableFontSize = true;
636+
}
637+
638+
@Override
639+
public void setHyphenationFrequency(int hyphenationFrequency) {
640+
super.setHyphenationFrequency(hyphenationFrequency);
641+
mShouldAdjustSpannableFontSize = true;
642+
}
643+
593644
public void setLetterSpacing(float letterSpacing) {
594645
if (Float.isNaN(letterSpacing)) {
595646
return;
@@ -648,6 +699,7 @@ public void setBorderStyle(@Nullable String style) {
648699

649700
public void setSpanned(Spannable spanned) {
650701
mSpanned = spanned;
702+
mShouldAdjustSpannableFontSize = true;
651703
}
652704

653705
public Spannable getSpanned() {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ private Object getReactTextUpdate(ReactTextView view, ReactStylesDiffMap props,
176176
view.getContext(), attributedString, mReactTextViewManagerCallback);
177177
view.setSpanned(spanned);
178178

179+
float minimumFontSize =
180+
(float) paragraphAttributes.getDouble(TextLayoutManagerMapBuffer.PA_KEY_MINIMUM_FONT_SIZE);
181+
view.setMinimumFontSize(minimumFontSize);
182+
179183
int textBreakStrategy =
180184
TextAttributeProps.getTextBreakStrategy(
181185
paragraphAttributes.getString(TextLayoutManagerMapBuffer.PA_KEY_TEXT_BREAK_STRATEGY));

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public static boolean isRTL(ReadableMap attributedString) {
9999
return false;
100100
}
101101

102-
public static void setCachedSpannabledForTag(int reactTag, @NonNull Spannable sp) {
102+
public static void setCachedSpannableForTag(int reactTag, @NonNull Spannable sp) {
103103
if (ENABLE_MEASURE_LOGGING) {
104104
FLog.e(TAG, "Set cached spannable for tag[" + reactTag + "]: " + sp.toString());
105105
}

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

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ public class TextLayoutManagerMapBuffer {
8181
public static final short PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3;
8282
public static final short PA_KEY_INCLUDE_FONT_PADDING = 4;
8383
public static final short PA_KEY_HYPHENATION_FREQUENCY = 5;
84+
public static final short PA_KEY_MINIMUM_FONT_SIZE = 6;
85+
public static final short PA_KEY_MAXIMUM_FONT_SIZE = 7;
8486

8587
private static final boolean ENABLE_MEASURE_LOGGING = ReactBuildConfig.DEBUG && false;
8688

@@ -98,6 +100,8 @@ public class TextLayoutManagerMapBuffer {
98100

99101
private static final boolean DEFAULT_INCLUDE_FONT_PADDING = true;
100102

103+
private static final boolean DEFAULT_ADJUST_FONT_SIZE_TO_FIT = false;
104+
101105
private static final Object sCacheLock = new Object();
102106

103107
private static final ConcurrentHashMap<Integer, Spannable> sTagToSpannableCache =
@@ -106,7 +110,7 @@ public class TextLayoutManagerMapBuffer {
106110
private static final LruCache<ReadableMapBuffer, Spannable> sSpannableCache =
107111
new LruCache<>(spannableCacheSize);
108112

109-
public static void setCachedSpannabledForTag(int reactTag, @NonNull Spannable sp) {
113+
public static void setCachedSpannableForTag(int reactTag, @NonNull Spannable sp) {
110114
if (ENABLE_MEASURE_LOGGING) {
111115
FLog.e(TAG, "Set cached spannable for tag[" + reactTag + "]: " + sp.toString());
112116
}
@@ -378,6 +382,73 @@ private static Layout createLayout(
378382
return layout;
379383
}
380384

385+
public static void adjustSpannableFontToFit(
386+
Spannable text,
387+
float width,
388+
YogaMeasureMode widthYogaMeasureMode,
389+
float height,
390+
YogaMeasureMode heightYogaMeasureMode,
391+
double minimumFontSizeAttr,
392+
int maximumNumberOfLines,
393+
boolean includeFontPadding,
394+
int textBreakStrategy,
395+
int hyphenationFrequency) {
396+
BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance);
397+
Layout layout =
398+
createLayout(
399+
text,
400+
boring,
401+
width,
402+
widthYogaMeasureMode,
403+
includeFontPadding,
404+
textBreakStrategy,
405+
hyphenationFrequency);
406+
407+
// Minimum font size is 4pts to match the iOS implementation.
408+
int minimumFontSize =
409+
(int)
410+
(Double.isNaN(minimumFontSizeAttr) ? PixelUtil.toPixelFromDIP(4) : minimumFontSizeAttr);
411+
412+
// Find the largest font size used in the spannable to use as a starting point.
413+
int currentFontSize = minimumFontSize;
414+
ReactAbsoluteSizeSpan[] spans = text.getSpans(0, text.length(), ReactAbsoluteSizeSpan.class);
415+
for (ReactAbsoluteSizeSpan span : spans) {
416+
currentFontSize = Math.max(currentFontSize, span.getSize());
417+
}
418+
419+
int initialFontSize = currentFontSize;
420+
while (currentFontSize > minimumFontSize
421+
&& ((maximumNumberOfLines != ReactConstants.UNSET
422+
&& layout.getLineCount() > maximumNumberOfLines)
423+
|| (heightYogaMeasureMode != YogaMeasureMode.UNDEFINED
424+
&& layout.getHeight() > height))) {
425+
// TODO: We could probably use a smarter algorithm here. This will require 0(n)
426+
// measurements based on the number of points the font size needs to be reduced by.
427+
currentFontSize -= Math.max(1, (int) PixelUtil.toPixelFromDIP(1));
428+
429+
float ratio = (float) currentFontSize / (float) initialFontSize;
430+
ReactAbsoluteSizeSpan[] sizeSpans =
431+
text.getSpans(0, text.length(), ReactAbsoluteSizeSpan.class);
432+
for (ReactAbsoluteSizeSpan span : sizeSpans) {
433+
text.setSpan(
434+
new ReactAbsoluteSizeSpan((int) Math.max((span.getSize() * ratio), minimumFontSize)),
435+
text.getSpanStart(span),
436+
text.getSpanEnd(span),
437+
text.getSpanFlags(span));
438+
text.removeSpan(span);
439+
}
440+
layout =
441+
createLayout(
442+
text,
443+
boring,
444+
width,
445+
widthYogaMeasureMode,
446+
includeFontPadding,
447+
textBreakStrategy,
448+
hyphenationFrequency);
449+
}
450+
}
451+
381452
public static long measureText(
382453
Context context,
383454
MapBuffer attributedString,
@@ -407,6 +478,33 @@ public static long measureText(
407478
int hyphenationFrequency =
408479
TextAttributeProps.getHyphenationFrequency(
409480
paragraphAttributes.getString(PA_KEY_HYPHENATION_FREQUENCY));
481+
boolean adjustFontSizeToFit =
482+
paragraphAttributes.contains(PA_KEY_ADJUST_FONT_SIZE_TO_FIT)
483+
? paragraphAttributes.getBoolean(PA_KEY_ADJUST_FONT_SIZE_TO_FIT)
484+
: DEFAULT_ADJUST_FONT_SIZE_TO_FIT;
485+
int maximumNumberOfLines =
486+
paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES)
487+
? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
488+
: ReactConstants.UNSET;
489+
490+
if (adjustFontSizeToFit) {
491+
double minimumFontSize =
492+
paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SIZE)
493+
? paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SIZE)
494+
: Double.NaN;
495+
496+
adjustSpannableFontToFit(
497+
text,
498+
width,
499+
widthYogaMeasureMode,
500+
height,
501+
heightYogaMeasureMode,
502+
minimumFontSize,
503+
maximumNumberOfLines,
504+
includeFontPadding,
505+
textBreakStrategy,
506+
hyphenationFrequency);
507+
}
410508

411509
BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance);
412510
Layout layout =
@@ -419,11 +517,6 @@ public static long measureText(
419517
textBreakStrategy,
420518
hyphenationFrequency);
421519

422-
int maximumNumberOfLines =
423-
paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES)
424-
? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
425-
: ReactConstants.UNSET;
426-
427520
int calculatedLineCount =
428521
maximumNumberOfLines == ReactConstants.UNSET || maximumNumberOfLines == 0
429522
? layout.getLineCount()
@@ -574,7 +667,8 @@ public static WritableArray measureLines(
574667
@NonNull Context context,
575668
MapBuffer attributedString,
576669
MapBuffer paragraphAttributes,
577-
float width) {
670+
float width,
671+
float height) {
578672

579673
Spannable text = getOrCreateSpannableForText(context, attributedString, null);
580674
BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance);
@@ -589,6 +683,33 @@ public static WritableArray measureLines(
589683
int hyphenationFrequency =
590684
TextAttributeProps.getTextBreakStrategy(
591685
paragraphAttributes.getString(PA_KEY_HYPHENATION_FREQUENCY));
686+
boolean adjustFontSizeToFit =
687+
paragraphAttributes.contains(PA_KEY_ADJUST_FONT_SIZE_TO_FIT)
688+
? paragraphAttributes.getBoolean(PA_KEY_ADJUST_FONT_SIZE_TO_FIT)
689+
: DEFAULT_ADJUST_FONT_SIZE_TO_FIT;
690+
int maximumNumberOfLines =
691+
paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES)
692+
? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
693+
: ReactConstants.UNSET;
694+
695+
if (adjustFontSizeToFit) {
696+
double minimumFontSize =
697+
paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SIZE)
698+
? paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SIZE)
699+
: Double.NaN;
700+
701+
adjustSpannableFontToFit(
702+
text,
703+
width,
704+
YogaMeasureMode.EXACTLY,
705+
height,
706+
YogaMeasureMode.UNDEFINED,
707+
minimumFontSize,
708+
maximumNumberOfLines,
709+
includeFontPadding,
710+
textBreakStrategy,
711+
hyphenationFrequency);
712+
}
592713

593714
Layout layout =
594715
createLayout(

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1219,7 +1219,7 @@ private void updateCachedSpannable() {
12191219
}
12201220

12211221
addSpansFromStyleAttributes(sb);
1222-
TextLayoutManager.setCachedSpannabledForTag(getId(), sb);
1222+
TextLayoutManager.setCachedSpannableForTag(getId(), sb);
12231223
}
12241224

12251225
void setEventDispatcher(@Nullable EventDispatcher eventDispatcher) {

0 commit comments

Comments
 (0)