Android 对 Emoji 的支持
在 Android 4.4 以前, Android 并不支持 emoji 表情,当时的解决方案主要是经过 imageSpan 配合 spannableString,来替换掉文字中的 emoji unicode 编码符号。从 Android 4.4 开始, 官方开始了 emoji 表情的支持,实现原理基本就是经过把 emoji 表情内置在系统的 ttf 字体库中,对文本进行过滤后显示出 emoji 表情。因为不一样 Android 版本内置的 ttf 字体库对 emoji 表情的版本支持程度不一样,致使老版本的 Android 对最新的 emoji 表情支持不全,因此一些 在新的 unicode 版本规范中被加入的 emoji 表情在老的 Android 设备上会显示方框乱码。为了处理这个问题,除去上文提到的 spannable 的处理方案,咱们还能够经过定义本身的 ttf 字体库给文本空间指定字体来显示 emoji 表情。
除此之外,谷歌官方也推出了EmojiCompat Support Library,目前这个库能向下兼容到 Android 4.4,其主要目标就是为了让咱们的 Android 设备可以支持最新的 emoji 表情,防止最新的 emoji 表情在咱们的手机上显示为☐(豆腐块)。EmojiCompat 经过 CharSequence 文本中的 emoji 对应的 unicode 编码来识别 emoji 表情,将他们替换成 EmojiSpans ,最后再将 EmojiSpan 渲染成对应的 emoji 表情符号。 官方链接
配置 EmojiCompat
EmojiCompat 提供两种字体的支持方式,它们分别是可下载的字体配置和本地捆绑的字体配置。
- 可下载的字体配置:可下载的字体的方式会在首次启动 app 的时候检查本地是否有该字体,没有的话会从网上下载最新的 Emoji 字体,然后遇到不支持的 Emoji,就会从这个字体文件中,加载资源并且渲染。关于字体下载的配置方法
- 本地捆绑的字体配置:本地捆绑的方式会在 App 打包的过程中,植入一个最新的 Emoji 字体文件,然后遇到不支持的 Emoji,就会从这个字体文件中,加载资源并且渲染。
本地字体配置方式
首先,需要在 build.gradle添加emoji-bundled依赖,如下。
implementation 'androidx.emoji:emoji-bundled:1.1.0'
然后,初始化 构建本地捆绑字体配置EmojiCompat,由于初始化是耗时的,所以最好提前进行初始化,比如Application中。
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
initEmoji()
}
private fun initEmoji() {
val config: EmojiCompat.Config = BundledEmojiCompatConfig(this)
config.setReplaceAll(true)
config.registerInitCallback(object : InitCallback() {
override fun onInitialized() {
//初始化成功回调
}
override fun onFailed(@Nullable throwable: Throwable?) {
//初始化失败回调
}
})
EmojiCompat.init(config)
}
}
可下载的字体配置
可下载的字体需要在 build.gradle添加emoji依赖,如下。
def emoji2_version = "1.0.0"
implementation "androidx.emoji2:emoji2:$emoji2_version"
implementation "androidx.emoji2:emoji2-views:$emoji2_version"
implementation "androidx.emoji2:emoji2-views-helper:$emoji2_version"
然后,构建可下载字体配置初始化 EmojiCompat,初始化的时候需要传入字体,如下。
private fun initEmojiCompat() {
val fontRequest = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.emoji_list
)
val config: EmojiCompat.Config = FontRequestEmojiCompatConfig(this, fontRequest)
config.setReplaceAll(true)
config.registerInitCallback(object : InitCallback() {
override fun onInitialized() {
//初始化成功回调
}
override fun onFailed(throwable: Throwable?) {
//初始化失败回调
}
})
EmojiCompat.init(config)
}
如何使用EmojiCompat
在AppCompat 1.4中的 AppCompatEditText 和 AppCompatTextView组件已经内置了EmojiCompat组件,使用起来非常方便,下面让我们来看看是怎么实现的。
/**
* Interface for Views that expose EmojiCompat configuration.
*/
public interface EmojiCompatConfigurationView {
void setEmojiCompatEnabled(boolean enabled);
boolean isEmojiCompatEnabled();
}
AppCompatEditText 继承了 EmojiCompatConfigurationView 这个接口,只需要 setEmojiCompatEnabled 就可以开启EmojiCompat功能,默认是打开的。
AppCompatEmojiEditTextHelper(@NonNull EditText view) {
mView = view;
mEmojiEditTextHelper = new EmojiEditTextHelper(view, false);
}
在AppCompatEditText 中有一个 mEmojiEditTextHelper = new EmojiEditTextHelper(view, false), EmojiEditTextHelper是emoji2库中的类,就是通过这个类来实现EmojiComate功能的。 再继续往下追:
public EmojiEditTextHelper(@NonNull EditText editText, boolean expectInitializedEmojiCompat) {
Preconditions.checkNotNull(editText, "editText cannot be null");
if (Build.VERSION.SDK_INT < 19) {
mHelper = new HelperInternal();
} else {
mHelper = new HelperInternal19(editText, expectInitializedEmojiCompat);
}
}
HelperInternal19(@NonNull EditText editText, boolean expectInitializedEmojiCompat) {
mEditText = editText;
mTextWatcher = new EmojiTextWatcher(mEditText, expectInitializedEmojiCompat);
mEditText.addTextChangedListener(mTextWatcher);
mEditText.setEditableFactory(EmojiEditableFactory.getInstance());
}
关键的类出现了:EmojiTextWatcher
public void onTextChanged(CharSequence charSequence, final int start, final int before,
final int after) {
if (mEditText.isInEditMode() || shouldSkipForDisabledOrNotConfigured()) {
return;
}
//before > after --> a deletion occurred
if (before <= after && charSequence instanceof Spannable) {
switch (EmojiCompat.get().getLoadState()){
case EmojiCompat.LOAD_STATE_SUCCEEDED:
final Spannable s = (Spannable) charSequence;
EmojiCompat.get().process(s, start, start + after, mMaxEmojiCount,
mEmojiReplaceStrategy);
break;
case EmojiCompat.LOAD_STATE_LOADING:
case EmojiCompat.LOAD_STATE_DEFAULT:
EmojiCompat.get().registerInitCallback(getInitCallback());
break;
case EmojiCompat.LOAD_STATE_FAILED:
default:
break;
}
}
}
每当textChanged的时候,使用EmojiCompat.get().process()将原来charSequence中的Emoji换成EmojiSpan。我们也可以手动调用EmojiCompat.get().process()这个方法进行Emoji的转换。
需要十分注意的地方:
if (before <= after && charSequence instanceof Spannable)
对于editText来说只有字符增加的时候才会触发EmojiCompat,删除字符的时候不会触发,算是一个增量操作。但是如果我们在代码里面主动editText.setText(text),而且新字符比原来少,就会被这个if的逻辑拦截住,这个时候就需要手动EmojiCompat.get().process()来处理text了。