mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Implement adjustsFontSizeToFit on Android (#26389)
Summary: This adds support for `adjustsFontSizeToFit` and `minimumFontScale` on Android. The implementation tries to match closely the behaviour on iOS (hardcoded 4px min size for example). It uses a simpler linear algorithm for now, opened to improving it now if it is a deal breaker or in a follow up. See https://twitter.com/janicduplessis/status/1171147709979516929 for a more detailed thread about the implementation ## Changelog [Android] [Added] - Implement `adjustsFontSizeToFit` on Android Pull Request resolved: https://github.com/facebook/react-native/pull/26389 Test Plan: Tested by adding the existing `adjustsFontSizeToFit` example from the iOS text page to android. Also added a case for limiting size by using `maxHeight` instead of `numberOfLines`. Reviewed By: mdvacca Differential Revision: D17285473 Pulled By: JoshuaGross fbshipit-source-id: 43dbdb05e2d6418e9a390d11f921518bfa58e697
This commit is contained in:
committed by
Facebook Github Bot
parent
9f8e4accfa
commit
2c1913f0b3
@@ -90,6 +90,8 @@ public class ViewProps {
|
||||
public static final String NEEDS_OFFSCREEN_ALPHA_COMPOSITING = "needsOffscreenAlphaCompositing";
|
||||
public static final String NUMBER_OF_LINES = "numberOfLines";
|
||||
public static final String ELLIPSIZE_MODE = "ellipsizeMode";
|
||||
public static final String ADJUSTS_FONT_SIZE_TO_FIT = "adjustsFontSizeToFit";
|
||||
public static final String MINIMUM_FONT_SCALE = "minimumFontScale";
|
||||
public static final String ON = "on";
|
||||
public static final String RESIZE_MODE = "resizeMode";
|
||||
public static final String RESIZE_METHOD = "resizeMethod";
|
||||
|
||||
+18
-1
@@ -337,7 +337,6 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
(Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 0 : Layout.HYPHENATION_FREQUENCY_NONE;
|
||||
protected int mJustificationMode =
|
||||
(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 0 : Layout.JUSTIFICATION_MODE_NONE;
|
||||
protected TextTransform mTextTransform = TextTransform.UNSET;
|
||||
|
||||
protected float mTextShadowOffsetDx = 0;
|
||||
protected float mTextShadowOffsetDy = 0;
|
||||
@@ -347,6 +346,8 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
protected boolean mIsUnderlineTextDecorationSet = false;
|
||||
protected boolean mIsLineThroughTextDecorationSet = false;
|
||||
protected boolean mIncludeFontPadding = true;
|
||||
protected boolean mAdjustsFontSizeToFit = false;
|
||||
protected float mMinimumFontScale = 0;
|
||||
|
||||
/**
|
||||
* mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. mFontWeight can be {@link
|
||||
@@ -627,4 +628,20 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
}
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.ADJUSTS_FONT_SIZE_TO_FIT)
|
||||
public void setAdjustFontSizeToFit(boolean adjustsFontSizeToFit) {
|
||||
if (adjustsFontSizeToFit != mAdjustsFontSizeToFit) {
|
||||
mAdjustsFontSizeToFit = adjustsFontSizeToFit;
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.MINIMUM_FONT_SCALE)
|
||||
public void setMinimumFontScale(float minimumFontScale) {
|
||||
if (minimumFontScale != mMinimumFontScale) {
|
||||
mMinimumFontScale = minimumFontScale;
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -60,6 +60,11 @@ public abstract class ReactTextAnchorViewManager<T extends View, C extends React
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.ADJUSTS_FONT_SIZE_TO_FIT)
|
||||
public void setAdjustFontSizeToFit(ReactTextView view, boolean adjustsFontSizeToFit) {
|
||||
view.setAdjustFontSizeToFit(adjustsFontSizeToFit);
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.TEXT_ALIGN_VERTICAL)
|
||||
public void setTextAlignVertical(ReactTextView view, @Nullable String textAlignVertical) {
|
||||
if (textAlignVertical == null || "auto".equals(textAlignVertical)) {
|
||||
|
||||
+111
-83
@@ -25,6 +25,7 @@ import com.facebook.react.bridge.ReactSoftException;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ReactShadowNode;
|
||||
import com.facebook.react.uimanager.Spacing;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
@@ -66,96 +67,40 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
|
||||
YogaMeasureMode widthMode,
|
||||
float height,
|
||||
YogaMeasureMode heightMode) {
|
||||
|
||||
// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
|
||||
TextPaint textPaint = sTextPaintInstance;
|
||||
textPaint.setTextSize(mTextAttributes.getEffectiveFontSize());
|
||||
Layout layout;
|
||||
Spanned text =
|
||||
Spannable text =
|
||||
Assertions.assertNotNull(
|
||||
mPreparedSpannableText,
|
||||
"Spannable element has not been prepared in onBeforeLayout");
|
||||
BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint);
|
||||
float desiredWidth = boring == null ? Layout.getDesiredWidth(text, textPaint) : Float.NaN;
|
||||
|
||||
// technically, width should never be negative, but there is currently a bug in
|
||||
boolean unconstrainedWidth = widthMode == YogaMeasureMode.UNDEFINED || width < 0;
|
||||
Layout layout = measureSpannedText(text, width, widthMode);
|
||||
|
||||
Layout.Alignment alignment = Layout.Alignment.ALIGN_NORMAL;
|
||||
switch (getTextAlign()) {
|
||||
case Gravity.LEFT:
|
||||
alignment = Layout.Alignment.ALIGN_NORMAL;
|
||||
break;
|
||||
case Gravity.RIGHT:
|
||||
alignment = Layout.Alignment.ALIGN_OPPOSITE;
|
||||
break;
|
||||
case Gravity.CENTER_HORIZONTAL:
|
||||
alignment = Layout.Alignment.ALIGN_CENTER;
|
||||
break;
|
||||
}
|
||||
if (mAdjustsFontSizeToFit) {
|
||||
int initialFontSize = mTextAttributes.getEffectiveFontSize();
|
||||
int currentFontSize = mTextAttributes.getEffectiveFontSize();
|
||||
// Minimum font size is 4pts to match the iOS implementation.
|
||||
int minimumFontSize =
|
||||
(int) Math.max(mMinimumFontScale * initialFontSize, PixelUtil.toPixelFromDIP(4));
|
||||
while (currentFontSize > minimumFontSize
|
||||
&& (mNumberOfLines != UNSET && layout.getLineCount() > mNumberOfLines
|
||||
|| heightMode != 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 = currentFontSize - (int) PixelUtil.toPixelFromDIP(1);
|
||||
|
||||
if (boring == null
|
||||
&& (unconstrainedWidth
|
||||
|| (!YogaConstants.isUndefined(desiredWidth) && desiredWidth <= width))) {
|
||||
// Is used when the width is not known and the text is not boring, ie. if it contains
|
||||
// unicode characters.
|
||||
|
||||
int hintWidth = (int) Math.ceil(desiredWidth);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
layout =
|
||||
new StaticLayout(
|
||||
text, textPaint, hintWidth, alignment, 1.f, 0.f, mIncludeFontPadding);
|
||||
} else {
|
||||
StaticLayout.Builder builder =
|
||||
StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, hintWidth)
|
||||
.setAlignment(alignment)
|
||||
.setLineSpacing(0.f, 1.f)
|
||||
.setIncludePad(mIncludeFontPadding)
|
||||
.setBreakStrategy(mTextBreakStrategy)
|
||||
.setHyphenationFrequency(mHyphenationFrequency);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder.setJustificationMode(mJustificationMode);
|
||||
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);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
builder.setUseLineSpacingFromFallbacks(true);
|
||||
}
|
||||
layout = builder.build();
|
||||
}
|
||||
|
||||
} else if (boring != null && (unconstrainedWidth || boring.width <= width)) {
|
||||
// Is used for single-line, boring text when the width is either unknown or bigger
|
||||
// than the width of the text.
|
||||
layout =
|
||||
BoringLayout.make(
|
||||
text,
|
||||
textPaint,
|
||||
boring.width,
|
||||
alignment,
|
||||
1.f,
|
||||
0.f,
|
||||
boring,
|
||||
mIncludeFontPadding);
|
||||
} else {
|
||||
// Is used for multiline, boring text and the width is known.
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
layout =
|
||||
new StaticLayout(
|
||||
text, textPaint, (int) width, alignment, 1.f, 0.f, mIncludeFontPadding);
|
||||
} else {
|
||||
StaticLayout.Builder builder =
|
||||
StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, (int) width)
|
||||
.setAlignment(alignment)
|
||||
.setLineSpacing(0.f, 1.f)
|
||||
.setIncludePad(mIncludeFontPadding)
|
||||
.setBreakStrategy(mTextBreakStrategy)
|
||||
.setHyphenationFrequency(mHyphenationFrequency);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
builder.setUseLineSpacingFromFallbacks(true);
|
||||
}
|
||||
layout = builder.build();
|
||||
layout = measureSpannedText(text, width, widthMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +146,89 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
|
||||
}
|
||||
}
|
||||
|
||||
private Layout measureSpannedText(Spannable text, float width, YogaMeasureMode widthMode) {
|
||||
// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
|
||||
TextPaint textPaint = sTextPaintInstance;
|
||||
textPaint.setTextSize(mTextAttributes.getEffectiveFontSize());
|
||||
Layout layout;
|
||||
BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint);
|
||||
float desiredWidth = boring == null ? Layout.getDesiredWidth(text, textPaint) : Float.NaN;
|
||||
|
||||
// technically, width should never be negative, but there is currently a bug in
|
||||
boolean unconstrainedWidth = widthMode == YogaMeasureMode.UNDEFINED || width < 0;
|
||||
|
||||
Layout.Alignment alignment = Layout.Alignment.ALIGN_NORMAL;
|
||||
switch (getTextAlign()) {
|
||||
case Gravity.LEFT:
|
||||
alignment = Layout.Alignment.ALIGN_NORMAL;
|
||||
break;
|
||||
case Gravity.RIGHT:
|
||||
alignment = Layout.Alignment.ALIGN_OPPOSITE;
|
||||
break;
|
||||
case Gravity.CENTER_HORIZONTAL:
|
||||
alignment = Layout.Alignment.ALIGN_CENTER;
|
||||
break;
|
||||
}
|
||||
|
||||
if (boring == null
|
||||
&& (unconstrainedWidth
|
||||
|| (!YogaConstants.isUndefined(desiredWidth) && desiredWidth <= width))) {
|
||||
// Is used when the width is not known and the text is not boring, ie. if it contains
|
||||
// unicode characters.
|
||||
|
||||
int hintWidth = (int) Math.ceil(desiredWidth);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
layout =
|
||||
new StaticLayout(text, textPaint, hintWidth, alignment, 1.f, 0.f, mIncludeFontPadding);
|
||||
} else {
|
||||
StaticLayout.Builder builder =
|
||||
StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, hintWidth)
|
||||
.setAlignment(alignment)
|
||||
.setLineSpacing(0.f, 1.f)
|
||||
.setIncludePad(mIncludeFontPadding)
|
||||
.setBreakStrategy(mTextBreakStrategy)
|
||||
.setHyphenationFrequency(mHyphenationFrequency);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder.setJustificationMode(mJustificationMode);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
builder.setUseLineSpacingFromFallbacks(true);
|
||||
}
|
||||
layout = builder.build();
|
||||
}
|
||||
|
||||
} else if (boring != null && (unconstrainedWidth || boring.width <= width)) {
|
||||
// Is used for single-line, boring text when the width is either unknown or bigger
|
||||
// than the width of the text.
|
||||
layout =
|
||||
BoringLayout.make(
|
||||
text, textPaint, boring.width, alignment, 1.f, 0.f, boring, mIncludeFontPadding);
|
||||
} else {
|
||||
// Is used for multiline, boring text and the width is known.
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
layout =
|
||||
new StaticLayout(
|
||||
text, textPaint, (int) width, alignment, 1.f, 0.f, mIncludeFontPadding);
|
||||
} else {
|
||||
StaticLayout.Builder builder =
|
||||
StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, (int) width)
|
||||
.setAlignment(alignment)
|
||||
.setLineSpacing(0.f, 1.f)
|
||||
.setIncludePad(mIncludeFontPadding)
|
||||
.setBreakStrategy(mTextBreakStrategy)
|
||||
.setHyphenationFrequency(mHyphenationFrequency);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
builder.setUseLineSpacingFromFallbacks(true);
|
||||
}
|
||||
layout = builder.build();
|
||||
}
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
// Return text alignment according to LTR or RTL style
|
||||
private int getTextAlign() {
|
||||
int textAlign = mTextAlign;
|
||||
|
||||
@@ -51,6 +51,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
|
||||
private int mTextAlign = Gravity.NO_GRAVITY;
|
||||
private int mNumberOfLines = ViewDefaults.NUMBER_OF_LINES;
|
||||
private TextUtils.TruncateAt mEllipsizeLocation = TextUtils.TruncateAt.END;
|
||||
private boolean mAdjustsFontSizeToFit = false;
|
||||
private int mLinkifyMaskType = 0;
|
||||
private boolean mNotifyOnInlineViewLayout;
|
||||
|
||||
@@ -474,6 +475,10 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
|
||||
setMaxLines(mNumberOfLines);
|
||||
}
|
||||
|
||||
public void setAdjustFontSizeToFit(boolean adjustsFontSizeToFit) {
|
||||
mAdjustsFontSizeToFit = adjustsFontSizeToFit;
|
||||
}
|
||||
|
||||
public void setEllipsizeLocation(TextUtils.TruncateAt ellipsizeLocation) {
|
||||
mEllipsizeLocation = ellipsizeLocation;
|
||||
}
|
||||
@@ -485,7 +490,9 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
|
||||
public void updateView() {
|
||||
@Nullable
|
||||
TextUtils.TruncateAt ellipsizeLocation =
|
||||
mNumberOfLines == ViewDefaults.NUMBER_OF_LINES ? null : mEllipsizeLocation;
|
||||
mNumberOfLines == ViewDefaults.NUMBER_OF_LINES || mAdjustsFontSizeToFit
|
||||
? null
|
||||
: mEllipsizeLocation;
|
||||
setEllipsize(ellipsizeLocation);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user