diff --git a/Examples/UIExplorer/TextExample.android.js b/Examples/UIExplorer/TextExample.android.js
index 42d17ed07..0275863a8 100644
--- a/Examples/UIExplorer/TextExample.android.js
+++ b/Examples/UIExplorer/TextExample.android.js
@@ -160,6 +160,22 @@ var TextExample = React.createClass({
+
+
+
+
+ NotoSerif Regular
+
+
+ NotoSerif Bold Italic
+
+
+ NotoSerif Italic (Missing Font file)
+
+
+
+
+
Size 23
diff --git a/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif.ttf b/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif.ttf
new file mode 100755
index 000000000..a1c6f1059
Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif.ttf differ
diff --git a/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif_bold_italic.ttf b/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif_bold_italic.ttf
new file mode 100755
index 000000000..32d38afee
Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif_bold_italic.ttf differ
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java
index eac5ed6db..07a303e22 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java
@@ -14,6 +14,7 @@ import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
+import android.content.res.AssetManager;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.TextPaint;
@@ -21,33 +22,43 @@ import android.text.style.MetricAffectingSpan;
public class CustomStyleSpan extends MetricAffectingSpan {
- // Typeface caching is a bit weird: once a Typeface is created, it cannot be changed, so we need
- // to cache each font family and each style that they have. Typeface does cache this already in
- // Typeface.create(Typeface, style) post API 16, but for that you already need a Typeface.
- // Therefore, here we cache one style for each font family, and let Typeface cache all styles for
- // that font family. Of course this is not ideal, and especially after adding Typeface loading
- // from assets, we will need to have our own caching mechanism for all Typeface creation types.
- // TODO: t6866343 add better Typeface caching
- private static final Map sTypefaceCache = new HashMap();
+ /**
+ * A {@link MetricAffectingSpan} that allows to change the style of the displayed font.
+ * CustomStyleSpan will try to load the fontFamily with the right style and weight from the
+ * assets. The custom fonts will have to be located in the res/assets folder of the application.
+ * 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). If the right font is not found in the assets folder
+ * CustomStyleSpan will fallback on the most appropriate default typeface depending on the style.
+ * Fonts are retrieved and cached using the {@link ReactFontManager}
+ */
+
+ private final AssetManager mAssetManager;
private final int mStyle;
private final int mWeight;
private final @Nullable String mFontFamily;
- public CustomStyleSpan(int fontStyle, int fontWeight, @Nullable String fontFamily) {
+ public CustomStyleSpan(
+ int fontStyle,
+ int fontWeight,
+ @Nullable String fontFamily,
+ AssetManager assetManager) {
mStyle = fontStyle;
mWeight = fontWeight;
mFontFamily = fontFamily;
+ mAssetManager = assetManager;
}
@Override
public void updateDrawState(TextPaint ds) {
- apply(ds, mStyle, mWeight, mFontFamily);
+ apply(ds, mStyle, mWeight, mFontFamily, mAssetManager);
}
@Override
public void updateMeasureState(TextPaint paint) {
- apply(paint, mStyle, mWeight, mFontFamily);
+ apply(paint, mStyle, mWeight, mFontFamily, mAssetManager);
}
/**
@@ -61,7 +72,7 @@ public class CustomStyleSpan extends MetricAffectingSpan {
* Returns {@link Typeface#NORMAL} or {@link Typeface#BOLD}.
*/
public int getWeight() {
- return (mWeight == ReactTextShadowNode.UNSET ? 0 : mWeight);
+ return (mWeight == ReactTextShadowNode.UNSET ? 0 : mWeight);
}
/**
@@ -71,7 +82,12 @@ public class CustomStyleSpan extends MetricAffectingSpan {
return mFontFamily;
}
- private static void apply(Paint paint, int style, int weight, @Nullable String family) {
+ private static void apply(
+ Paint paint,
+ int style,
+ int weight,
+ @Nullable String family,
+ AssetManager assetManager) {
int oldStyle;
Typeface typeface = paint.getTypeface();
if (typeface == null) {
@@ -92,23 +108,14 @@ public class CustomStyleSpan extends MetricAffectingSpan {
}
if (family != null) {
- typeface = getOrCreateTypeface(family, want);
+ typeface = ReactFontManager.getInstance().getTypeface(family, want, assetManager);
}
if (typeface != null) {
- paint.setTypeface(Typeface.create(typeface, want));
+ paint.setTypeface(typeface);
} else {
paint.setTypeface(Typeface.defaultFromStyle(want));
}
}
- private static Typeface getOrCreateTypeface(String family, int style) {
- if (sTypefaceCache.get(family) != null) {
- return sTypefaceCache.get(family);
- }
-
- Typeface typeface = Typeface.create(family, style);
- sTypefaceCache.put(family, typeface);
- return typeface;
- }
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactFontManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactFontManager.java
new file mode 100644
index 000000000..10e2a4d7b
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactFontManager.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc. All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the LICENSE file in the root
+ * directory of this source tree. An additional grant of patent rights can be found in the PATENTS
+ * file in the same directory.
+ */
+
+package com.facebook.react.views.text;
+
+import javax.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import android.content.res.AssetManager;
+import android.graphics.Typeface;
+import android.util.SparseArray;
+
+/**
+ * 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)
+ */
+public class ReactFontManager {
+
+ 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 Map mFontCache;
+
+ private ReactFontManager() {
+ mFontCache = new HashMap<>();
+ }
+
+ public static ReactFontManager getInstance() {
+ if (sReactFontManagerInstance == null) {
+ sReactFontManagerInstance = new ReactFontManager();
+ }
+ return sReactFontManagerInstance;
+ }
+
+ public
+ @Nullable Typeface getTypeface(
+ String fontFamilyName,
+ int style,
+ AssetManager assetManager) {
+ FontFamily fontFamily = mFontCache.get(fontFamilyName);
+ if (fontFamily == null) {
+ fontFamily = new FontFamily();
+ mFontCache.put(fontFamilyName, fontFamily);
+ }
+
+ Typeface typeface = fontFamily.getTypeface(style);
+ if (typeface == null) {
+ typeface = createTypeface(fontFamilyName, style, assetManager);
+ if (typeface != null) {
+ fontFamily.setTypeface(style, typeface);
+ }
+ }
+
+ return typeface;
+ }
+
+ private static
+ @Nullable Typeface createTypeface(
+ 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) {
+ // unfortunately Typeface.createFromAsset throws an exception instead of returning null
+ // if the typeface doesn't exist
+ }
+ }
+
+ return Typeface.create(fontFamilyName, style);
+ }
+
+ private static class FontFamily {
+
+ private SparseArray mTypefaceSparseArray;
+
+ private FontFamily() {
+ mTypefaceSparseArray = new SparseArray<>(4);
+ }
+
+ public Typeface getTypeface(int style) {
+ return mTypefaceSparseArray.get(style);
+ }
+
+ public void setTypeface(int style, Typeface typeface) {
+ mTypefaceSparseArray.put(style, typeface);
+ }
+
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java
index 4cad9dc65..9d9d74e8b 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java
@@ -43,16 +43,16 @@ import com.facebook.react.uimanager.ViewProps;
/**
* {@link ReactShadowNode} class for spannable text view.
- *
+ *
* This node calculates {@link Spannable} based on subnodes of the same type and passes the
- * resulting object down to textview's shadowview and actual native {@link TextView} instance.
- * It is important to keep in mind that {@link Spannable} is calculated only on layout step, so if
- * there are any text properties that may/should affect the result of {@link Spannable} they should
- * be set in a corresponding {@link ReactTextShadowNode}. Resulting {@link Spannable} object is then
- * then passed as "computedDataFromMeasure" down to shadow and native view.
- *
- * TODO(7255858): Rename *CSSNode to *ShadowView (or sth similar) as it's no longer is used
- * solely for layouting
+ * resulting object down to textview's shadowview and actual native {@link TextView} instance. It is
+ * important to keep in mind that {@link Spannable} is calculated only on layout step, so if there
+ * are any text properties that may/should affect the result of {@link Spannable} they should be set
+ * in a corresponding {@link ReactTextShadowNode}. Resulting {@link Spannable} object is then then
+ * passed as "computedDataFromMeasure" down to shadow and native view.
+ *
+ * TODO(7255858): Rename *CSSNode to *ShadowView (or sth similar) as it's no longer is used solely
+ * for layouting
*/
public class ReactTextShadowNode extends LayoutShadowNode {
@@ -100,7 +100,7 @@ public class ReactTextShadowNode extends LayoutShadowNode {
buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops);
} else {
throw new IllegalViewOperationException("Unexpected view type nested under text node: "
- + child.getClass());
+ + child.getClass());
}
((ReactTextShadowNode) child).markUpdateSeen();
}
@@ -128,7 +128,8 @@ public class ReactTextShadowNode extends LayoutShadowNode {
new CustomStyleSpan(
textCSSNode.mFontStyle,
textCSSNode.mFontWeight,
- textCSSNode.mFontFamily)));
+ textCSSNode.mFontFamily,
+ textCSSNode.getThemedContext().getAssets())));
}
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textCSSNode.getReactTag())));
}
@@ -197,7 +198,7 @@ public class ReactTextShadowNode extends LayoutShadowNode {
0,
boring,
true);
- } else {
+ } else {
// Is used for multiline, boring text and the width is known.
layout = new StaticLayout(
text,