diff --git a/RNTester/js/examples/Text/TextExample.android.js b/RNTester/js/examples/Text/TextExample.android.js
index ae3aa5c81179da..1af502d1bb8957 100644
--- a/RNTester/js/examples/Text/TextExample.android.js
+++ b/RNTester/js/examples/Text/TextExample.android.js
@@ -16,7 +16,7 @@ const React = require('react');
const TextInlineView = require('../../components/TextInlineView');
const TextLegend = require('../../components/TextLegend');
-const {StyleSheet, Text, View} = require('react-native');
+const {LayoutAnimation, StyleSheet, Text, View} = require('react-native');
class Entity extends React.Component<{|children: React.Node|}> {
render() {
@@ -70,10 +70,137 @@ class AttributeToggler extends React.Component<{}, $FlowFixMeState> {
}
}
+type AdjustingFontSizeProps = $ReadOnly<{||}>;
+
+type AdjustingFontSizeState = {|
+ dynamicText: string,
+ shouldRender: boolean,
+|};
+
+class AdjustingFontSize extends React.Component<
+ AdjustingFontSizeProps,
+ AdjustingFontSizeState,
+> {
+ state = {
+ dynamicText: '',
+ shouldRender: true,
+ };
+
+ reset = () => {
+ LayoutAnimation.easeInEaseOut();
+ this.setState({
+ shouldRender: false,
+ });
+ setTimeout(() => {
+ LayoutAnimation.easeInEaseOut();
+ this.setState({
+ dynamicText: '',
+ shouldRender: true,
+ });
+ }, 300);
+ };
+
+ addText = () => {
+ this.setState({
+ dynamicText:
+ this.state.dynamicText +
+ (Math.floor((Math.random() * 10) % 2) ? ' foo' : ' bar'),
+ });
+ };
+
+ removeText = () => {
+ this.setState({
+ dynamicText: this.state.dynamicText.slice(
+ 0,
+ this.state.dynamicText.length - 4,
+ ),
+ });
+ };
+
+ render() {
+ if (!this.state.shouldRender) {
+ return ;
+ }
+ return (
+
+
+ Truncated text is baaaaad.
+
+
+ Shrinking to fit available space is much better!
+
+
+
+ {'Add text to me to watch me shrink!' + ' ' + this.state.dynamicText}
+
+
+
+ {'Multiline text component shrinking is supported, watch as this reeeeaaaally loooooong teeeeeeext grooooows and then shriiiinks as you add text to me! ioahsdia soady auydoa aoisyd aosdy ' +
+ ' ' +
+ this.state.dynamicText}
+
+
+
+ {'Text limited by height, watch as this reeeeaaaally loooooong teeeeeeext grooooows and then shriiiinks as you add text to me! ioahsdia soady auydoa aoisyd aosdy ' +
+ ' ' +
+ this.state.dynamicText}
+
+
+
+
+ {'Differently sized nested elements will shrink together. '}
+
+
+ {'LARGE TEXT! ' + this.state.dynamicText}
+
+
+
+
+
+ Reset
+
+
+ Remove Text
+
+
+ Add Text
+
+
+
+ );
+ }
+}
+
class TextExample extends React.Component<{}> {
render(): React.Node {
return (
+
+
+
The text should wrap if it goes on multiple lines. See, this is
diff --git a/RNTester/js/examples/Text/TextExample.ios.js b/RNTester/js/examples/Text/TextExample.ios.js
index 8b28c6dfb26756..0d7ef7740b26a2 100644
--- a/RNTester/js/examples/Text/TextExample.ios.js
+++ b/RNTester/js/examples/Text/TextExample.ios.js
@@ -222,6 +222,14 @@ class AdjustingFontSize extends React.Component<
this.state.dynamicText}
+
+ {'Text limited by height, watch as this reeeeaaaally loooooong teeeeeeext grooooows and then shriiiinks as you add text to me! ioahsdia soady auydoa aoisyd aosdy ' +
+ ' ' +
+ this.state.dynamicText}
+
+
= 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;
+ }
+
private final YogaMeasureFunction mTextMeasureFunction =
new YogaMeasureFunction() {
@Override
@@ -62,96 +154,38 @@ public long measure(
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.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 layout = measureSpannedText(text, width, widthMode);
+
+ 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);
+
+ 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 = builder.build();
+ layout = measureSpannedText(text, width, widthMode);
}
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java
index 9087ba2bb04727..0b69380b61dfe9 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java
@@ -48,6 +48,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;
@@ -456,6 +457,10 @@ public void setNumberOfLines(int numberOfLines) {
setMaxLines(mNumberOfLines);
}
+ public void setAdjustFontSizeToFit(boolean adjustsFontSizeToFit) {
+ mAdjustsFontSizeToFit = adjustsFontSizeToFit;
+ }
+
public void setEllipsizeLocation(TextUtils.TruncateAt ellipsizeLocation) {
mEllipsizeLocation = ellipsizeLocation;
}
@@ -467,7 +472,7 @@ public void setNotifyOnInlineViewLayout(boolean notifyOnInlineViewLayout) {
public void updateView() {
@Nullable
TextUtils.TruncateAt ellipsizeLocation =
- mNumberOfLines == ViewDefaults.NUMBER_OF_LINES ? null : mEllipsizeLocation;
+ mNumberOfLines == ViewDefaults.NUMBER_OF_LINES || mAdjustsFontSizeToFit ? null : mEllipsizeLocation;
setEllipsize(ellipsizeLocation);
}