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); }