Skip to content

Commit

Permalink
Custom fonts support added
Browse files Browse the repository at this point in the history
Reviewed By: andreicoman11

Differential Revision: D2629077

fb-gh-sync-id: 8d647aff13f97d90c5047ad0ddbcae90215ca4ca
  • Loading branch information
pasqualeanatriello authored and facebook-github-bot-4 committed Nov 7, 2015
1 parent 1ae7a77 commit bfeaa6a
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 36 deletions.
16 changes: 16 additions & 0 deletions Examples/UIExplorer/TextExample.android.js
Expand Up @@ -160,6 +160,22 @@ var TextExample = React.createClass({
</View>
</View>
</UIExplorerBlock>
<UIExplorerBlock title="Custom Fonts">
<View style={{flexDirection: 'row', alignItems: 'flex-start'}}>
<View style={{flex: 1}}>
<Text style={{fontFamily: 'notoserif'}}>
NotoSerif Regular
</Text>
<Text style={{fontFamily: 'notoserif', fontStyle: 'italic', fontWeight: 'bold'}}>
NotoSerif Bold Italic
</Text>
<Text style={{fontFamily: 'notoserif', fontStyle: 'italic'}}>
NotoSerif Italic (Missing Font file)
</Text>
</View>
</View>
</UIExplorerBlock>

<UIExplorerBlock title="Font Size">
<Text style={{fontSize: 23}}>
Size 23
Expand Down
Binary file not shown.
Binary file not shown.
Expand Up @@ -14,40 +14,51 @@
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;
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<String, Typeface> sTypefaceCache = new HashMap<String, Typeface>();
/**
* 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);
}

/**
Expand All @@ -61,7 +72,7 @@ public int getStyle() {
* Returns {@link Typeface#NORMAL} or {@link Typeface#BOLD}.
*/
public int getWeight() {
return (mWeight == ReactTextShadowNode.UNSET ? 0 : mWeight);
return (mWeight == ReactTextShadowNode.UNSET ? 0 : mWeight);
}

/**
Expand All @@ -71,7 +82,12 @@ public int getWeight() {
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) {
Expand All @@ -92,23 +108,14 @@ private static void apply(Paint paint, int style, int weight, @Nullable String f
}

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;
}
}
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2015-present, Facebook, Inc. All rights reserved.
* <p/>
* 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<String, FontFamily> 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<Typeface> 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);
}

}
}
Expand Up @@ -43,16 +43,16 @@

/**
* {@link ReactShadowNode} class for spannable text view.
*
* <p/>
* 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.
* <p/>
* TODO(7255858): Rename *CSSNode to *ShadowView (or sth similar) as it's no longer is used solely
* for layouting
*/
public class ReactTextShadowNode extends LayoutShadowNode {

Expand Down Expand Up @@ -100,7 +100,7 @@ private static final void buildSpannedFromTextCSSNode(
buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops);
} else {
throw new IllegalViewOperationException("Unexpected view type nested under text node: "
+ child.getClass());
+ child.getClass());
}
((ReactTextShadowNode) child).markUpdateSeen();
}
Expand Down Expand Up @@ -128,7 +128,8 @@ private static final void buildSpannedFromTextCSSNode(
new CustomStyleSpan(
textCSSNode.mFontStyle,
textCSSNode.mFontWeight,
textCSSNode.mFontFamily)));
textCSSNode.mFontFamily,
textCSSNode.getThemedContext().getAssets())));
}
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textCSSNode.getReactTag())));
}
Expand Down Expand Up @@ -197,7 +198,7 @@ public void measure(CSSNode node, float width, MeasureOutput measureOutput) {
0,
boring,
true);
} else {
} else {
// Is used for multiline, boring text and the width is known.
layout = new StaticLayout(
text,
Expand Down

10 comments on commit bfeaa6a

@sospartan
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mkonicek should I make a pull request to support load font file from file system , since the Android framework is supporting that. After that we can load extra typeface by downloading font files at runtime.

@flyingmate
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@obipawan
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sospartan
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@flyingmate @obipawan I've send PR #4696

@PierBover
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what happens with other font weights like light, black, etc?

@andrispraulitis
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's say I've added MavenPro-Bold.ttf font..

iOS
{ fontFamily: 'Maven Pro', fontWeight: 'bold' }

Android
{ fontFamily: 'MavenPro-Bold' }

This seems a lot of code just to make it work in Android...

Any way to fix Android to use iOS approach?

@timparker
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we handle other weights like semibold, extrabold, black etc, as @PierBover said. We can't get by with just 2 fontweights in our app.

@SudoPlz
Copy link
Contributor

@SudoPlz SudoPlz commented on bfeaa6a Jul 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@songyouwei
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrispraulitis Have you found any way to fix Android to use iOS approach?

@antoniochen0205
Copy link

@antoniochen0205 antoniochen0205 commented on bfeaa6a Jul 27, 2016 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.