支持emoji的字体_Android

正文共: 5934字 16图

预计阅读时间: 15分钟

Hi,大家好,我是承香墨影!

今天看题目就知道,继续来分析 Android 下的 Emoji 。Google 新出的 Support 包里,增加了一个 EmojiCompat,就是为了解决 Emoji 的问题。

上一篇文章,已经分析了如何使用 EmojiCompat 的相关细节,不太了解的可以先看看:《Android 开发,你遇上 Emoji 头疼吗?》,今天就换一个维度,来从源码的角度看看 EmojiCompat 有什么能让我们学习的实现细节!

Emoji 确实让我们头疼,那我们来分析 Google 给我们的解决方案 EmojiCompat 。不过呢?分析源码我们就带着问题来看它?本文尝试弄清楚几个问题?

  1. EmojiCompat 如何保证兼容。
  2. EmojiCompat 的实现原理。
  3. 能不能干掉 BundleEmoji 打入包内的 7.4MB 大小的字体包?

主要是分析 EmojiCompat 的一些细节,如果你对 EmojiCompat 有所了解,你会知道它会在 assets 里,打包的时候,嵌入一个 NotoColorEmojiCompat.ttf 字体文件,这个字体包大概有 7.4MB ,会直接增加 Apk 的大小,那么最后我们尝试解决让它不直接增加在 Apk 里。

一、EmojiCompat 基础结构

EmojiCompat 使用起来,非常的方便,它包含几个点。在之前的文章中,就已经说清楚了,建议还是先看看之前的文章《Android 开发,你遇上 Emoji 头疼吗?》,这里为了保证文章的完整性,我这里简单回顾一下。

EmojiCompat 会使用 ttf 字体,来做到 Emoji 的兼容支持,本身在 2010 年之后,Emoji 就已经在 Unicode 中分配了码点,在那之后,Emoji 就是一个字体。

EmojiCompat 使用的字体包,前面也提过,大概会有 7.4MB,而根据 EmojiCompat 使用的字体文件的来源,它被分为两种:

  1. 可下载的字体。
  2. Apk 包内捆绑的字体。

可下载的字体,是 com.android.support:support-emoji:VersionXXX原生支持的,可是它需要依赖 Google 服务,所以在国内大环境下,你基本上是用不上的。

那么除非你是在做海外产品,否则你只能使用第二种方式,Apk 内捆绑字体,这个时候你就需要使用 com.android.support:support-emoji-bundled:VersionXxx ,它会自动在 Apk 打包的时候,向 assets 目录下,捆绑嵌入一个 NotoColorEmojiCompat.ttf 字体文件,它大概有 7.4MB。

这些,操作的其实都是 Emoji 的字符串,如果你想更方便的使用 EmojiCompat 还提供了一些 EmojiAppCompatXxx 控件,使用它只需要替换项目内需要显示 Emoji 的 TextView 或者 EditText 等就可以了,不过使用这些支持 Emoji 的控件,你需要引用 com.android.support:support-emoji-appcompat:Version 依赖。

好了,到这里 EmojiCompat 的大致结构就已经清晰了,接下来我们看看 EmojiCompat 的源码细节。

二、EmojiCompat 的源码细节

2.1 EmojiCompat.Config

EmojiCompat 在使用之前,需要调用 EmojiCompat.init()  来进行初始化,而 init() 方法,需要传递一个 Config 的抽象类,EmojiCompat 就是根据这个 Config 来决定这个字体文件的来源,到底是从线上下载还是 Apk 本地捆绑。

先来看 Config 的结构。

支持emoji的字体_支持emoji的字体_02

Config 这个抽象类,提供的几个抽象方法,作用呢看名字就已经很清晰了。

这里简单介绍一下:

1、如果要监听 EmojiCompat 的初始化状态,需要使用 registerInitCallback()unregisterInitCallback() ,来增加监听和解绑监听。

2、如果要给 Emoji 增加一个背景色,可以使用 setEmojiSpanIndicatorColor() 设置一个背景色,并使用 setEmojiSpanIndicatorEnabled() 将这个开关打开。

3、设置匹配度,使用 setReplaceAll() 进行配置。

Config 还是很简单的,这里说一下 setReplaceAll() 方法,EmojiCompat  默认的模式会优先使用当前设备自带的 System Font,在 System Font 不支持这个 Emoji 的时候,才会使用 Support Font 来渲染 Emoji。

