Skip to content

Commit

Permalink
RN: Unify Typeface Logic (Android)
Browse files Browse the repository at this point in the history
Summary:
Refactors how `Typeface` style and weight are applied in React Native on Android.

- Unifies all style and weight normalization logic into a new `TypefaceStyle` class.
  - Fixes font weight support for the Fabric renderer.
  - De-duplicates code with `TextAttributeProps`.
  - Simplified normalization logic.
- Fixes a rare crash due to `Typeface.sDefaultTypeface` (Android SDK) being `null`.
- Adds a new example to test font weights in `TextInput`.
- Adds missing `Nullsafe` and `Nullable` annotations.
- Clean up a bunch of obsolete inline comments.

Changelog:
[Android][Fixed] - Fixed a rare crash due to `Typeface.sDefaultTypeface` (Android SDK) being `null`.
[Android][Fixed] - Fixed font weight support for the Fabric renderer.
[Android][Added] - Added a new example to test font weights in `TextInput`.

Reviewed By: JoshuaGross

Differential Revision: D29631134

fbshipit-source-id: 3f227d84253104fa828a5561b77ba7a9cbc030c4
  • Loading branch information
yungsters authored and facebook-github-bot committed Jul 13, 2021
1 parent 3e2bb33 commit 9d2fedc
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
import android.graphics.Typeface;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Nullsafe;

