EditText&IME 候选逻辑关联

最近公司业务遇到一个比较奇怪的现象,那就是 iOS 和 Android 在基本同等延迟逻辑下,为啥 Android 搜索触发比 iOS 触发要高,并且统计数据和线上实时数据统计也基本相同。因为这块逻辑从第一代版本、第二代版本,到第三代修改版本都是我负责的,并且也不断修修补补和 iOS 逻辑靠近过几次,居然会有那么大的差距,这是不合理的。

于是开始调查,首先调查方向就是确定和 iOS 是真的在想通场景下,会有大量额外的搜索触发。请测试同学拿了 Android 版本和 iOS 进行对比,逐个字符上屏触发逻辑基本是一致的,没有太大的差异。但是细心测试同学发现使用 iOS 默认输入法一开始输入某些字符的时候,系统会提供候选词的概念。

一、CandidateView

候选词概念是输入法里面用来快速定位单词的,帮助用户更好的输入他想要的单词,最大特点就是输入过程中,随着累积输入的字符,而有不同的单词提示。这个单词能否快速命中关乎到输入法好不好用的一个标准。候选词本身也有联想功能,是通过大批量数据得出的搜索建议。一下子牵涉到老本行,虽然说当时只是个很小的角色,改的也不是输入法核心功能,但是基本流程还算了解,并且印象还特别深刻,于是乎对于目前遇到的问题一下子产生了联想和定位。

输入法出现候选词状态时,从 iOS 那边了解到,iOS 那边会有个输入中的判定。只不过 iOS 这种判断相对 Android 比较容易,于是一般开发就很容易想到,Android 可以往这方面去想。

项目使用的是 EditText 输入控件,去里面找有没有 candidate 和 underline 相关的关键字和 API,很遗憾找了一圈基本没有,包括度娘和谷歌,完全没有相关的介绍。经过一圈的查找,觉得方向出问题了,也就是把搜索关键字放宽,不再仅限 EditText,于是乎出现了熟悉的东西 InputMethodService, 当年可是对着这玩意想吐的,里面有不少 candidate 关键字方法,例如:

public void onStartCandidatesView(EditorInfo info, boolean restarting) {
// Intentionally empty
}

二、InputConnection

当初开发的时候,还做了一点点 candidate view 的开发,当时了解不深,对于这些机制不太清楚。于是在 Google 搜索引擎帮助下联想到了 InputMethodManager 这个管理类,里面确实有 candidate 关键字的方法,但是很遗憾没有找到关于 candidate 任何相关回调接口。于是我不死心继续去找相关的类,又想到之前看过一些 Android 底层源码实现,尤其是 IME 、EditText 两者之间的链接 InputConnection

InputConnection 这个类是连接接 IME 和 EditText 关键接口

The InputConnection interface is the communication channel from an InputMethod back to the application that is receiving its input. It is used to perform such things as reading text around the cursor, committing text to the text box, and sending raw key events to the application.

三、EditText&TextView

不过很遗憾这里面没有 Candidate 关键字的方法,有 commit 概念,进入到了死胡同了,最后回到起点,研究一下 EditText 上屏特色,这个特色就是在输入法进入候选词状态时,EditText 文字会有特殊变化,我看到过的最常见的就是添加 underline 状态,于是去找 EditText 里面去找,找到 onDraw 方法,里面没有搜到 underline 相关,于是去其父类 TextView 去找,倒是找到了一点,最后通过确认,这个东西只是用来处理整体文字的下划线,通过 Paint 也就是常用的画笔来实现的。

四、Editor

又感觉是一条死胡同,于是不死心去一行行看 TextView 的代码,居然发现里面有 Editor 这个类,并且这个类负责了 TextView 的的部分状态的绘制当然也包括下划线。

/**
 * Helper class used by TextView to handle editable text views.
 *
 * @hide
 */
public class Editor {
}

这居然是一个 Helper 类,作为开发不会陌生,就是常用来干脏活累活的内部类,并且为了安全这玩意基本是不对外公开的,以期后期最大化修改,而外部使用者无感,对于 SDK 可拓展来说很重要。于是一行行看看到 SuggestionsPopupWindow 和 SuggestionRangeSpan,这两个类都是私有的私有的类,不过基本确定技术方向,是通过 Spannable 技术来给 EditText 绘制部分下划线的,很符合我看到的表现。

五、Spannable

