From 0f6f264623f4eb63a96f344329b91391b0eccc19 Mon Sep 17 00:00:00 2001 From: Adam Comella Date: Wed, 23 Jan 2019 11:29:34 -0800 Subject: [PATCH] Android: Add a maxFontSizeMultiplier prop to and (#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 `` and `` 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 --- .../facebook/react/uimanager/PixelUtil.java | 20 ++++++++--- .../facebook/react/uimanager/ViewProps.java | 1 + .../views/text/ReactBaseTextShadowNode.java | 8 +++++ .../react/views/text/TextAttributes.java | 34 ++++++++++++++++--- .../react/views/textinput/ReactEditText.java | 7 ++++ .../textinput/ReactTextInputManager.java | 5 +++ 6 files changed, 67 insertions(+), 8 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java index ad195d14d85b5f..c5765bc24489d5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java @@ -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; } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java index 7b38c54997fed8..377e902863a8ef 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java @@ -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"; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index dd1db9681ec675..8e709caa7c466e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -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)) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java index a9f35e1496effe..4e1c37094363db 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java @@ -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,7 +138,7 @@ public float getEffectiveLetterSpacing() { } 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 float getEffectiveLetterSpacing() { 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}" ); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 05886aae2b62f8..e8d24d7ea73aa4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -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. diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index 26a38d6d0bc265..363511025c190d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -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