Android: Add a maxFontSizeMultiplier prop to <Text> and <TextInput> (#23069)

Summary:
Equivalent of this iOS PR: https://github.com/facebook/react-native/pull/20915

Motivation:
----------

Whenever a user changes the system font size to its maximum allowable setting, React Native apps that allow font scaling can become unusable because the text gets too big. Experimenting with a native app like iMessage on iOS, the font size used for non-body text (e.g. header, navigational elements) is capped while the body text (e.g. text in the message bubbles) is allowed to grow.

This PR introduces a new prop on `<Text>` and `<TextInput>` called `maxFontSizeMultiplier`. This enables devs to set the maximum allowed text scale factor on a Text/TextInput. The default is 0 which means no limit.
Pull Request resolved: https://github.com/facebook/react-native/pull/23069

Differential Revision: D13748513

Pulled By: mdvacca

fbshipit-source-id: 8dd5d6d97bf79387d9a2236fa2e586ccb01afde9
This commit is contained in:
Adam Comella
2019-01-23 11:29:34 -08:00
committed by Facebook Github Bot
parent 5bc709d517
commit 4936d284df
6 changed files with 67 additions and 8 deletions
@@ -7,6 +7,7 @@
package com.facebook.react.uimanager;
import android.util.DisplayMetrics;
import android.util.TypedValue;
/**
@@ -42,10 +43,21 @@ public class PixelUtil {
* Convert from SP to PX
*/
public static float toPixelFromSP(float value) {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
value,
DisplayMetricsHolder.getWindowDisplayMetrics());
return toPixelFromSP(value, Float.NaN);
}
/**
* Convert from SP to PX
*/
public static float toPixelFromSP(float value, float maxFontScale) {
DisplayMetrics displayMetrics = DisplayMetricsHolder.getWindowDisplayMetrics();
float scaledDensity = displayMetrics.scaledDensity;
float currentFontScale = scaledDensity / displayMetrics.density;
if (maxFontScale >= 1 && maxFontScale < currentFontScale) {
scaledDensity = displayMetrics.density * maxFontScale;
}
return value * scaledDensity;
}
/**
@@ -106,6 +106,7 @@ public class ViewProps {
public static final String VISIBLE = "visible";
public static final String ALLOW_FONT_SCALING = "allowFontScaling";
public static final String MAX_FONT_SIZE_MULTIPLIER = "maxFontSizeMultiplier";
public static final String INCLUDE_FONT_PADDING = "includeFontPadding";
public static final String BORDER_WIDTH = "borderWidth";
@@ -349,6 +349,14 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
}
}
@ReactProp(name = ViewProps.MAX_FONT_SIZE_MULTIPLIER, defaultFloat = Float.NaN)
public void setMaxFontSizeMultiplier(float maxFontSizeMultiplier) {
if (maxFontSizeMultiplier != mTextAttributes.getMaxFontSizeMultiplier()) {
mTextAttributes.setMaxFontSizeMultiplier(maxFontSizeMultiplier);
markUpdated();
}
}
@ReactProp(name = ViewProps.TEXT_ALIGN)
public void setTextAlign(@Nullable String textAlign) {
if (textAlign == null || "auto".equals(textAlign)) {
@@ -7,6 +7,7 @@
package com.facebook.react.views.text;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ViewDefaults;
@@ -15,13 +16,17 @@ import com.facebook.react.uimanager.ViewDefaults;
* to child so inheritance can be implemented correctly. An example complexity that causes a prop
* to end up in TextAttributes is when multiple props need to be considered together to determine
* the rendered aka effective value. For example, to figure out the rendered/effective font size,
* you need to take into account the fontSize and allowFontScaling props.
* you need to take into account the fontSize, maxFontSizeMultiplier, and allowFontScaling props.
*/
public class TextAttributes {
// Setting the default to 0 indicates that there is no max.
public static final float DEFAULT_MAX_FONT_SIZE_MULTIPLIER = 0.0f;
private boolean mAllowFontScaling = true;
private float mFontSize = Float.NaN;
private float mLineHeight = Float.NaN;
private float mLetterSpacing = Float.NaN;
private float mMaxFontSizeMultiplier = Float.NaN;
private float mHeightOfTallestInlineImage = Float.NaN;
public TextAttributes() {
@@ -37,6 +42,7 @@ public class TextAttributes {
result.mFontSize = !Float.isNaN(child.mFontSize) ? child.mFontSize : mFontSize;
result.mLineHeight = !Float.isNaN(child.mLineHeight) ? child.mLineHeight : mLineHeight;
result.mLetterSpacing = !Float.isNaN(child.mLetterSpacing) ? child.mLetterSpacing : mLetterSpacing;
result.mMaxFontSizeMultiplier = !Float.isNaN(child.mMaxFontSizeMultiplier) ? child.mMaxFontSizeMultiplier : mMaxFontSizeMultiplier;
result.mHeightOfTallestInlineImage = !Float.isNaN(child.mHeightOfTallestInlineImage) ? child.mHeightOfTallestInlineImage : mHeightOfTallestInlineImage;
return result;
@@ -77,6 +83,17 @@ public class TextAttributes {
mLetterSpacing = value;
}
public float getMaxFontSizeMultiplier() {
return mMaxFontSizeMultiplier;
}
public void setMaxFontSizeMultiplier(float maxFontSizeMultiplier) {
if (maxFontSizeMultiplier != 0 && maxFontSizeMultiplier < 1) {
throw new JSApplicationIllegalArgumentException("maxFontSizeMultiplier must be NaN, 0, or >= 1");
}
mMaxFontSizeMultiplier = maxFontSizeMultiplier;
}
public float getHeightOfTallestInlineImage() {
return mHeightOfTallestInlineImage;
}
@@ -94,7 +111,7 @@ public class TextAttributes {
public int getEffectiveFontSize() {
float fontSize = !Float.isNaN(mFontSize) ? mFontSize : ViewDefaults.FONT_SIZE_SP;
return mAllowFontScaling
? (int) Math.ceil(PixelUtil.toPixelFromSP(fontSize))
? (int) Math.ceil(PixelUtil.toPixelFromSP(fontSize, getEffectiveMaxFontSizeMultiplier()))
: (int) Math.ceil(PixelUtil.toPixelFromDIP(fontSize));
}
@@ -104,7 +121,7 @@ public class TextAttributes {
}
float lineHeight = mAllowFontScaling
? PixelUtil.toPixelFromSP(mLineHeight)
? PixelUtil.toPixelFromSP(mLineHeight, getEffectiveMaxFontSizeMultiplier())
: PixelUtil.toPixelFromDIP(mLineHeight);
// Take into account the requested line height
@@ -121,7 +138,7 @@ public class TextAttributes {
}
float letterSpacingPixels = mAllowFontScaling
? PixelUtil.toPixelFromSP(mLetterSpacing)
? PixelUtil.toPixelFromSP(mLetterSpacing, getEffectiveMaxFontSizeMultiplier())
: PixelUtil.toPixelFromDIP(mLetterSpacing);
// `letterSpacingPixels` and `getEffectiveFontSize` are both in pixels,
@@ -129,6 +146,13 @@ public class TextAttributes {
return letterSpacingPixels / getEffectiveFontSize();
}
// Never returns NaN
public float getEffectiveMaxFontSizeMultiplier() {
return !Float.isNaN(mMaxFontSizeMultiplier)
? mMaxFontSizeMultiplier
: DEFAULT_MAX_FONT_SIZE_MULTIPLIER;
}
public String toString() {
return (
"TextAttributes {"
@@ -140,6 +164,8 @@ public class TextAttributes {
+ "\n getEffectiveLetterSpacing(): " + getEffectiveLetterSpacing()
+ "\n getLineHeight(): " + getLineHeight()
+ "\n getEffectiveLineHeight(): " + getEffectiveLineHeight()
+ "\n getMaxFontSizeMultiplier(): " + getMaxFontSizeMultiplier()
+ "\n getEffectiveMaxFontSizeMultiplier(): " + getEffectiveMaxFontSizeMultiplier()
+ "\n}"
);
}
@@ -647,6 +647,13 @@ public class ReactEditText extends EditText {
applyTextAttributes();
}
public void setMaxFontSizeMultiplier(float maxFontSizeMultiplier) {
if (maxFontSizeMultiplier != mTextAttributes.getMaxFontSizeMultiplier()) {
mTextAttributes.setMaxFontSizeMultiplier(maxFontSizeMultiplier);
applyTextAttributes();
}
}
protected void applyTextAttributes() {
// In general, the `getEffective*` functions return `Float.NaN` if the
// property hasn't been set.
@@ -217,6 +217,11 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
view.setTypeface(newTypeface);
}
@ReactProp(name = ViewProps.MAX_FONT_SIZE_MULTIPLIER, defaultFloat = Float.NaN)
public void setMaxFontSizeMultiplier(ReactEditText view, float maxFontSizeMultiplier) {
view.setMaxFontSizeMultiplier(maxFontSizeMultiplier);
}
/**
/* This code was taken from the method setFontWeight of the class ReactTextShadowNode
/* TODO: Factor into a common place they can both use