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了。