表情包字体库

1 概述

最近项目中要添加表情包聊天的功能(文本和表情包要混合在一起),最直接的解决方案应该就是图文混排,对于这个方案网上有很多的实现,图文混排实现起来比较麻烦,而且和服务端交互的时候还要将图片和与之对应的字符串之间进行变换,对于有追求的我来说,这个方案我是无法接受的,因此直接被否定;表情包是用来聊天的,如果每一个表情包可以看着是一个英文字符,那样实现起来岂不是完美,既不用考虑与服务端的交互,也不用像图文混排那样写自定义的图文混排控件(直接使用TextView和EditText就行),那么用表情包图片生成与之对应的字体库就是最好的解决方案,第一时间想到的就是iconfont,如下图所示:


顾名思义就是图形字体,但是遗憾的对于Android开发只有黑白的,没有彩色的,因此被否定了,然后就开始通过Google搜索解决方案,最终找到了Google提供的解决方案color-emoji,也就是最终采取的解决方案。

color-emoji用到的字体库是TTF(TrueType fonts)字体库,TrueType是Apple和Microsoft在20世纪80年代后期开发的轮廓字体标准,作为在PostScript中使用的AdobeType 1 fonts的竞争对手。 它已成为macOS和Microsoft Windows操作系统上最常用的字体格式。TrueType的主要优势在于它为字体开发人员提供了对字体显示精确程度的高度控制(小到到特定像素、各种字体大小)。 由于目前使用的渲染技术差异很大,因此不再需要TrueType字体中的像素级控制。TrueType字体中的字符(或字形)的轮廓由直线段和二次贝塞尔曲线组成。

Apple已经实现了一个专有扩展,为其emoji字体Apple Color Emoji提供彩色的.ttf文件;在iOS 5之前,使用SoftBank编码在Apple设备上编码emoji。 从iOS 5开始,emoji使用Unicode标准进行编码。emoji字形存储为PNG图像,后来在OpenType1.8版本中进行了标准化。

在Apple Color Emoji推出多年之后,在2013年,Google终于也推出了自己的开源Color Font标准:Open Standard Font Fun for Everyone ,Google同样实现了OpenType的标准,并且提供了一个开源的实现:color-emoji

说点题外话,表情包使我们的生活越来越生动,展示一下工作中的表情包:


我怎么这么好看,这么好看怎么办

憋说话,吻我

嘿嘿嘿

微微一笑很倾城

看看我的牙白吗

以上图片我会用在下面的例子中,让大家从欢笑中体验字体库的魅力。

2 预备知识

2.1 字符编码

2.1.1 字符

所有国家的文字、符号等都是由字符组成的。

2.1.2 字符集

字符集是字符的集合,字符集有很多,常见字符集有:ASCII字符集ISOxxx字符集GBxxx字符集Unicode字符集等,字符集只规定了字符的编号,却没有规定这个编号应该如何存储(由字符编码规定)。

2.1.3 字符编码

计算机能够识别和存储字符集中的字符,就需要对字符集中的字符进行字符编码,字符编码就是规定每个字符是用一个字节还是多个字节存储,这个规定就叫做字符编码;各个国家和地区在制定编码标准的时候,字符集合和字符编码一般都是同时制定的,因此平时所说的ASCIIGB2312GBKUTF-8UTF-16UTF-32等即是字符集合同时也是字符编码。

2.1.4 Unicode字符集

由于编码的不同,同一个二进制数字可以被解释成不同的符号,因此打开一个文本文件就必须知道它的编码,否则用错误的编码方式解读就会出现乱码;如果有一个将世界上所有的符号都纳入其中字符集并且每一个符号都给予一个独一无二的编码,那么乱码问题就会消失,这就是Unicode;Unicode 是一个很大的集合,现在的规模可以容纳100多万个符号,每个符号的编码都不一样,具体的符号对应表,可以查询unicode编码表,或者专门的汉字对应表

2.1.5 ASCII 字符编码

ASCII 字符编码一共包含128个字符的编码(包括32个不能打印出来的控制符号),比如换行LF是10(二进制00001010),大写的字母C是67(二进制0100 0011),只用了一个字节的后7位,最前面的一位统一规定为0。