支持emoji的字体_Android_03

之前文章中,给的解决方案是直接将 ttf 字体,使用 Typeface 设置的到 TextView 上去,但是阅读源码之后,发现 EmojiCompat 其实是提供了对应的策略的调整的,就是使用 setReplaceAll() 。当然,使用设置字体的方式也能解决问题,但是总是不及官方提供的方法好,这里纠错一下。

2.2 初始化不同字体的策略区分

前面提到了 Config ,而 EmojiCompat 用来区分不同的加载方式,使用的就是不同的 Config 实现类。

这里就涉及到两个类:

1、FontRequestEmojiCompatConfig

它可以做到去请求可下载的 Emoji 字体,不过它依赖 Google 服务,在国内进本处于废弃状态。

可能这里简单看个名字,你会以为它真的是去下载 Emoji 字体,其实并不是。它是依赖 FontProviderHelper 来和系统中的 Google 服务,通过 FileProvider 进行交互,获取到 Google 服务之前缓存好的字体包资源,如果没有字体的缓存,就会由 Google 服务来下载字体。

使用这种方式,非常的优雅,在 Android 的生态下,直接使用系统已经提供好的字体资源,不需要我们再去关心下载的逻辑,可惜大多数情况下我们使用不到,所以这里不再过多介绍。

2、BundledEmojiCompatConfig

使用 BundledEmojiCompatConfig ,需要提前引入 support-emoji-bundled 依赖。

支持emoji的字体_Android_04

新增加的依赖其实也非常的简单,只有一个有效类,它就是我们需要的 BundledEmojiCompatConfig,接下来我们看看它内部是如何实现的。

支持emoji的字体_Android_05

BundledEmojiCompatConfig 内部实现了 EmojiCompat.MetadataRepoLoader 来做初始化,它其中其实是开了个新线程来进行初始化,具体逻辑在 InitRunnable 中。

支持emoji的字体_初始化_06

InitRunnable 主要是为了初始化 MetadataRepo ,在此过程中,会去加载前面提到的 NotoColorEmojiCompat.ttf 字体,这是一个耗时操作,被放在子线程中去完成。

本质上来说 init() 的过程,实际上就是为了得到的 MetadataRepo 对象,它是用来维护加载好的所有 Emoji 的数据,这里记住它,之后会用到。

2.3 EmojiCompat 的初始化

前面提到,EmojiCompat 的初始化是需要调用它的 init() 方法,而 init() 方法内部其实就是一个最常见的单例,最终还是调用的它自己私有的初始化方法。

支持emoji的字体_Google_07

在 EmojiCompat 的构造方法中,会初始化一些必要的资源和从 Config 中提取一些默认配置。

这里只想需要关注几个点:

1、EmojiCompat 使用了 ReentrantReadWriteLock,所以大多数操作都是线程安全的。

2、EmojiCompat 大部分的实际操作,都是通过 mHelper 来实现的,不同的 Api Level 有不同的实现,也正是通过它,来做到版本兼容的。

EmojiCompat 的构造方法,最后一行会调用 mHelper.loadMetadata() 方法,Api Level 19 以下使用的 CompatInternal,其实就是空实现,没有什么有意义的代码,我们这里主要关注 CompatInternal19 。

支持emoji的字体_Android_08

在 CompatInternal19 的 loadMetadata() 中,会去调用 MetadataLoader.load() 方法,我们这里使用本地捆绑的方式加载 Emoji 字体,所以会调用到前面介绍的 BundledEmojiCompatConfig 中的 BundledMetadataLoader 去,通过监听回调,来获得初始化的加载的用于存放 Emoji 字体信息的 MetadatatRepo 类。

2.4 EmojiCompat.process() 的过程

EmojiCompat 替换 Emoji,需要使用它的 process() 方法,当你初始化完成之后,就可以调用它了,接下来我们来分析一下 process() 是如何工作的。

支持emoji的字体_支持emoji的字体_09

process() 有多个重载方法,最终都会调用到这个参数最多的方法上。我这里在截图中隐藏掉了一些不重要的校验逻辑,不过不影响阅读。

process() 方法中,可以通过 replaceStrategy 设置 Emoji 字体的替换规则,这里和 Config 中的设置一样。而最终,是借助 EmojiProcessor.process() 来完成操作。

