Skip to content
Permalink
Browse files

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

…23069)

Summary:
Equivalent of this iOS PR: #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: #23069

Differential Revision: D13748513

Pulled By: mdvacca

fbshipit-source-id: 8dd5d6d97bf79387d9a2236fa2e586ccb01afde9
  • Loading branch information...
rigdern authored and facebook-github-bot committed Jan 23, 2019
1 parent 5bc709d commit 4936d284df36071047ce776d9e2486c0371f7b97
@@ -7,6 +7,7 @@

package com.facebook.react.uimanager;

import android.util.DisplayMetrics;
import android.util.TypedValue;

/**
@@ -42,10 +43,21 @@ public static float toSPFromPixel(float value) {
* 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 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 void setAllowFontScaling(boolean allowFontScaling) {
}
}

@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 @@
* 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 TextAttributes applyChild(TextAttributes child) {
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 void setLetterSpacing(float value) {
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 void setHeightOfTallestInlineImage(float value) {
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 float getEffectiveLineHeight() {
}

float lineHeight = mAllowFontScaling
? PixelUtil.toPixelFromSP(mLineHeight)
? PixelUtil.toPixelFromSP(mLineHeight, getEffectiveMaxFontSizeMultiplier())
: PixelUtil.toPixelFromDIP(mLineHeight);

// Take into account the requested line height
@@ -121,14 +138,21 @@ public float getEffectiveLetterSpacing() {
}

float letterSpacingPixels = mAllowFontScaling
? PixelUtil.toPixelFromSP(mLetterSpacing)
? PixelUtil.toPixelFromSP(mLetterSpacing, getEffectiveMaxFontSizeMultiplier())
: PixelUtil.toPixelFromDIP(mLetterSpacing);

// `letterSpacingPixels` and `getEffectiveFontSize` are both in pixels,
// yielding an accurate em value.
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 String toString() {
+ "\n getEffectiveLetterSpacing(): " + getEffectiveLetterSpacing()
+ "\n getLineHeight(): " + getLineHeight()
+ "\n getEffectiveLineHeight(): " + getEffectiveLineHeight()
+ "\n getMaxFontSizeMultiplier(): " + getMaxFontSizeMultiplier()
+ "\n getEffectiveMaxFontSizeMultiplier(): " + getEffectiveMaxFontSizeMultiplier()
+ "\n}"
);
}
@@ -647,6 +647,13 @@ public void setFontSize(float fontSize) {
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 void setFontFamily(ReactEditText view, String fontFamily) {
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

0 comments on commit 4936d28

Please sign in to comment.
You can’t perform that action at this time.