2.1.6 UTF-8 字符编码

Unicode字符集有多种字符编码,如UTF-8UTF-16UTF-32,其中UTF-8是最广泛使用的,UTF-8最大的特点就是使用一种变长的编码方式,它可以使用1~4个字节表示一个字符,对于0x00-0x7F之间的字符,UTF-8字符编码ASCII字符编码完全相同(即使用一个字节进行字符编码),因此英语字母的UTF-8字符编码ASCII字符编码是相同的,对于使用n(n > 1)字节的进行编码的字符,第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的 Unicode 码,编码规则如下表所示(x表示可用编码的位):

Unicode字符编码范围(十六进制) |       UTF-8编码方式 (二进制)
--------------------------+---------------------------------------------
0000 0000-0000 007F       |       0xxxxxxx
0000 0080-0000 07FF       |       110xxxxx 10xxxxxx
0000 0800-0000 FFFF       |       1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF       |       11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

跟据上表,解读UTF-8字符编码非常简单,如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节;比如字在Unicode字符集中的编号为7F3A(111111100111010),根据上表可以发现字处在第三行的范围内(0000 0800 - 0000 FFFF),因此字的UTF-8字符编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx,然后从字的最后一个二进制位开始依次从后向前填充格式中的x,多出的位补0,这样就得到了字的UTF-8编码是11100111 10111100 10111010,转换成十六进制就是E7BCBA

Android项目中字符用的是UTF-8字符编码,对应Unicode字符集,字对应的Unicode编号就被占用了,对于表情包字体库就要占用Unicode字符集中没有被占用的编号,即就要知道那些范围的字符编号是可以自定义用的,下面是维基百科上给出的可以自定义编号的范围:


可惜的是 color-emoji只支持第一个范围中的编号,但是对于表情包来说已经足够了。

2.2 TextView中的setFakeBoldTextsetTextSkewX方法

setFakeBoldText : 设置字体为伪粗体,之所以叫伪粗体fake bold,因为它并不是使用更高weight的字体让文字变粗,而是通过程序在运行时把文字给描粗了,效果与与下面2.4 中的bold一样。
setTextSkewX:设置文本绘制的水平倾斜因子,默认值为0,正数代表向左倾斜,负数代表向右倾斜,为了近似斜文本,请使用-0.25左右的值,下面2.4 中的italic也是调用的该方法设置水平倾斜因子为-0.25

2.3 Font Weight

用来指定字体的权重,即字体笔触的粗细,可取的值及其意义如下:
100900
这些值组成一个有序序列,下面是与这些值大致对应的权重名称:

100 - Thin
200 - Extra Light (Ultra Light)
300 - Light
400 - Normal 默认值
500 - Medium
600 - Semi Bold (Demi Bold)
700 - Bold
800 - Extra Bold (Ultra Bold)
900 - Black (Heavy)
normal
Same as ‘400’.
bold
Same as ‘700’.

通常指定的字体库只有上面几种权重值,当指定权重存在时,则使用该权重,否者使用下面的规则来匹配权重:
1> 如果所需的权重小于400,则首先降序检查小于所需权重的各个权重,如仍然没有,则升序检查大于所需字重的各权重,直到找到匹配的权重。
2> 如果所需的权重大于500,则首先升序检查大于所需权重的各权重,之后降序检查小于所需权重的各权重,直到找到匹配的权重。
3> 如果所需的权重是400,那么会优先匹配500对应的权重,如仍没有,那么执行第一条所需权重小于400的规则。
4> 如果所需的权重是500,则优先匹配400对应的权重,如仍没有,那么执行第一条所需权重小于400的规则
下图说明了权重的匹配规则,灰色表示权重不存在,需要匹配权重:


具有400,700和900权重值的字体的权重映射

具有300、600重量值的字体的权重映射

对于TTF(TrueType fonts)字体库引入了从100到900的比例,其中400是Normal,因此对于3中生成FruityGirl.ttf字体库拥有上面所有的权重值,在Android的layout文件中使用android:fontFamily属性设置字体时,默认的权重值Normal

2.4 Font Style

