Skip to content
Permalink
Browse files
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: #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
  • Loading branch information
janicduplessis authored and facebook-github-bot committed Feb 10, 2020
1 parent 9f8e4ac commit 2c1913f0b3d12147654501f7ee43af1d313655d8
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 88 deletions.
@@ -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 <View />;
}
return (
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={{fontSize: 36, marginVertical: 6}}>
Truncated text is baaaaad.
</Text>
<Text
numberOfLines={1}
adjustsFontSizeToFit={true}
style={{fontSize: 40, marginVertical: 6}}>
Shrinking to fit available space is much better!
</Text>

<Text
adjustsFontSizeToFit={true}
numberOfLines={1}
style={{fontSize: 30, marginVertical: 6}}>
{'Add text to me to watch me shrink!' + ' ' + this.state.dynamicText}
</Text>

<Text
adjustsFontSizeToFit={true}
numberOfLines={4}
style={{fontSize: 20, marginVertical: 6}}>
{'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>

<Text
adjustsFontSizeToFit={true}
style={{fontSize: 20, marginVertical: 6, maxHeight: 50}}>
{'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}
</Text>

<Text
adjustsFontSizeToFit={true}
numberOfLines={1}
style={{marginVertical: 6}}>
<Text style={{fontSize: 14}}>
{'Differently sized nested elements will shrink together. '}
</Text>
<Text style={{fontSize: 20}}>
{'LARGE TEXT! ' + this.state.dynamicText}
</Text>
</Text>

<View
style={{
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: 5,
marginVertical: 6,
}}>
<Text style={{backgroundColor: '#ffaaaa'}} onPress={this.reset}>
Reset
</Text>
<Text style={{backgroundColor: '#aaaaff'}} onPress={this.removeText}>
Remove Text
</Text>
<Text style={{backgroundColor: '#aaffaa'}} onPress={this.addText}>
Add Text
</Text>
</View>
</View>
);
}
}

class TextExample extends React.Component<{...}> {
render(): React.Node {
return (
<RNTesterPage title="<Text>">
<RNTesterBlock title="Dynamic Font Size Adjustment">
<AdjustingFontSize />
</RNTesterBlock>
<RNTesterBlock title="Wrap">
<Text>
The text should wrap if it goes on multiple lines. See, this is
@@ -222,6 +222,14 @@ class AdjustingFontSize extends React.Component<
this.state.dynamicText}
</Text>

<Text
adjustsFontSizeToFit={true}
style={{fontSize: 20, marginVertical: 6, maxHeight: 50}}>
{'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}
</Text>

<Text
adjustsFontSizeToFit={true}
numberOfLines={1}
@@ -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";
@@ -337,7 +337,6 @@ protected Spannable spannedFromShadowNode(
(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 @@ protected Spannable spannedFromShadowNode(
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 void setTextTransform(@Nullable String textTransform) {
}
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();
}
}
}
@@ -60,6 +60,11 @@ public void setEllipsizeMode(ReactTextView view, @Nullable String ellipsizeMode)
}
}

@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)) {

0 comments on commit 2c1913f

Please sign in to comment.