1 概述
最近项目中要添加表情包聊天的功能(文本和表情包要混合在一起),最直接的解决方案应该就是图文混排,对于这个方案网上有很多的实现,图文混排实现起来比较麻烦,而且和服务端交互的时候还要将图片和与之对应的字符串之间进行变换,对于有追求的我来说,这个方案我是无法接受的,因此直接被否定;表情包是用来聊天的,如果每一个表情包可以看着是一个英文字符,那样实现起来岂不是完美,既不用考虑与服务端的交互,也不用像图文混排那样写自定义的图文混排控件(直接使用TextView和EditText就行),那么用表情包图片生成与之对应的字体库就是最好的解决方案,第一时间想到的就是iconfont,如下图所示:
顾名思义就是图形字体,但是遗憾的对于Android开发只有黑白的,没有彩色的,因此被否定了,然后就开始通过Google搜索解决方案,最终找到了Google提供的解决方案 color-emoji,也就是最终采取的解决方案。
color-emoji用到的字体库是TTF(TrueType fonts)字体库,TrueType是Apple和Microsoft在20世纪80年代后期开发的轮廓字体标准,作为在PostScript中使用的Adobe的 Type 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 字符编码
计算机能够识别和存储字符集中的字符,就需要对字符集中的字符进行字符编码,字符编码就是规定每个字符是用一个字节还是多个字节存储,这个规定就叫做字符编码;各个国家和地区在制定编码标准的时候,字符集合和字符编码一般都是同时制定的,因此平时所说的ASCII
、GB2312
、GBK
、UTF-8
、UTF-16
、UTF-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-8
、UTF-16
、UTF-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
中的setFakeBoldText
和setTextSkewX
方法
setFakeBoldText
: 设置字体为伪粗体,之所以叫伪粗体fake bold
,因为它并不是使用更高weight
的字体让文字变粗,而是通过程序在运行时把文字给描粗了,效果与与下面2.4 中的bold
一样。
setTextSkewX
:设置文本绘制的水平倾斜因子,默认值为0
,正数代表向左倾斜,负数代表向右倾斜,为了近似斜文本,请使用-0.25
左右的值,下面2.4 中的italic
也是调用的该方法设置水平倾斜因子为-0.25
。
2.3 Font Weight
用来指定字体的权重,即字体笔触的粗细,可取的值及其意义如下:
100
到 900
这些值组成一个有序序列,下面是与这些值大致
对应的权重名称:
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的规则
下图说明了权重的匹配规则,灰色表示权重不存在,需要匹配权重:
对于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
的部分内容:
Roboto-Thin.ttf
Roboto-ThinItalic.ttf
Roboto-Light.ttf
Roboto-LightItalic.ttf
Roboto-Regular.ttf
Roboto-Italic.ttf
Roboto-Medium.ttf
Roboto-MediumItalic.ttf
Roboto-Black.ttf
Roboto-BlackItalic.ttf
Roboto-Bold.ttf
Roboto-BoldItalic.ttf
RobotoCondensed-Light.ttf
RobotoCondensed-LightItalic.ttf
RobotoCondensed-Regular.ttf
RobotoCondensed-Italic.ttf
RobotoCondensed-Bold.ttf
RobotoCondensed-BoldItalic.ttf
NotoSerif-Regular.ttf
NotoSerif-Bold.ttf
NotoSerif-Italic.ttf
NotoSerif-BoldItalic.ttf
...
SECCJK-Regular.ttc
SECCJK-Regular.ttc
/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 bufferForPath = new HashMap();
List familyList = new ArrayList();
// 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 systemFonts = new HashMap();
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常量的解释说明了如下消息:
nativeCreateFromArray
第2
和3
个参数只要有一个参数是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> familyVec;
familyVec.reserve(families.size());
for (size_t i = 0; i < families.size(); i++) {
FontFamilyWrapper* family = reinterpret_cast(families[i]);
familyVec.emplace_back(family->family);
}
return reinterpret_cast(
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>&& 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(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 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(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(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(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
节点会作为默认字体,为该默认字体创建style
为NORMAL、BOLD、ITALIC、BOLD_ITALIC
,weight
为400
的四个Typeface对象保存到sDefaults
数组中,为/system/etc/font.xml
文件中名称为sans-serif、serif、monospace
的family
节点创建style
为NORMAL
、weight
为400
的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/myfont
或R.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,如下所示:
关于android:fontStyle
和android:fontWeight
在2中已经讲过了。
4.3 在layout文件中使用
接着就是在layout文件中使用font_family.xml
文件定义的字体了:
Activity的布局文件:
使用android:fontFamily
属性给EditText设置字体库就可以了,是不是很简单,这才是我追求的解决方案。
5 参考
- Android中使用ttf字体库官方文档
- CSS Fonts Module Level 3
- Font
- 自定义 View 1-3 drawText() 文字的绘制