用来指定字体的样式,可取的值及其意义如下:
normal:普通,默认值
italic:斜体
bold:粗体 与Font Weight中700相对应
在Android的layout文件中使用android:textStyle属性设置字体的样式

2.5 Android设置字体的方法

2.5.1 系统字体的初始化

首先看一下我测试机上字体配置文件/system/etc/font.xml的部分内容:

<?xml version="1.0" encoding="utf-8"?>
<!--
    All fonts without names are added to the default list. Fonts are chosen
    based on a match: full BCP-47 language tag including script, then just
    language, and finally order (the first font containing the glyph).

    Order of appearance is also the tiebreaker for weight matching. This is
    the reason why the 900 weights of Roboto precede the 700 weights - we
    prefer the former when an 800 weight is requested. Since bold spans
    effectively add 300 to the weight, this ensures that 900 is the bold
    paired with the 500 weight, ensuring adequate contrast.
-->
<familyset version="22">
    <!-- first font is default -->
    <family name="sans-serif">
        <font weight="100" style="normal">Roboto-Thin.ttf</font>
        <font weight="100" style="italic">Roboto-ThinItalic.ttf</font>
        <font weight="300" style="normal">Roboto-Light.ttf</font>
        <font weight="300" style="italic">Roboto-LightItalic.ttf</font>
        <font weight="400" style="normal">Roboto-Regular.ttf</font>
        <font weight="400" style="italic">Roboto-Italic.ttf</font>
        <font weight="500" style="normal">Roboto-Medium.ttf</font>
        <font weight="500" style="italic">Roboto-MediumItalic.ttf</font>
        <font weight="900" style="normal">Roboto-Black.ttf</font>
        <font weight="900" style="italic">Roboto-BlackItalic.ttf</font>
        <font weight="700" style="normal">Roboto-Bold.ttf</font>
        <font weight="700" style="italic">Roboto-BoldItalic.ttf</font>
    </family>

    <!-- Note that aliases must come after the fonts they reference. -->
    <alias name="sans-serif-thin" to="sans-serif" weight="100" />
    <alias name="sans-serif-light" to="sans-serif" weight="300" />
    <alias name="sans-serif-medium" to="sans-serif" weight="500" />
    <alias name="sans-serif-black" to="sans-serif" weight="900" />
    <alias name="arial" to="sans-serif" />
    <alias name="helvetica" to="sans-serif" />
    <alias name="tahoma" to="sans-serif" />
    <alias name="verdana" to="sans-serif" />

    <family name="sans-serif-condensed">
        <font weight="300" style="normal">RobotoCondensed-Light.ttf</font>
        <font weight="300" style="italic">RobotoCondensed-LightItalic.ttf</font>
        <font weight="400" style="normal">RobotoCondensed-Regular.ttf</font>
        <font weight="400" style="italic">RobotoCondensed-Italic.ttf</font>
        <font weight="700" style="normal">RobotoCondensed-Bold.ttf</font>
        <font weight="700" style="italic">RobotoCondensed-BoldItalic.ttf</font>
    </family>
    <alias name="sans-serif-condensed-light" to="sans-serif-condensed" weight="300" />

    <family name="serif">
        <font weight="400" style="normal">NotoSerif-Regular.ttf</font>
        <font weight="700" style="normal">NotoSerif-Bold.ttf</font>
        <font weight="400" style="italic">NotoSerif-Italic.ttf</font>
        <font weight="700" style="italic">NotoSerif-BoldItalic.ttf</font>
    </family>
    ...
    <!-- 简体中文字体 -->
    <family lang="zh-Hans">
        <font weight="400" style="normal" index="2">SECCJK-Regular.ttc</font>
    </family>
    <!-- 繁体中文字体 -->
    <family lang="zh-Hant">
        <font weight="400" style="normal" index="3">SECCJK-Regular.ttc</font>
    </family>

/system/etc/font.xml文件的第一个family节点中的weight="400" style="normal"对应的字体库会作为默认字体库,后面会在源码上说明这一点,/system/etc/font.xml文件配置的字体就是系统提供的所有字体,解析上面的配置文件主要是在android.graphics.Typeface完成的,接下里首先通过下面的时序图从全局上看一下解析的流程:

注意 本文是根据API26版本源码分析的