@Nullsafe(Nullsafe.Mode.LOCAL)
public class CustomStyleSpan extends MetricAffectingSpan implements ReactSpan {

/**
Expand All @@ -40,7 +41,7 @@ public CustomStyleSpan(
int fontWeight,
@Nullable String fontFeatureSettings,
@Nullable String fontFamily,
@NonNull AssetManager assetManager) {
AssetManager assetManager) {
mStyle = fontStyle;
mWeight = fontWeight;
mFeatureSettings = fontFeatureSettings;
Expand All @@ -54,21 +55,18 @@ public void updateDrawState(TextPaint ds) {
}

@Override
public void updateMeasureState(@NonNull TextPaint paint) {
public void updateMeasureState(TextPaint paint) {
apply(paint, mStyle, mWeight, mFeatureSettings, mFontFamily, mAssetManager);
}

/** Returns {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. */
public int getStyle() {
return (mStyle == ReactTextShadowNode.UNSET ? 0 : mStyle);
return mStyle == ReactBaseTextShadowNode.UNSET ? Typeface.NORMAL : mStyle;
}

/** Returns {@link Typeface#NORMAL} or {@link Typeface#BOLD}. */
public int getWeight() {
return (mWeight == ReactTextShadowNode.UNSET ? 0 : mWeight);
return mWeight == ReactBaseTextShadowNode.UNSET ? TypefaceStyle.NORMAL : mWeight;
}

/** Returns the font family set for this StyleSpan. */
public @Nullable String getFontFamily() {
return mFontFamily;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,39 @@
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Typeface;
import android.os.Build;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
import com.facebook.infer.annotation.Nullsafe;
import java.util.HashMap;
import java.util.Map;

/**
* Class responsible to load and cache Typeface objects. It will first try to load typefaces inside
* the assets/fonts folder and if it doesn't find the right Typeface in that folder will fall back
* on the best matching system Typeface The supported custom fonts extensions are .ttf and .otf. For
* each font family the bold, italic and bold_italic variants are supported. Given a "family" font
* family the files in the assets/fonts folder need to be family.ttf(.otf) family_bold.ttf(.otf)
* family_italic.ttf(.otf) and family_bold_italic.ttf(.otf)
* Responsible for loading and caching Typeface objects.
*
* <p>This will first try to load a typeface from the assets/fonts folder. If one is not found in
* that folder, this will fallback to the best matching system typeface.
*
* <p>Custom fonts support the extensions `.ttf` and `.otf` and the variants `bold`, `italic`, and
* `bold_italic`. For example, given a font named "ExampleFontFamily", the following are supported:
*
* <ul>
* <li>ExampleFontFamily.ttf (or .otf)
* <li>ExampleFontFamily_bold.ttf (or .otf)
* <li>ExampleFontFamily_italic.ttf (or .otf)
* <li>ExampleFontFamily_bold_italic.ttf (or .otf)
*/
@Nullsafe(Nullsafe.Mode.LOCAL)
public class ReactFontManager {

// NOTE: Indices in `EXTENSIONS` correspond to the `TypeFace` style constants.
private static final String[] EXTENSIONS = {"", "_bold", "_italic", "_bold_italic"};
private static final String[] FILE_EXTENSIONS = {".ttf", ".otf"};
private static final String FONTS_ASSET_PATH = "fonts/";

private static ReactFontManager sReactFontManagerInstance;

private final Map<String, FontFamily> mFontCache;
private final Map<String, AssetFontFamily> mFontCache;
private final Map<String, Typeface> mCustomTypefaceCache;

private ReactFontManager() {
Expand All @@ -49,36 +57,43 @@ public static ReactFontManager getInstance() {
return sReactFontManagerInstance;
}

public @Nullable Typeface getTypeface(
String fontFamilyName, int style, AssetManager assetManager) {
return getTypeface(fontFamilyName, style, 0, assetManager);
public Typeface getTypeface(String fontFamilyName, int style, AssetManager assetManager) {
return getTypeface(fontFamilyName, new TypefaceStyle(style), assetManager);
}

public @Nullable Typeface getTypeface(
public Typeface getTypeface(
String fontFamilyName, int weight, boolean italic, AssetManager assetManager) {
return getTypeface(fontFamilyName, new TypefaceStyle(weight, italic), assetManager);
}

public Typeface getTypeface(
String fontFamilyName, int style, int weight, AssetManager assetManager) {
return getTypeface(fontFamilyName, new TypefaceStyle(style, weight), assetManager);
}

public Typeface getTypeface(
String fontFamilyName, TypefaceStyle typefaceStyle, AssetManager assetManager) {
if (mCustomTypefaceCache.containsKey(fontFamilyName)) {
Typeface typeface = mCustomTypefaceCache.get(fontFamilyName);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && weight >= 100 && weight <= 1000) {
return Typeface.create(typeface, weight, (style & Typeface.ITALIC) != 0);
}
return Typeface.create(typeface, style);
// Apply `typefaceStyle` because custom fonts configure variants using `app:fontStyle` and
// `app:fontWeight` in their resource XML configuration file.
return typefaceStyle.apply(mCustomTypefaceCache.get(fontFamilyName));
}

FontFamily fontFamily = mFontCache.get(fontFamilyName);
if (fontFamily == null) {
fontFamily = new FontFamily();
mFontCache.put(fontFamilyName, fontFamily);
AssetFontFamily assetFontFamily = mFontCache.get(fontFamilyName);
if (assetFontFamily == null) {
assetFontFamily = new AssetFontFamily();
mFontCache.put(fontFamilyName, assetFontFamily);
}

Typeface typeface = fontFamily.getTypeface(style);
if (typeface == null) {
typeface = createTypeface(fontFamilyName, style, assetManager);
if (typeface != null) {
fontFamily.setTypeface(style, typeface);
}
}
int style = typefaceStyle.getNearestStyle();

return typeface;
Typeface assetTypeface = assetFontFamily.getTypefaceForStyle(style);
if (assetTypeface == null) {
assetTypeface = createAssetTypeface(fontFamilyName, style, assetManager);
assetFontFamily.setTypefaceForStyle(style, assetTypeface);
}
// Do not apply `typefaceStyle` because asset font files already incorporate the style.
return assetTypeface;
}

/*
Expand All @@ -88,7 +103,7 @@ public static ReactFontManager getInstance() {
*
* ReactFontManager.getInstance().addCustomFont(this, "Srisakdi", R.font.srisakdi);
*/
public void addCustomFont(@NonNull Context context, @NonNull String fontFamily, int fontId) {
public void addCustomFont(Context context, String fontFamily, int fontId) {
Typeface font = ResourcesCompat.getFont(context, fontId);
if (font != null) {
mCustomTypefaceCache.put(fontFamily, font);
Expand All @@ -106,16 +121,16 @@ public void addCustomFont(@NonNull Context context, @NonNull String fontFamily,
*/
public void setTypeface(String fontFamilyName, int style, Typeface typeface) {
if (typeface != null) {
FontFamily fontFamily = mFontCache.get(fontFamilyName);
if (fontFamily == null) {
fontFamily = new FontFamily();
mFontCache.put(fontFamilyName, fontFamily);
AssetFontFamily assetFontFamily = mFontCache.get(fontFamilyName);
if (assetFontFamily == null) {
assetFontFamily = new AssetFontFamily();
mFontCache.put(fontFamilyName, assetFontFamily);
}
fontFamily.setTypeface(style, typeface);
assetFontFamily.setTypefaceForStyle(style, typeface);
}
}

private static @Nullable Typeface createTypeface(
private static Typeface createAssetTypeface(
String fontFamilyName, int style, AssetManager assetManager) {
String extension = EXTENSIONS[style];
for (String fileExtension : FILE_EXTENSIONS) {
Expand All @@ -129,27 +144,27 @@ public void setTypeface(String fontFamilyName, int style, Typeface typeface) {
try {
return Typeface.createFromAsset(assetManager, fileName);
} catch (RuntimeException e) {
// unfortunately Typeface.createFromAsset throws an exception instead of returning null
// if the typeface doesn't exist
// If the typeface asset does not exist, try another extension.
continue;
}
}

return Typeface.create(fontFamilyName, style);
}

private static class FontFamily {
/** Responsible for caching typefaces for each custom font family. */
private static class AssetFontFamily {

private SparseArray<Typeface> mTypefaceSparseArray;

private FontFamily() {
private AssetFontFamily() {
mTypefaceSparseArray = new SparseArray<>(4);
}

public Typeface getTypeface(int style) {
public @Nullable Typeface getTypefaceForStyle(int style) {
return mTypefaceSparseArray.get(style);
}

public void setTypeface(int style, Typeface typeface) {
public void setTypefaceForStyle(int style, Typeface typeface) {
mTypefaceSparseArray.put(style, typeface);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,54 @@

import android.content.res.AssetManager;
import android.graphics.Typeface;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.ReadableArray;
import java.util.ArrayList;
import java.util.List;

@Nullsafe(Nullsafe.Mode.LOCAL)
public class ReactTypefaceUtils {
private static final String TAG = "ReactTypefaceUtils";
public static final int UNSET = -1;

public static int parseFontWeight(@Nullable String fontWeightString) {
int fontWeightNumeric =
fontWeightString != null ? parseNumericFontWeight(fontWeightString) : UNSET;
int fontWeight = fontWeightNumeric != UNSET ? fontWeightNumeric : Typeface.NORMAL;

if ("bold".equals(fontWeightString)) fontWeight = Typeface.BOLD;
else if ("normal".equals(fontWeightString)) fontWeight = Typeface.NORMAL;

return fontWeight;
if (fontWeightString != null) {
switch (fontWeightString) {
case "100":
return 100;
case "200":
return 200;
case "300":
return 300;
case "normal":
case "400":
return 400;
case "500":
return 500;
case "600":
return 600;
case "bold":
case "700":
return 700;
case "800":
return 800;
case "900":
return 900;
}
}
return ReactBaseTextShadowNode.UNSET;
}

public static int parseFontStyle(@Nullable String fontStyleString) {
int fontStyle = UNSET;
if ("italic".equals(fontStyleString)) {
fontStyle = Typeface.ITALIC;
} else if ("normal".equals(fontStyleString)) {
fontStyle = Typeface.NORMAL;
if (fontStyleString != null) {
if ("italic".equals(fontStyleString)) {
return Typeface.ITALIC;
}
if ("normal".equals(fontStyleString)) {
return Typeface.NORMAL;
}
}

return fontStyle;
return ReactBaseTextShadowNode.UNSET;
}

public static @Nullable String parseFontVariant(@Nullable ReadableArray fontVariantArray) {
Expand Down Expand Up @@ -80,67 +96,14 @@ public static Typeface applyStyles(
@Nullable Typeface typeface,
int style,
int weight,
@Nullable String family,
@Nullable String fontFamilyName,
AssetManager assetManager) {
int oldStyle;
if (typeface == null) {
oldStyle = Typeface.NORMAL;
TypefaceStyle typefaceStyle = new TypefaceStyle(style, weight);
if (fontFamilyName == null) {
return typefaceStyle.apply(typeface == null ? Typeface.DEFAULT : typeface);
} else {
oldStyle = typeface.getStyle();
}

int newStyle = oldStyle;
boolean italic = false;
if (weight == UNSET) weight = Typeface.NORMAL;
if (style == Typeface.ITALIC) italic = true;
boolean UNDER_SDK_28 = Build.VERSION.SDK_INT < Build.VERSION_CODES.P;
boolean applyNumericValues = !(weight < (Typeface.BOLD_ITALIC + 1) || family != null);
boolean numericBold = UNDER_SDK_28 && weight > 699 && applyNumericValues;
boolean numericNormal = UNDER_SDK_28 && weight < 700 && applyNumericValues;
if (weight == Typeface.BOLD) {
newStyle = (newStyle == Typeface.ITALIC) ? Typeface.BOLD_ITALIC : Typeface.BOLD;
typeface = Typeface.create(typeface, newStyle);
return ReactFontManager.getInstance()
.getTypeface(fontFamilyName, typefaceStyle, assetManager);
}
if (weight == Typeface.NORMAL) {
typeface = Typeface.create(typeface, Typeface.NORMAL);
newStyle = Typeface.NORMAL;
}
if (style == Typeface.ITALIC) {
newStyle = (newStyle == Typeface.BOLD) ? Typeface.BOLD_ITALIC : Typeface.ITALIC;
typeface = Typeface.create(typeface, newStyle);
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1 && weight > Typeface.BOLD_ITALIC) {
typeface = Typeface.create(typeface, weight, italic);
}
if (family != null && UNDER_SDK_28 && weight > Typeface.BOLD_ITALIC) {
FLog.d(
TAG,
"Support for numeric font weight numeric values with custom fonts under Android API 28 Pie is not yet supported in ReactNative.");
}
if (family != null) {
typeface = ReactFontManager.getInstance().getTypeface(family, newStyle, weight, assetManager);
}
if (numericBold || numericNormal) {
newStyle = numericBold ? Typeface.BOLD : Typeface.NORMAL;
typeface = Typeface.create(typeface, newStyle);
FLog.d(
TAG,
"Support for numeric font weight numeric values available only from Android API 28 Pie. Android device lower then API 28 will use normal or bold.");
}
return typeface;
}

/**
* Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise
* return the weight.
*/
private static int parseNumericFontWeight(String fontWeightString) {
// This should be much faster than using regex to verify input and Integer.parseInt
return fontWeightString.length() == 3
&& fontWeightString.endsWith("00")
&& fontWeightString.charAt(0) <= '9'
&& fontWeightString.charAt(0) >= '1'
? 100 * (fontWeightString.charAt(0) - '0')
: UNSET;
}
}
Loading

0 comments on commit 9d2fedc

Please sign in to comment.