Android: expose ReactFontManager as a standalone target (#38506)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/38506

There are use cases where an app needs to install a custom font in a separate infra/tool. Instead of bloating those infra/tool with deep React Native dependencies, this allows it to depend on only the pieces that matter.

To ensure backward compatibility, the existing ReactFontManager will proxy method calls to the new impl going forward.

Changelog: [Changed][Android] Moved ReactFontManager to a common package

Reviewed By: mdvacca

Differential Revision: D47569319

fbshipit-source-id: 18ba6617d6c3f68823bdccaacfd8cc961cd6ce34
This commit is contained in:
Kevin Gozali
2023-07-18 23:17:48 -07:00
committed by Facebook GitHub Bot
parent cb173e6a6f
commit 7341f9abdc
7 changed files with 266 additions and 208 deletions
@@ -0,0 +1,239 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.common.assets;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Typeface;
import android.os.Build;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
import com.facebook.infer.annotation.Nullsafe;
import java.util.HashMap;
import java.util.Map;
/**
* 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, AssetFontFamily> mFontCache;
private final Map<String, Typeface> mCustomTypefaceCache;
private ReactFontManager() {
mFontCache = new HashMap<>();
mCustomTypefaceCache = new HashMap<>();
}
public static ReactFontManager getInstance() {
if (sReactFontManagerInstance == null) {
sReactFontManagerInstance = new ReactFontManager();
}
return sReactFontManagerInstance;
}
public Typeface getTypeface(String fontFamilyName, int style, AssetManager assetManager) {
return getTypeface(fontFamilyName, new TypefaceStyle(style), assetManager);
}
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)) {
// 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));
}
AssetFontFamily assetFontFamily = mFontCache.get(fontFamilyName);
if (assetFontFamily == null) {
assetFontFamily = new AssetFontFamily();
mFontCache.put(fontFamilyName, assetFontFamily);
}
int style = typefaceStyle.getNearestStyle();
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;
}
/*
* This method allows you to load custom fonts from res/font folder as provided font family name.
* Fonts may be one of .ttf, .otf or XML (https://developer.android.com/guide/topics/ui/look-and-feel/fonts-in-xml).
* To support multiple font styles or weights, you must provide a font in XML format.
*
* ReactFontManager.getInstance().addCustomFont(this, "Srisakdi", R.font.srisakdi);
*/
public void addCustomFont(Context context, String fontFamily, int fontId) {
Typeface font = ResourcesCompat.getFont(context, fontId);
if (font != null) {
mCustomTypefaceCache.put(fontFamily, font);
}
}
/**
* Equivalent method to {@see addCustomFont(Context, String, int)} which accepts a Typeface
* object.
*/
public void addCustomFont(String fontFamily, @Nullable Typeface font) {
if (font != null) {
mCustomTypefaceCache.put(fontFamily, font);
}
}
/**
* Add additional font family, or replace the exist one in the font memory cache.
*
* @param style
* @see {@link Typeface#DEFAULT}
* @see {@link Typeface#BOLD}
* @see {@link Typeface#ITALIC}
* @see {@link Typeface#BOLD_ITALIC}
*/
public void setTypeface(String fontFamilyName, int style, Typeface typeface) {
if (typeface != null) {
AssetFontFamily assetFontFamily = mFontCache.get(fontFamilyName);
if (assetFontFamily == null) {
assetFontFamily = new AssetFontFamily();
mFontCache.put(fontFamilyName, assetFontFamily);
}
assetFontFamily.setTypefaceForStyle(style, typeface);
}
}
private static Typeface createAssetTypeface(
String fontFamilyName, int style, AssetManager assetManager) {
String extension = EXTENSIONS[style];
for (String fileExtension : FILE_EXTENSIONS) {
String fileName =
new StringBuilder()
.append(FONTS_ASSET_PATH)
.append(fontFamilyName)
.append(extension)
.append(fileExtension)
.toString();
try {
return Typeface.createFromAsset(assetManager, fileName);
} catch (RuntimeException e) {
// If the typeface asset does not exist, try another extension.
continue;
}
}
return Typeface.create(fontFamilyName, style);
}
/** Responsible for normalizing style and numeric weight for backward compatibility. */
public static class TypefaceStyle {
public static final int BOLD = 700;
public static final int NORMAL = 400;
public static final int UNSET = -1;
private static final int MIN_WEIGHT = 1;
private static final int MAX_WEIGHT = 1000;
private final boolean mItalic;
private final int mWeight;
public TypefaceStyle(int weight, boolean italic) {
mItalic = italic;
mWeight = weight == UNSET ? NORMAL : weight;
}
public TypefaceStyle(int style) {
if (style == UNSET) {
style = Typeface.NORMAL;
}
mItalic = (style & Typeface.ITALIC) != 0;
mWeight = (style & Typeface.BOLD) != 0 ? BOLD : NORMAL;
}
/**
* If `weight` is supplied, it will be combined with the italic bit from `style`. Otherwise, any
* existing weight bit in `style` will be used.
*/
public TypefaceStyle(int style, int weight) {
if (style == UNSET) {
style = Typeface.NORMAL;
}
mItalic = (style & Typeface.ITALIC) != 0;
mWeight = weight == UNSET ? (style & Typeface.BOLD) != 0 ? BOLD : NORMAL : weight;
}
public int getNearestStyle() {
if (mWeight < BOLD) {
return mItalic ? Typeface.ITALIC : Typeface.NORMAL;
} else {
return mItalic ? Typeface.BOLD_ITALIC : Typeface.BOLD;
}
}
public Typeface apply(Typeface typeface) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return Typeface.create(typeface, getNearestStyle());
} else {
return Typeface.create(typeface, mWeight, mItalic);
}
}
}
/** Responsible for caching typefaces for each custom font family. */
private static class AssetFontFamily {
private SparseArray<Typeface> mTypefaceSparseArray;
private AssetFontFamily() {
mTypefaceSparseArray = new SparseArray<>(4);
}
public @Nullable Typeface getTypefaceForStyle(int style) {
return mTypefaceSparseArray.get(style);
}
public void setTypefaceForStyle(int style, Typeface typeface) {
mTypefaceSparseArray.put(style, typeface);
}
}
}
@@ -14,6 +14,7 @@ import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.common.assets.ReactFontManager;
@Nullsafe(Nullsafe.Mode.LOCAL)
public class CustomStyleSpan extends MetricAffectingSpan implements ReactSpan {
@@ -60,11 +61,13 @@ public class CustomStyleSpan extends MetricAffectingSpan implements ReactSpan {
}
public int getStyle() {
return mStyle == ReactBaseTextShadowNode.UNSET ? Typeface.NORMAL : mStyle;
return mStyle == ReactFontManager.TypefaceStyle.UNSET ? Typeface.NORMAL : mStyle;
}
public int getWeight() {
return mWeight == ReactBaseTextShadowNode.UNSET ? TypefaceStyle.NORMAL : mWeight;
return mWeight == ReactFontManager.TypefaceStyle.UNSET
? ReactFontManager.TypefaceStyle.NORMAL
: mWeight;
}
public @Nullable String getFontFamily() {
@@ -22,6 +22,7 @@ import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.assets.ReactFontManager;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
@@ -56,7 +57,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
// character.
// https://en.wikipedia.org/wiki/Bi-directional_text#weak_characters
private static final String INLINE_VIEW_PLACEHOLDER = "0";
public static final int UNSET = -1;
public static final int UNSET = ReactFontManager.TypefaceStyle.UNSET;
public static final String PROP_SHADOW_OFFSET = "textShadowOffset";
public static final String PROP_SHADOW_OFFSET_WIDTH = "width";
@@ -10,172 +10,57 @@ package com.facebook.react.views.text;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Typeface;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
import com.facebook.infer.annotation.Nullsafe;
import java.util.HashMap;
import java.util.Map;
/**
* 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)
* @deprecated This class is deprecated and it will be deleted in the near future. Please use {@link
* com.facebook.react.common.assets.ReactFontManager} instead.
*/
@Nullsafe(Nullsafe.Mode.LOCAL)
@Deprecated
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 com.facebook.react.common.assets.ReactFontManager mDelegate;
private final Map<String, AssetFontFamily> mFontCache;
private final Map<String, Typeface> mCustomTypefaceCache;
private ReactFontManager() {
mFontCache = new HashMap<>();
mCustomTypefaceCache = new HashMap<>();
private ReactFontManager(com.facebook.react.common.assets.ReactFontManager delegate) {
mDelegate = delegate;
}
public static ReactFontManager getInstance() {
if (sReactFontManagerInstance == null) {
sReactFontManagerInstance = new ReactFontManager();
sReactFontManagerInstance =
new ReactFontManager(com.facebook.react.common.assets.ReactFontManager.getInstance());
}
return sReactFontManagerInstance;
}
public Typeface getTypeface(String fontFamilyName, int style, AssetManager assetManager) {
return getTypeface(fontFamilyName, new TypefaceStyle(style), assetManager);
return mDelegate.getTypeface(fontFamilyName, style, assetManager);
}
public Typeface getTypeface(
String fontFamilyName, int weight, boolean italic, AssetManager assetManager) {
return getTypeface(fontFamilyName, new TypefaceStyle(weight, italic), assetManager);
return mDelegate.getTypeface(fontFamilyName, weight, italic, assetManager);
}
public Typeface getTypeface(
String fontFamilyName, int style, int weight, AssetManager assetManager) {
return getTypeface(fontFamilyName, new TypefaceStyle(style, weight), assetManager);
return mDelegate.getTypeface(fontFamilyName, style, weight, assetManager);
}
public Typeface getTypeface(
String fontFamilyName, TypefaceStyle typefaceStyle, AssetManager assetManager) {
if (mCustomTypefaceCache.containsKey(fontFamilyName)) {
// 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));
}
AssetFontFamily assetFontFamily = mFontCache.get(fontFamilyName);
if (assetFontFamily == null) {
assetFontFamily = new AssetFontFamily();
mFontCache.put(fontFamilyName, assetFontFamily);
}
int style = typefaceStyle.getNearestStyle();
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;
}
/*
* This method allows you to load custom fonts from res/font folder as provided font family name.
* Fonts may be one of .ttf, .otf or XML (https://developer.android.com/guide/topics/ui/look-and-feel/fonts-in-xml).
* To support multiple font styles or weights, you must provide a font in XML format.
*
* ReactFontManager.getInstance().addCustomFont(this, "Srisakdi", R.font.srisakdi);
*/
public void addCustomFont(Context context, String fontFamily, int fontId) {
Typeface font = ResourcesCompat.getFont(context, fontId);
if (font != null) {
mCustomTypefaceCache.put(fontFamily, font);
}
mDelegate.addCustomFont(context, fontFamily, fontId);
}
/**
* Equivalent method to {@see addCustomFont(Context, String, int)} which accepts a Typeface
* object.
*/
public void addCustomFont(String fontFamily, @Nullable Typeface font) {
if (font != null) {
mCustomTypefaceCache.put(fontFamily, font);
}
mDelegate.addCustomFont(fontFamily, font);
}
/**
* Add additional font family, or replace the exist one in the font memory cache.
*
* @param style
* @see {@link Typeface#DEFAULT}
* @see {@link Typeface#BOLD}
* @see {@link Typeface#ITALIC}
* @see {@link Typeface#BOLD_ITALIC}
*/
public void setTypeface(String fontFamilyName, int style, Typeface typeface) {
if (typeface != null) {
AssetFontFamily assetFontFamily = mFontCache.get(fontFamilyName);
if (assetFontFamily == null) {
assetFontFamily = new AssetFontFamily();
mFontCache.put(fontFamilyName, assetFontFamily);
}
assetFontFamily.setTypefaceForStyle(style, typeface);
}
}
private static Typeface createAssetTypeface(
String fontFamilyName, int style, AssetManager assetManager) {
String extension = EXTENSIONS[style];
for (String fileExtension : FILE_EXTENSIONS) {
String fileName =
new StringBuilder()
.append(FONTS_ASSET_PATH)
.append(fontFamilyName)
.append(extension)
.append(fileExtension)
.toString();
try {
return Typeface.createFromAsset(assetManager, fileName);
} catch (RuntimeException e) {
// If the typeface asset does not exist, try another extension.
continue;
}
}
return Typeface.create(fontFamilyName, style);
}
/** Responsible for caching typefaces for each custom font family. */
private static class AssetFontFamily {
private SparseArray<Typeface> mTypefaceSparseArray;
private AssetFontFamily() {
mTypefaceSparseArray = new SparseArray<>(4);
}
public @Nullable Typeface getTypefaceForStyle(int style) {
return mTypefaceSparseArray.get(style);
}
public void setTypefaceForStyle(int style, Typeface typeface) {
mTypefaceSparseArray.put(style, typeface);
}
mDelegate.setTypeface(fontFamilyName, style, typeface);
}
}
@@ -13,6 +13,7 @@ import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.assets.ReactFontManager;
import java.util.ArrayList;
import java.util.List;
@@ -44,7 +45,7 @@ public class ReactTypefaceUtils {
return 900;
}
}
return ReactBaseTextShadowNode.UNSET;
return ReactFontManager.TypefaceStyle.UNSET;
}
public static int parseFontStyle(@Nullable String fontStyleString) {
@@ -56,7 +57,7 @@ public class ReactTypefaceUtils {
return Typeface.NORMAL;
}
}
return ReactBaseTextShadowNode.UNSET;
return ReactFontManager.TypefaceStyle.UNSET;
}
public static @Nullable String parseFontVariant(@Nullable ReadableArray fontVariantArray) {
@@ -184,7 +185,8 @@ public class ReactTypefaceUtils {
int weight,
@Nullable String fontFamilyName,
AssetManager assetManager) {
TypefaceStyle typefaceStyle = new TypefaceStyle(style, weight);
ReactFontManager.TypefaceStyle typefaceStyle =
new ReactFontManager.TypefaceStyle(style, weight);
if (fontFamilyName == null) {
return typefaceStyle.apply(typeface == null ? Typeface.DEFAULT : typeface);
} else {
@@ -1,72 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.views.text;
import android.graphics.Typeface;
import android.os.Build;
import com.facebook.infer.annotation.Nullsafe;
/** Responsible for normalizing style and numeric weight for backward compatibility. */
@Nullsafe(Nullsafe.Mode.LOCAL)
class TypefaceStyle {
public static final int BOLD = 700;
public static final int NORMAL = 400;
private static final int MIN_WEIGHT = 1;
private static final int MAX_WEIGHT = 1000;
private final boolean mItalic;
private final int mWeight;
public TypefaceStyle(int weight, boolean italic) {
mItalic = italic;
mWeight = weight == ReactBaseTextShadowNode.UNSET ? NORMAL : weight;
}
public TypefaceStyle(int style) {
if (style == ReactBaseTextShadowNode.UNSET) {
style = Typeface.NORMAL;
}
mItalic = (style & Typeface.ITALIC) != 0;
mWeight = (style & Typeface.BOLD) != 0 ? BOLD : NORMAL;
}
/**
* If `weight` is supplied, it will be combined with the italic bit from `style`. Otherwise, any
* existing weight bit in `style` will be used.
*/
public TypefaceStyle(int style, int weight) {
if (style == ReactBaseTextShadowNode.UNSET) {
style = Typeface.NORMAL;
}
mItalic = (style & Typeface.ITALIC) != 0;
mWeight =
weight == ReactBaseTextShadowNode.UNSET
? (style & Typeface.BOLD) != 0 ? BOLD : NORMAL
: weight;
}
public int getNearestStyle() {
if (mWeight < BOLD) {
return mItalic ? Typeface.ITALIC : Typeface.NORMAL;
} else {
return mItalic ? Typeface.BOLD_ITALIC : Typeface.BOLD;
}
}
public Typeface apply(Typeface typeface) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return Typeface.create(typeface, getNearestStyle());
} else {
return Typeface.create(typeface, mWeight, mItalic);
}
}
}
@@ -19,6 +19,7 @@ import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridgeless.ReactHost;
import com.facebook.react.common.annotations.UnstableReactNativeAPI;
import com.facebook.react.common.assets.ReactFontManager;
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.defaults.DefaultComponentsRegistry;
@@ -34,7 +35,6 @@ import com.facebook.react.shell.MainReactPackage;
import com.facebook.react.uiapp.component.MyLegacyViewManager;
import com.facebook.react.uiapp.component.MyNativeViewManager;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.views.text.ReactFontManager;
import com.facebook.soloader.SoLoader;
import java.util.ArrayList;
import java.util.Arrays;