首先看一下第二步的源码:

private static File getSystemFontConfigLocation() {
    return new File("/system/etc/");
}

static final String FONTS_CONFIG = "fonts.xml";

/*
 * 该方法只会被调用一次,即上面的静态代码块中
 */
private static void init() {
    // Load font config and initialize Minikin state
    // 创建与字体配置文件(/system/etc/font.xml)相对应的File对象
    File systemFontConfigLocation = getSystemFontConfigLocation();
    File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG);
    try {
        FileInputStream fontsIn = new FileInputStream(configFilename);
        FontConfig fontConfig = FontListParser.parse(fontsIn);

        Map<String, ByteBuffer> bufferForPath = new HashMap<String, ByteBuffer>();

        List<FontFamily> familyList = new ArrayList<FontFamily>();
        // Note that the default typeface is always present in the fallback list;
        // this is an enhancement from pre-Minikin behavior.
        // 获取font.xml中的默认字体family并且保存到familyList中,即上图中的第3步
        for (int i = 0; i < fontConfig.getFamilies().length; i++) {
            FontConfig.Family f = fontConfig.getFamilies()[i];
            if (i == 0 || f.getName() == null) {
                // 解析/system/etc/font.xml文件中的family节点然后创建FontFamily对象
                FontFamily family = makeFamilyFromParsed(f, bufferForPath);
                if (family != null) {
                    familyList.add(family);
                }
            }
        }
        sFallbackFonts = familyList.toArray(new FontFamily[familyList.size()]);
        // 使用系统默认字体对应的FontFamily对象创建Typeface对象,即上图中的第4步
        setDefault(Typeface.createFromFamilies(sFallbackFonts));

        // 解析/system/etc/font.xml`文件配置的系统字体并且保存到sSystemFontMap中,即上图的第5步
        Map<String, Typeface> systemFonts = new HashMap<String, Typeface>();
        for (int i = 0; i < fontConfig.getFamilies().length; i++) {
            Typeface typeface;
            FontConfig.Family f = fontConfig.getFamilies()[i];
            if (f.getName() != null) {
                if (i == 0) {
                    // The first entry is the default typeface; no sense in
                    // duplicating the corresponding FontFamily.
                    typeface = sDefaultTypeface;
                } else {
                    // 解析/system/etc/font.xml文件中的family节点然后创建FontFamily对象
                    FontFamily fontFamily = makeFamilyFromParsed(f, bufferForPath);
                    if (fontFamily == null) {
                        continue;
                    }
                    FontFamily[] families = { fontFamily };
                    typeface = Typeface.createFromFamiliesWithDefault(families,
                            RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
                }
                systemFonts.put(f.getName(), typeface);
            }
        }
        for (FontConfig.Alias alias : fontConfig.getAliases()) {
            Typeface base = systemFonts.get(alias.getToName());
            Typeface newFace = base;
            int weight = alias.getWeight();
            if (weight != 400) {
                newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight));
            }
            systemFonts.put(alias.getName(), newFace);
        }
        sSystemFontMap = systemFonts;

    } catch (RuntimeException e) {
        Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e);
        // TODO: normal in non-Minikin case, remove or make error when Minikin-only
    } catch (FileNotFoundException e) {
        Log.e(TAG, "Error opening " + configFilename, e);
    } catch (IOException e) {
        Log.e(TAG, "Error reading " + configFilename, e);
    } catch (XmlPullParserException e) {
        Log.e(TAG, "XML parse exception for " + configFilename, e);
    }
}

上面的代码主要做了上图中3、4、5、6步,第3步和5步很简单,就是解析/system/etc/font.xml文件,有兴趣的同学可以自己看一下源码,下面详细讲解一下第4、6步:

public static final int RESOLVE_BY_FONT_TABLE = -1;

// 第4步调用的方法
private static void setDefault(Typeface t) {
    sDefaultTypeface = t;
    // 将style为NORMAL、weight为400的默认系统字体对应的Typeface对象保存到native层中
    nativeSetDefault(t.native_instance);
}