EmojiProcessor.process() 的实现逻辑还是很清晰的,大体上做以下两个事情。

1、首先判断传递进来的 charSequence 是不是一个 Spannable 内,有没有 EmojiSpan,有的话全部移除。

2、然后用一个循环去检验 charSequence 中是否有 Emoji,有的话,使用 EmojiSpan 包装它。

下面是 EmojiProcessor.process() 的关键代码。

支持emoji的字体_Google_10

当 Action 为 ACTION_FLUSH 的时候,就会使用 EmojiSpan 替换替换它。

2.5 EmojiAppCompatXxx 控件的逻辑

如果我们不想直接使用 process() 来操作所有的字符串,可以使用 EmojiCompat 提供的一些对 Emoji 支持的控件,这就需要引入 support-emoji-appcompat 依赖。

支持emoji的字体_支持emoji的字体_11

在这个包里,只定义了几个可能需要显示字体的控件,接下来我们看看它们的实现逻辑,这里拿 EmojiAppCompatTextView 来研究,其他几个大致是一样的。

支持emoji的字体_Google_12

所有相关的操作都被包装在了 EmojiTextViewHelper() 中,它需要把当前的 TextView 对象传递进去。而在 EmojiTextViewHelper 中,实际上关键代码是它的 wrapTransformationMethod() 方法,在其中对 TransformationMethod 做了一个包装,将普通的 TransformationMethod 包装成了 EmojiTransformationMethod 。

TransformationMethod 有些朋友可能不太清楚他是干嘛的,简单来说,它会去替换当前显示的内容,例如如果你设置一个 TextView 需要显示 password,输入的字符会被替换成星号(*),就是它干的。

这里使用 EmojiTransformationMethod 对其进行包装,实际上是想用 Emoji 字体来替换它原本的显示,关键代码在 getTransformation() 方法中。

支持emoji的字体_支持emoji的字体_13

可以看到,它实际上也是去调用的 EmojiCompat.get().process(),和你直接调用并没有什么不一样。

好了,到这里 EmojiCompat 源码的所有相关细节

四、如何不让ttf 字体打包在 Apk 中

对于 Emoji 的支持,肯定是需要引入一些资源的,这一点是毋庸置疑的。只不过能不能让这个资源不要被打包在 Apk 里,我想对于任何 Apk ,一下子增大 7.4MB,都是有压力的。

那我们来讨论一下,如何解决这个问题,我们从线上下载一个 NotoColorEmojiCompat.ttf 字体给 EmojiCompat 可不可以?这样虽然会增大服务器下载的流量,但是可以节省 Apk 的体积。能做到这一步,接下来就是一个方案选择的问题。

从 BundledEmojiCompatConfig 的源码中,可以了解到,它只是需要一个 ttf 的字体文件,而如果我们参照它,重写一个实现 Config 的类,就可以做到从线上下载一个字体文件,使用这个下载的字体文件进行初始化。

好了,这样思路就明确了,那我们看看源码细节,看是否能如此实现。

1、Config 是否可被实现。

一个 Config 中,需要使用的 EmojiCompat.Config 和 EmojiCompat.MetadataRepoLoader 都是 public 的,所以可以被开发者自行实现的。

2、MetadataRepo.create() 能不能支持别的来源。

MetadataRepo 是 EmojiCompat 初始化的关键,初始化就是为了得到这个对象。而 MetadataRepo.create() 还提供了其他几个重载方法。

支持emoji的字体_Android_14

第一个参数都需要一个 Typeface ,而 Typeface 是可以加载一个我们指定目录下的字体文件的,那就看第二个参数。

第二个参数,是一个 MetadataListReader 对象,它需要的其实就是这个字体文件的 InputStream。字体文件的输入流,文件都有了,InputStream 一定也能拿到。

到这里就清晰了,我们完全可以在初始化的时候,从线上下载一个字体文件,然后再使用 MetadataRepo 去初始化它,最终将状态返回给 EmojiCompat,来做到我们不将 Emoji 字体打包在 Apk 内的目的。

思路很简单,当然还需要额外处理一些下载失败和还没有初始化完成的时候,如何显示的问题,这里就不提供示例代码了。

到此,就分析完 EmojiCompat 的所有细节,不知道对你有什么帮助?