/**
 * This is the interface for text to which markup objects can be
 * attached and detached.  Not all Spannable classes have mutable text;
 * see {@link Editable} for that.
 */
public interface Spannable
extends Spanned
{
}
/**
* This is the interface for text that has markup objects attached to
* ranges of it. Not all text classes have mutable markup or text;
* see {@link Spannable} for mutable markup and {@link Editable} for
* mutable text.
*/
public interface Spanned
extends CharSequence
{

通过这两个接口描述,直接从 EditText 获取到 Text 就能获取到 CharSequence,从而拿到可能存在的特殊的 Spannable 设定,于是开始验证猜想,在这个方向去写代码

Spanned 接口

/**
* Return an array of the markup objects attached to the specified
* slice of this CharSequence and whose type is the specified type
* or a subclass of it. Specify Object.class for the type if you
* want all the objects regardless of type.
*/
public <T> T[] getSpans(int start, int end, Class<T> type);

可以通过上面接口获取到你想要拿到的类型,通过了解一般 Spannable 都需要从

/**
 * The classes that affect character-level text formatting extend this
 * class.  Most extend its subclass {@link MetricAffectingSpan}, but simple
 * ones may just implement {@link UpdateAppearance}.
 */
public abstract class CharacterStyle {
}

去执行具体的绘制操作。至此基本流程都已经打通,最终在 EditText 接口回调中

public void afterTextChanged(Editable editable)

获取当前的 Text 里面有没有想要找到的 Spannable。从 log 中看到了想要看到的 underline spannable,但是有个缺点就是上完评后,这玩意打出来的还有。这不是坑爹。于是通过延迟一点时间再去获取,就没有了。

整个理论形成闭环,至此完成。当然这只是第一步理论基础,还需要验证各种输入法和手机,还有不同的系统。

六、测试输入法

经过同事介绍和收集,列出以下输入法:

1.搜狗输入法里添加的日语
2.百度输入法里添加的日语
3.QQ输入法里添加的日语
4.百度日文输入法
5.华为(小艺)输入法里添加的日语
6.讯飞输入法里添加的日语
7.百度输入法华为版
8.simeji
9.ATOK
10.谷歌日语输入法
11.蘑菇输入法

手机/系统:OnePlus 8/Android 11
结果:
1.搜狗输入法里添加的日语:候选词状态符合预期
2.百度输入法里添加的日语: 候选词状态符合预期
3.QQ输入法里添加的日语: QQ日语没有候选词,只有平假名和片假名,中文输入有,英文也是直接上屏,没有候选词
4.百度日文输入法:候选词状态符合预期
5.华为(小艺)输入法里添加的日语:候选词状态符合预期,虽然有点难用,候选词没问题–华为手机/8.0
6.讯飞输入法里添加的日语:候选词状态符合预期。他们逻辑候选词不上屏,根本不会触发一般逻辑
7.百度输入法华为版:候选词状态符合预期
8.simeji:就是百度日文输入法,候选词状态符合预期

经过同事介绍和收集,列出以下输入法:

1.搜狗输入法里添加的日语
2.百度输入法里添加的日语
3.QQ输入法里添加的日语
4.百度日文输入法
5.华为(小艺)输入法里添加的日语
6.讯飞输入法里添加的日语
7.百度输入法华为版
8.simeji
9.ATOK
10.谷歌日语输入法

手机/系统:OnePlus 8/Android 11
结果:
1.搜狗输入法里添加的日语:候选词状态符合预期
2.百度输入法里添加的日语: 候选词状态符合预期
3.QQ输入法里添加的日语: QQ日语没有候选词,只有平假名和片假名,中文输入有,英文也是直接上屏,没有候选词
4.百度日文输入法:候选词状态符合预期
5.华为(小艺)输入法里添加的日语:候选词状态符合预期,虽然有点难用,候选词没问题–华为手机/8.0
6.讯飞输入法里添加的日语:候选词状态符合预期。他们逻辑候选词不上屏,根本不会触发一般逻辑
7.百度输入法华为版:候选词状态符合预期
8.simeji:就是百度日文输入法,候选词状态符合预期
9.ATOK:候选词状态符合预期,键盘朴素,操作花哨
10.谷歌日语输入法:候选词状态符合预期

以上收集到的输入法日语状态时,候选栏获取状态正常,之后,后续继续完善更多机型和系统即可。