private Typeface(long ni) {
    if (ni == 0) {
        throw new RuntimeException("native typeface cannot be made");
    }

    native_instance = ni;
    // 获取下面native代码中createFromFamilies方法中fSkiaStyle的值保存到mStyle中
    mStyle = nativeGetStyle(ni);
    mWeight = nativeGetWeight(ni);
}

private static Typeface createFromFamilies(FontFamily[] families) {
    long[] ptrArray = new long[families.length];
    for (int i = 0; i < families.length; i++) {
        ptrArray[i] = families[i].mNativePtr;
    }
    // 创建style为NORMAL、weight为400的默认系统字体对应的Typeface对象
    return new Typeface(nativeCreateFromArray(
            ptrArray, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
}

/**
 * 第6步调用的方法
 * Create a new typeface from an array of font families, including
 * also the font families in the fallback list.
 * @param weight the weight for this family. {@link RESOLVE_BY_FONT_TABLE} can be used. In that
 *               case, the table information in the first family's font is used. If the first
 *               family has multiple fonts, the closest to the regular weight and upright font
 *               is used.
 * @param italic the italic information for this family. {@link RESOLVE_BY_FONT_TABLE} can be
 *               used. In that case, the table information in the first family's font is used.
 *               If the first family has multiple fonts, the closest to the regular weight and
 *               upright font is used.
 * @param families array of font families
 */
private static Typeface createFromFamiliesWithDefault(FontFamily[] families,
            int weight, int italic) {
    long[] ptrArray = new long[families.length + sFallbackFonts.length];
    for (int i = 0; i < families.length; i++) {
        ptrArray[i] = families[i].mNativePtr;
    }
    for (int i = 0; i < sFallbackFonts.length; i++) {
        ptrArray[i + families.length] = sFallbackFonts[i].mNativePtr;
    }
    return new Typeface(nativeCreateFromArray(ptrArray, weight, italic));
}

上面对于RESOLVE_BY_FONT_TABLE常量的解释说明了如下消息:
nativeCreateFromArray23个参数只要有一个参数是RESOLVE_BY_FONT_TABLE,则参数ptrArray数组第一个元素会被使用,如果第一个元素下有多个font节点,则weight为Normal(即regular)且style为upright也就是Normal的字体库会被使用,接下来看下native层的方法nativeCreateFromArray的源码:

static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray,
        int weight, int italic) {
    ScopedLongArrayRO families(env, familyArray);
    std::vector<std::shared_ptr<minikin::FontFamily>> familyVec;
    familyVec.reserve(families.size());
    for (size_t i = 0; i < families.size(); i++) {
        FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]);
        familyVec.emplace_back(family->family);
    }
    return reinterpret_cast<jlong>(
            Typeface::createFromFamilies(std::move(familyVec), weight, italic));
}

// FontStyle的默认构造函数,其中weight值范围是1到9,即对应于2.2中的100到900,这样做的目的是用4bit保存weight值
FontStyle() : FontStyle(0 /* variant */, 4 /* weight */, false /* italic */) {}

Typeface* Typeface::createFromFamilies(
        std::vector<std::shared_ptr<minikin::FontFamily>>&& families,
        int weight, int italic) {
    Typeface* result = new Typeface;
    result->fFontCollection.reset(new minikin::FontCollection(families));

    // 这个判断也就说明了上面RESOLVE_BY_FONT_TABLE注释
    if (weight == RESOLVE_BY_FONT_TABLE || italic == RESOLVE_BY_FONT_TABLE) {
        int weightFromFont;
        bool italicFromFont;

        // 上面的默认构造方法会被调用
        const minikin::FontStyle defaultStyle;
        // families[0]获取的就是第一个family节点,然后通过getClosestMatch方法找到最接近defaultStyle的字体
        const minikin::MinikinFont* mf =
                families.empty() ?  nullptr : families[0]->getClosestMatch(defaultStyle).font;
        if (mf != nullptr) {
            // 对于我的测试机,一定会走到这里,最终weightFromFont为400,italicFromFont为false
            SkTypeface* skTypeface = reinterpret_cast<const MinikinFontSkia*>(mf)->GetSkTypeface();
            const SkFontStyle& style = skTypeface->fontStyle();
            weightFromFont = style.weight();
            italicFromFont = style.slant() != SkFontStyle::kUpright_Slant;
        } else {
            // We can't obtain any information from fonts. Just use default values.
            weightFromFont = SkFontStyle::kNormal_Weight;
            italicFromFont = false;
        }

        if (weight == RESOLVE_BY_FONT_TABLE) {
            weight = weightFromFont;
        }
        if (italic == RESOLVE_BY_FONT_TABLE) {
            italic = italicFromFont? 1 : 0;
        }
    }

    // Sanitize the invalid value passed from public API.
    if (weight < 0) {
        weight = SkFontStyle::kNormal_Weight;
    }

    result->fBaseWeight = weight;
    // 对于我的测试机,weight为400,italic为false,因此fSkiaStyle为SkTypeface::kNormal
    result->fSkiaStyle = computeSkiaStyle(weight, italic);
    result->fStyle = computeMinikinStyle(weight, italic);
    return result;
}

