Add implementation of adjustsFontSizeToFit on the new architecture on Android (#44075)

Summary:
`adjustsFontSizeToFit` prop is exposed on both platforms but is missing implementation on the new arch on Android. This Pr adds it.

Fixes https://github.com/facebook/react-native/issues/43104

## Changelog:

[ANDROID] [FIXED] - Fixed `adjustsFontSizeToFit`  not working on Android when using the new architecture

Pull Request resolved: https://github.com/facebook/react-native/pull/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
This commit is contained in:
Jakub Piasecki
2024-04-17 08:57:41 -07:00
committed by Facebook GitHub Bot
parent 46366867f7
commit 747a96b7b3
8 changed files with 205 additions and 13 deletions
@@ -7311,6 +7311,7 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi
public fun invalidateDrawable (Landroid/graphics/drawable/Drawable;)V
public fun onAttachedToWindow ()V
public fun onDetachedFromWindow ()V
protected fun onDraw (Landroid/graphics/Canvas;)V
public fun onFinishTemporaryDetach ()V
protected fun onLayout (ZIIII)V
public fun onStartTemporaryDetach ()V
@@ -7322,10 +7323,14 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi
public fun setBorderRadius (FI)V
public fun setBorderStyle (Ljava/lang/String;)V
public fun setBorderWidth (IF)V
public fun setBreakStrategy (I)V
public fun setEllipsizeLocation (Landroid/text/TextUtils$TruncateAt;)V
public fun setFontSize (F)V
public fun setHyphenationFrequency (I)V
public fun setIncludeFontPadding (Z)V
public fun setLetterSpacing (F)V
public fun setLinkifyMask (I)V
public fun setMinimumFontSize (F)V
public fun setNotifyOnInlineViewLayout (Z)V
public fun setNumberOfLines (I)V
public fun setSpanned (Landroid/text/Spannable;)V
@@ -7530,7 +7535,7 @@ public class com/facebook/react/views/text/TextLayoutManager {
public static fun isRTL (Lcom/facebook/react/bridge/ReadableMap;)Z
public static fun measureLines (Landroid/content/Context;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;F)Lcom/facebook/react/bridge/WritableArray;
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
public static fun setCachedSpannabledForTag (ILandroid/text/Spannable;)V
public static fun setCachedSpannableForTag (ILandroid/text/Spannable;)V
}
public class com/facebook/react/views/text/TextLayoutManagerMapBuffer {
@@ -7548,15 +7553,18 @@ public class com/facebook/react/views/text/TextLayoutManagerMapBuffer {
public static final field PA_KEY_ELLIPSIZE_MODE S
public static final field PA_KEY_HYPHENATION_FREQUENCY S
public static final field PA_KEY_INCLUDE_FONT_PADDING S
public static final field PA_KEY_MAXIMUM_FONT_SIZE S
public static final field PA_KEY_MAX_NUMBER_OF_LINES S
public static final field PA_KEY_MINIMUM_FONT_SIZE S
public static final field PA_KEY_TEXT_BREAK_STRATEGY S
public fun <init> ()V
public static fun adjustSpannableFontToFit (Landroid/text/Spannable;FLcom/facebook/yoga/YogaMeasureMode;FLcom/facebook/yoga/YogaMeasureMode;DIZII)V
public static fun deleteCachedSpannableForTag (I)V
public static fun getOrCreateSpannableForText (Landroid/content/Context;Lcom/facebook/react/common/mapbuffer/MapBuffer;Lcom/facebook/react/views/text/ReactTextViewManagerCallback;)Landroid/text/Spannable;
public static fun isRTL (Lcom/facebook/react/common/mapbuffer/MapBuffer;)Z
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;
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;
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
public static fun setCachedSpannabledForTag (ILandroid/text/Spannable;)V
public static fun setCachedSpannableForTag (ILandroid/text/Spannable;)V
}
public final class com/facebook/react/views/text/TextTransform : java/lang/Enum {
@@ -513,7 +513,8 @@ public class FabricUIManager implements UIManager, LifecycleEventListener, UIBlo
mReactApplicationContext,
attributedString,
paragraphAttributes,
PixelUtil.toPixelFromDIP(width));
PixelUtil.toPixelFromDIP(width),
PixelUtil.toPixelFromDIP(height));
}
@SuppressWarnings("unused")
@@ -8,6 +8,7 @@
package com.facebook.react.views.text;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Layout;
@@ -44,6 +45,7 @@ import com.facebook.react.views.text.internal.span.ReactTagSpan;
import com.facebook.react.views.text.internal.span.TextInlineImageSpan;
import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan;
import com.facebook.react.views.view.ReactViewBackgroundManager;
import com.facebook.yoga.YogaMeasureMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -61,10 +63,12 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
private TextUtils.TruncateAt mEllipsizeLocation;
private boolean mAdjustsFontSizeToFit;
private float mFontSize;
private float mMinimumFontSize;
private float mLetterSpacing;
private int mLinkifyMaskType;
private boolean mNotifyOnInlineViewLayout;
private boolean mTextIsSelectable;
private boolean mShouldAdjustSpannableFontSize;
private ReactViewBackgroundManager mReactBackgroundManager;
private Spannable mSpanned;
@@ -92,8 +96,10 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
mLinkifyMaskType = 0;
mNotifyOnInlineViewLayout = false;
mTextIsSelectable = false;
mShouldAdjustSpannableFontSize = false;
mEllipsizeLocation = TextUtils.TruncateAt.END;
mFontSize = Float.NaN;
mMinimumFontSize = Float.NaN;
mLetterSpacing = 0.f;
mSpanned = null;
@@ -355,6 +361,27 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mAdjustsFontSizeToFit && getSpanned() != null && mShouldAdjustSpannableFontSize) {
mShouldAdjustSpannableFontSize = false;
TextLayoutManagerMapBuffer.adjustSpannableFontToFit(
getSpanned(),
getWidth(),
YogaMeasureMode.EXACTLY,
getHeight(),
YogaMeasureMode.EXACTLY,
mMinimumFontSize,
mNumberOfLines,
getIncludeFontPadding(),
getBreakStrategy(),
getHyphenationFrequency());
setText(getSpanned());
}
super.onDraw(canvas);
}
public void setText(ReactTextUpdate update) {
mContainsImages = update.containsImages();
// Android's TextView crashes when it tries to relayout if LayoutParams are
@@ -575,6 +602,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
public void setNumberOfLines(int numberOfLines) {
mNumberOfLines = numberOfLines == 0 ? ViewDefaults.NUMBER_OF_LINES : numberOfLines;
setMaxLines(mNumberOfLines);
mShouldAdjustSpannableFontSize = true;
}
public void setAdjustFontSizeToFit(boolean adjustsFontSizeToFit) {
@@ -590,6 +618,29 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
applyTextAttributes();
}
public void setMinimumFontSize(float minimumFontSize) {
mMinimumFontSize = minimumFontSize;
mShouldAdjustSpannableFontSize = true;
}
@Override
public void setIncludeFontPadding(boolean includepad) {
super.setIncludeFontPadding(includepad);
mShouldAdjustSpannableFontSize = true;
}
@Override
public void setBreakStrategy(int breakStrategy) {
super.setBreakStrategy(breakStrategy);
mShouldAdjustSpannableFontSize = true;
}
@Override
public void setHyphenationFrequency(int hyphenationFrequency) {
super.setHyphenationFrequency(hyphenationFrequency);
mShouldAdjustSpannableFontSize = true;
}
public void setLetterSpacing(float letterSpacing) {
if (Float.isNaN(letterSpacing)) {
return;
@@ -648,6 +699,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
public void setSpanned(Spannable spanned) {
mSpanned = spanned;
mShouldAdjustSpannableFontSize = true;
}
public Spannable getSpanned() {
@@ -176,6 +176,10 @@ public class ReactTextViewManager
view.getContext(), attributedString, mReactTextViewManagerCallback);
view.setSpanned(spanned);
float minimumFontSize =
(float) paragraphAttributes.getDouble(TextLayoutManagerMapBuffer.PA_KEY_MINIMUM_FONT_SIZE);
view.setMinimumFontSize(minimumFontSize);
int textBreakStrategy =
TextAttributeProps.getTextBreakStrategy(
paragraphAttributes.getString(TextLayoutManagerMapBuffer.PA_KEY_TEXT_BREAK_STRATEGY));
@@ -99,7 +99,7 @@ public class TextLayoutManager {
return false;
}
public static void setCachedSpannabledForTag(int reactTag, @NonNull Spannable sp) {
public static void setCachedSpannableForTag(int reactTag, @NonNull Spannable sp) {
if (ENABLE_MEASURE_LOGGING) {
FLog.e(TAG, "Set cached spannable for tag[" + reactTag + "]: " + sp.toString());
}
@@ -81,6 +81,8 @@ public class TextLayoutManagerMapBuffer {
public static final short PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3;
public static final short PA_KEY_INCLUDE_FONT_PADDING = 4;
public static final short PA_KEY_HYPHENATION_FREQUENCY = 5;
public static final short PA_KEY_MINIMUM_FONT_SIZE = 6;
public static final short PA_KEY_MAXIMUM_FONT_SIZE = 7;
private static final boolean ENABLE_MEASURE_LOGGING = ReactBuildConfig.DEBUG && false;
@@ -98,6 +100,8 @@ public class TextLayoutManagerMapBuffer {
private static final boolean DEFAULT_INCLUDE_FONT_PADDING = true;
private static final boolean DEFAULT_ADJUST_FONT_SIZE_TO_FIT = false;
private static final Object sCacheLock = new Object();
private static final ConcurrentHashMap<Integer, Spannable> sTagToSpannableCache =
@@ -106,7 +110,7 @@ public class TextLayoutManagerMapBuffer {
private static final LruCache<ReadableMapBuffer, Spannable> sSpannableCache =
new LruCache<>(spannableCacheSize);
public static void setCachedSpannabledForTag(int reactTag, @NonNull Spannable sp) {
public static void setCachedSpannableForTag(int reactTag, @NonNull Spannable sp) {
if (ENABLE_MEASURE_LOGGING) {
FLog.e(TAG, "Set cached spannable for tag[" + reactTag + "]: " + sp.toString());
}
@@ -378,6 +382,73 @@ public class TextLayoutManagerMapBuffer {
return layout;
}
public static void adjustSpannableFontToFit(
Spannable text,
float width,
YogaMeasureMode widthYogaMeasureMode,
float height,
YogaMeasureMode heightYogaMeasureMode,
double minimumFontSizeAttr,
int maximumNumberOfLines,
boolean includeFontPadding,
int textBreakStrategy,
int hyphenationFrequency) {
BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance);
Layout layout =
createLayout(
text,
boring,
width,
widthYogaMeasureMode,
includeFontPadding,
textBreakStrategy,
hyphenationFrequency);
// Minimum font size is 4pts to match the iOS implementation.
int minimumFontSize =
(int)
(Double.isNaN(minimumFontSizeAttr) ? PixelUtil.toPixelFromDIP(4) : minimumFontSizeAttr);
// Find the largest font size used in the spannable to use as a starting point.
int currentFontSize = minimumFontSize;
ReactAbsoluteSizeSpan[] spans = text.getSpans(0, text.length(), ReactAbsoluteSizeSpan.class);
for (ReactAbsoluteSizeSpan span : spans) {
currentFontSize = Math.max(currentFontSize, span.getSize());
}
int initialFontSize = currentFontSize;
while (currentFontSize > minimumFontSize
&& ((maximumNumberOfLines != ReactConstants.UNSET
&& layout.getLineCount() > maximumNumberOfLines)
|| (heightYogaMeasureMode != YogaMeasureMode.UNDEFINED
&& layout.getHeight() > height))) {
// TODO: We could probably use a smarter algorithm here. This will require 0(n)
// measurements based on the number of points the font size needs to be reduced by.
currentFontSize -= Math.max(1, (int) PixelUtil.toPixelFromDIP(1));
float ratio = (float) currentFontSize / (float) initialFontSize;
ReactAbsoluteSizeSpan[] sizeSpans =
text.getSpans(0, text.length(), ReactAbsoluteSizeSpan.class);
for (ReactAbsoluteSizeSpan span : sizeSpans) {
text.setSpan(
new ReactAbsoluteSizeSpan((int) Math.max((span.getSize() * ratio), minimumFontSize)),
text.getSpanStart(span),
text.getSpanEnd(span),
text.getSpanFlags(span));
text.removeSpan(span);
}
layout =
createLayout(
text,
boring,
width,
widthYogaMeasureMode,
includeFontPadding,
textBreakStrategy,
hyphenationFrequency);
}
}
public static long measureText(
Context context,
MapBuffer attributedString,
@@ -407,6 +478,33 @@ public class TextLayoutManagerMapBuffer {
int hyphenationFrequency =
TextAttributeProps.getHyphenationFrequency(
paragraphAttributes.getString(PA_KEY_HYPHENATION_FREQUENCY));
boolean adjustFontSizeToFit =
paragraphAttributes.contains(PA_KEY_ADJUST_FONT_SIZE_TO_FIT)
? paragraphAttributes.getBoolean(PA_KEY_ADJUST_FONT_SIZE_TO_FIT)
: DEFAULT_ADJUST_FONT_SIZE_TO_FIT;
int maximumNumberOfLines =
paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES)
? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
: ReactConstants.UNSET;
if (adjustFontSizeToFit) {
double minimumFontSize =
paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SIZE)
? paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SIZE)
: Double.NaN;
adjustSpannableFontToFit(
text,
width,
widthYogaMeasureMode,
height,
heightYogaMeasureMode,
minimumFontSize,
maximumNumberOfLines,
includeFontPadding,
textBreakStrategy,
hyphenationFrequency);
}
BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance);
Layout layout =
@@ -419,11 +517,6 @@ public class TextLayoutManagerMapBuffer {
textBreakStrategy,
hyphenationFrequency);
int maximumNumberOfLines =
paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES)
? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
: ReactConstants.UNSET;
int calculatedLineCount =
maximumNumberOfLines == ReactConstants.UNSET || maximumNumberOfLines == 0
? layout.getLineCount()
@@ -574,7 +667,8 @@ public class TextLayoutManagerMapBuffer {
@NonNull Context context,
MapBuffer attributedString,
MapBuffer paragraphAttributes,
float width) {
float width,
float height) {
Spannable text = getOrCreateSpannableForText(context, attributedString, null);
BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance);
@@ -589,6 +683,33 @@ public class TextLayoutManagerMapBuffer {
int hyphenationFrequency =
TextAttributeProps.getTextBreakStrategy(
paragraphAttributes.getString(PA_KEY_HYPHENATION_FREQUENCY));
boolean adjustFontSizeToFit =
paragraphAttributes.contains(PA_KEY_ADJUST_FONT_SIZE_TO_FIT)
? paragraphAttributes.getBoolean(PA_KEY_ADJUST_FONT_SIZE_TO_FIT)
: DEFAULT_ADJUST_FONT_SIZE_TO_FIT;
int maximumNumberOfLines =
paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES)
? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
: ReactConstants.UNSET;
if (adjustFontSizeToFit) {
double minimumFontSize =
paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SIZE)
? paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SIZE)
: Double.NaN;
adjustSpannableFontToFit(
text,
width,
YogaMeasureMode.EXACTLY,
height,
YogaMeasureMode.UNDEFINED,
minimumFontSize,
maximumNumberOfLines,
includeFontPadding,
textBreakStrategy,
hyphenationFrequency);
}
Layout layout =
createLayout(
@@ -1219,7 +1219,7 @@ public class ReactEditText extends AppCompatEditText {
}
addSpansFromStyleAttributes(sb);
TextLayoutManager.setCachedSpannabledForTag(getId(), sb);
TextLayoutManager.setCachedSpannableForTag(getId(), sb);
}
void setEventDispatcher(@Nullable EventDispatcher eventDispatcher) {
@@ -983,6 +983,8 @@ constexpr static MapBuffer::Key PA_KEY_TEXT_BREAK_STRATEGY = 2;
constexpr static MapBuffer::Key PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3;
constexpr static MapBuffer::Key PA_KEY_INCLUDE_FONT_PADDING = 4;
constexpr static MapBuffer::Key PA_KEY_HYPHENATION_FREQUENCY = 5;
constexpr static MapBuffer::Key PA_KEY_MINIMUM_FONT_SIZE = 6;
constexpr static MapBuffer::Key PA_KEY_MAXIMUM_FONT_SIZE = 7;
inline MapBuffer toMapBuffer(const ParagraphAttributes& paragraphAttributes) {
auto builder = MapBufferBuilder();
@@ -1000,6 +1002,10 @@ inline MapBuffer toMapBuffer(const ParagraphAttributes& paragraphAttributes) {
builder.putString(
PA_KEY_HYPHENATION_FREQUENCY,
toString(paragraphAttributes.android_hyphenationFrequency));
builder.putDouble(
PA_KEY_MINIMUM_FONT_SIZE, paragraphAttributes.minimumFontSize);
builder.putDouble(
PA_KEY_MAXIMUM_FONT_SIZE, paragraphAttributes.maximumFontSize);
return builder.build();
}