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,