static SkTypeface::Style computeSkiaStyle(int weight, bool italic) {
    // This bold detection comes from SkTypeface.h
    if (weight >= SkFontStyle::kSemiBold_Weight) {
        return italic ? SkTypeface::kBoldItalic : SkTypeface::kBold;
    } else {
        return italic ? SkTypeface::kItalic : SkTypeface::kNormal;
    }
}

enum Weight {
    kInvisible_Weight   =    0,
    kThin_Weight        =  100,
    kExtraLight_Weight  =  200,
    kLight_Weight       =  300,
    kNormal_Weight      =  400,
    kMedium_Weight      =  500,
    kSemiBold_Weight    =  600,
    kBold_Weight        =  700,
    kExtraBold_Weight   =  800,
    kBlack_Weight       =  900,
    kExtraBlack_Weight  = 1000,
};

enum Style {
    kNormal = 0,
    kBold   = 0x01,
    kItalic = 0x02,

    // helpers
    kBoldItalic = 0x03
};

上面讲解完了/system/etc/font.xml文件的解析过程,接下来的7到13步都是用来创建系统字体对应的Typeface对象:

// Android系统目前只提供了字体的四种Style,可以通过android:textStyle属性设置
public static final int NORMAL = 0;
public static final int BOLD = 1;
public static final int ITALIC = 2;
public static final int BOLD_ITALIC = 3;

static {
    // 初始化系统字体
    init();
    // Set up defaults and typefaces exposed in public API
    // 对应上图中的第7步
    DEFAULT         = create((String) null, 0);
    // 对应上图中的第8步
    DEFAULT_BOLD    = create((String) null, Typeface.BOLD);
    // 对应上图中的第9步
    SANS_SERIF      = create("sans-serif", 0);
    // 对应上图中的第10步
    SERIF           = create("serif", 0);
    // 对应上图中的第11步
    MONOSPACE       = create("monospace", 0);

    // sDefaults保存系统默认字体的四种style(NORMAL、BOLD、ITALIC、BOLD_ITALIC)对应的Typeface
    sDefaults = new Typeface[] {
        DEFAULT,
        DEFAULT_BOLD,
        // 对应上图中的第12步
        create((String) null, Typeface.ITALIC),
        // 对应上图中的第13步
        create((String) null, Typeface.BOLD_ITALIC),
    };
}

/**
 * 根据字体family名称和字体style创建Typeface对象,如果字体family名称为null则返回系统默认字体对应的Typeface对象
 */
public static Typeface create(String familyName, int style) {
    // sSystemFontMap保存的是上面init方法初始化的系统字体
    if (sSystemFontMap != null) {
        return create(sSystemFontMap.get(familyName), style);
    }
    return null;
}

public static Typeface create(Typeface family, int style) {
    if (style < 0 || style > 3) {
        style = 0;
    }
    long ni = 0;
    if (family != null) {
        // Return early if we're asked for the same face/style
        // 由第4步中mStyle的值可知上图中第9、10、11步下面的判断会成立,直接返回
        if (family.mStyle == style) {
            return family;
        }

        ni = family.native_instance;
    }

    Typeface typeface;
    SparseArray<Typeface> styles = sTypefaceCache.get(ni);

    if (styles != null) {
        typeface = styles.get(style);
        if (typeface != null) {
            return typeface;
        }
    }
    
    // 上图中的第7、8、12、13会走到这里
    typeface = new Typeface(nativeCreateFromTypeface(ni, style));
    if (styles == null) {
        styles = new SparseArray<Typeface>(4);
        sTypefaceCache.put(ni, styles);
    }
    styles.put(style, typeface);

    return typeface;
}

接下来就是看一下nativeCreateFromTypeface方法:

static jlong Typeface_createFromTypeface(JNIEnv* env, jobject, jlong familyHandle, jint style) {
    Typeface* family = reinterpret_cast<Typeface*>(familyHandle);
    Typeface* face = Typeface::createRelative(family, (SkTypeface::Style)style);
    // TODO: the following logic shouldn't be necessary, the above should always succeed.
    // Try to find the closest matching font, using the standard heuristic
    if (NULL == face) {
        face = Typeface::createRelative(family, (SkTypeface::Style)(style ^ SkTypeface::kItalic));
    }
    for (int i = 0; NULL == face && i < 4; i++) {
        face = Typeface::createRelative(family, (SkTypeface::Style)i);
    }
    return reinterpret_cast<jlong>(face);
}

Typeface* Typeface::createRelative(Typeface* src, SkTypeface::Style style) {
    // 对于第7、8步得到的是默认字体对象,对于12、13得到的是其对应的系统字体对象
    Typeface* resolvedFace = Typeface::resolveDefault(src);
    Typeface* result = new Typeface;
    if (result != nullptr) {
        result->fFontCollection = resolvedFace->fFontCollection;
        result->fBaseWeight = resolvedFace->fBaseWeight;
        result->fSkiaStyle = style;
        result->fStyle = computeRelativeStyle(result->fBaseWeight, style);
    }
    return result;
}

Typeface* Typeface::resolveDefault(Typeface* src) {
    // gDefaultTypeface 就是在第4步中设置的默认字体对象
    LOG_ALWAYS_FATAL_IF(gDefaultTypeface == nullptr);
    return src == nullptr ? gDefaultTypeface : src;
}

到这里系统字体的初始化已经分析完了,得出如下结论:
系统根据字体配置文件/system/etc/font.xml进行系统字体的初始化,该配置文件的第一个family节点会作为默认字体,为该默认字体创建styleNORMAL、BOLD、ITALIC、BOLD_ITALICweight400的四个Typeface对象保存到sDefaults数组中,为/system/etc/font.xml文件中名称为sans-serif、serif、monospacefamily节点创建styleNORMALweight400的Typeface对象SANS_SERIF、SERIF、MONOSPACE

2.5.2 在layout文件中通过android:typeface属性设置系统字体

目前通过这种方式可以设置如下几种系统字体,即2.5.1中初始化的四种系统字体:
noraml 普通字体,系统默认使用的字体
sans 非衬线字体
serif 衬线字体
monospace 等宽字体

2.5.3 App中使用自定义字体

在Android是使用android:fontFamily属性,关于该属性如何使用,可以看下面4.1的内容,由于该属性是给TextView使用的,所以就要从TextView的源码开始分析,先看一下下面的流程图:


上图就是android:fontFamily属性的解析过程,有兴趣的同学可以自己对着上图看源码,这里概括一下整个流程:
1> 解析res/font/font_family.xml文件,根据TextView的android:textStyle属性的值找到最匹配的font节点(即上面的第28步)
2> 根据该font节点对应的字体库创建Typeface对象,然后设置TextView的字段mTextPaint的字体为该Typeface对象

3 color-emoji的使用

color-emoji是Google开源的工具,用来将png类型的图片生成字体库,首先下载在https://github.com/googlei18n/color-emoji地址下载源码,然后进入到color-emoji/examples/FruityGirl目录下,目录结构如下所示:

$ tree -L 2
.
├── FruityGirl.tmpl.ttx.tmpl
├── Makefile
└── png
    ├── F000.png
    ├── F001.png
    ├── F002.png
    ├── F003.png
    ├── F004.png
    ├── F005.png
    ├── F006.png
    ├── F007.png
    ├── F008.png
    ├── F009.png
    ├── F00A.png
    ├── F00B.png
    ├── F00C.png
    ├── F00D.png
    ├── F00E.png
    ├── F00F.png
    ├── F010.png
    ├── F011.png
    ├── F012.png

上面又一个Makefile的文件,这个文件编译过android系统源码的应该都很熟悉了,就是通过make命令来开始编译操作,png目录下的图片就是用来生成字体库的,图片的名称就是生成的字体库中与之对应的Unicode编号,将生成的字体库应用到TextView或者EditText上后,就可以使用该编号来显示对应的图片。

首先将目录中原来的png图片都删除掉,然后将上面五张表情包拷贝到png目录下,查了五个字(与上面的五张表情包惺惺相惜)的Unicode编码:傻(50BB)、吻(543B)、嘿(563F)、微(5FAE)、白(767D)。

执行make命令,然后看一下目录结构:

$ tree -L 2
.
├── FruityGirl.tmpl.ttf
├── FruityGirl.tmpl.ttx.tmpl
├── FruityGirl.ttf
├── Makefile
└── png
    ├── 50BB.png
    ├── 543B.png
    ├── 563F.png
    ├── 5FAE.png
    └── 767D.png

可以看到多了一个FruityGirl.ttf文件,这就是包含上面五张图片的字体库。

4 在Android中使用2中生成的字体库

举个例子,首先看一下运行效果:



上面截图中就是一个普通EditText控件,只不过是应用了2中生成的字体库,当输入傻、吻、嘿、微和白五个汉字时,就会显示上面的五张图片。

4.1 将字体库添加到Android工程中

Android 8.0(API级别26)引入了一项新功能,在XML文件中的声明字体库,即把字体库文件当作资源文件,以在res/font/文件夹中添加字体库文件的方式来将字体库文件作为资源文件,这些字体库文件被编译到R文件中并且借助新的资源类型访问字体资源,例如,要访问字体资源,请使用@font/myfontR.font.myfont

要在运行Android 4.1(API级别16)及更高版本的设备上使用该新功能,请使用the Support Library 26。
要将字体库添做为资源,首先在res目录下创建font文件夹,如下图所示:


点击OK按钮,然后将字体库文件放到该目录下,如下图所示的目录结构将生成R.font.test:

上图中的test.ttf字体库文件就是3中生成的FruityGirl.ttf字体库文件重命名得到的

4.2 创建font_family.xml文件

接着在font目录下创建font_family.xml文件,font_family.xml文件包含一个或多个字体库文件及其style和weight,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@font/font_family">
    <font
        android:font="@font/test"
        android:fontStyle="normal"
        android:fontWeight="400" />
</font-family>

关于android:fontStyleandroid:fontWeight在2中已经讲过了。

4.3 在layout文件中使用

接着就是在layout文件中使用font_family.xml文件定义的字体了:

Activity的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="80dp">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="36dp"
        android:fontFamily="@font/test"/>
</LinearLayout>

使用android:fontFamily属性给EditText设置字体库就可以了,是不是很简单,这才是我追求的解决方案。

5 参考

  1. Android中使用ttf字体库官方文档
  2. CSS Fonts Module Level 3
  3. Font
  4. 自定义 View 1-3 drawText() 文字的绘制
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 143,736评论 1 303
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,631评论 1 258
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 95,145评论 0 213
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,213评论 0 181
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 49,025评论 1 259
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,893评论 1 178
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,497评论 2 273
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,240评论 0 167
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 29,114评论 6 235
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,596评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,363评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,717评论 1 232
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,279评论 0 32
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,183评论 2 214
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,629评论 3 209
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,647评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,047评论 0 167
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,625评论 2 232
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,734评论 2 236

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,850评论 3 114
  • 1. Stop Dancing Around Criticism and Put It to Use with T...
    kid551阅读 373评论 0 1
  • 每个人的一生中都有一些无法忘记的美好回忆。随着岁月的推移,有些人,有些事,却仍牵动于心。 我也不例外,在特殊的时间...
    人生由你阅读 253评论 1 3
  • 揭谛!揭谛!波罗揭谛!波罗僧揭谛!菩提萨婆诃! --摘自《心经》学书作品,20181020
    ycongcong阅读 257评论 0 3