落格输入法是如何处理按键消息的

要做一款移动设备上的软键盘,那么怎么处理用户的点击位置,就是你遇到的第一个难题,在这个问题上,我也走了很长的路。

我把落格输入法开发以来的触控逻辑大致分类为三个阶段,现在分别来讲讲设计思路,希望能够对你有所帮助。

第一代触控引擎

显然,对于一个初学者来说,没什么比系统控件更好用的了,功能全,速度也不慢,业务逻辑完善,所以,落格输入法的第一代消息处理就是用的 UIButton 的 TouchUpInside 消息。因为一开始我甚至用的是 xib 构建键盘布局,所以直接使用了 @IBAction func buttonTouchUpInside(_ sender: UIButton)  这样的声明,你一看就懂了,对吧?处理很方便,用户点击了哪个 Button,那么传进来的就是哪个,不需要做额外的判断,系统都帮你搞定了一切。但很快就遇到了第一个问题——按下按钮后程序总要执行一会,于是 UI 就会卡顿,很明显从按下按键到候选出现,会有延迟。

为了避免输入法业务逻辑干扰(实际上是阻挡)UI 更新,我改为在后台处理进一步的消息,实际上你也应该总是这么做——永远不要在主线程处理业务逻辑。

但这样又引入了另一个问题,当用户点击按键间隔太短速度太快时,按键处理的顺序会错乱,于是,我把异步改为同步,这样业务逻辑还是在后台线程处理,但会严格按照调用的顺序依次执行(这确保了用户按键以实际顺序进行处理)

注意第一行,async → sync。

第二代触控引擎

第一代引擎正常工作了很久,但最终还是遇到了另一个问题:当用户点击更加快的时候,某些后台逻辑处理不正常。

这实际上是由于 UIButton 自身触控处理机制冲突造成的,当用户点击屏幕键盘速度太快,实际上短时间内同时按下了两个按钮,此时主线程自身是互相阻止的,只有当用户两个手指都离开屏幕,消息才会发送,即 @IBAction func buttonTouchUpInside(_ sender: UIButton) 被立即调用两次。在极短时间内,两次连续调用,虽然是有严格顺序的,但每个按键消息都会处理候选栏的刷新,这就需要异步更改 UI,这就导致了一些处理逻辑异常——在 UI 刷新完成之前,下一个消息已经开始执行。

解决的思路有两个,要么把业务逻辑的一些判断改为和 UI 不关联,要么想办法让系统能够处理用户同时按下多个 UIButton 的情况。 ——显然,应该从后者入手,于是,我在开发 落格输入法 X 时做了一个按键缓存机制,它不再使用 TouchUpInside ,而是 TouchDown 和 TouchUp :

注意 buttonTouchUpInside 就是上一代的处理逻辑,并没有什么变化,这么写是为了让你明白我的变更思路,这样当用户按下一个键,那么就立即缓存它,当用户再按下一个键,如果已经有缓存了,那么就处理它,并将新的加入缓存;当用户抬起手指,那么就处理缓存的那个按键。如此一来,所有的按键都会得到处理,并且不会被 TouchUpInside 这个信号阻拦——因为我已经不再使用它了。

由于更改了信号获取源头(从 TouchUpInside 改为 TouchDown + TouchUp ),对用户来说“手感”变化很大。

第三代触控引擎

第二代实际上已经工作的很好,但在 落格输入法 X 上架之前,我们内部测试就发现了另一个问题——实际上并不能说是新发现的,因为它一直都存在,那就是“q”和“p”的问题。在新的 iOS 系统当中,似乎是为了避免和系统手势冲突,iOS 为屏幕边缘的 5 个像素做了保留处理,当你点击到屏幕边缘的时候(即按按键“q”或者“p”时稍微靠边了点), TouchDown 这个消息是不会立即被触发的。

它会被延迟到你抬起手指的那一刻,然后和 TouchUp 一起发送给键盘。这就导致了用户正常打字的时候,遇到这两个位置,总是会感觉“卡顿”了一下,因为视觉和声音反馈上,确实是延迟到你抬起手指的那一刻而不是按下就立即触发。

如果是在 app 当中,你可以这样做:

但显然,在键盘中这个代码并不生效。总之,为了让即将上架的 落格输入法 X 更加具有竞争力,我只好硬着头皮继续想办法。

我在尝试了很多方案之后,我终于找到了一个能获取信号的控件—— UITapGestureRecognizer 。

不是用它来进行标准识别点击——这同样是没用的,必须使用它的 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool 这个代理方法,只有它能够越过系统屏蔽,正确获得 TouchDown 调用,而不是被延迟到用户抬起手指的那一刻。

对应地,我又使用 UIView 本身的 func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) 来获取 TouchUp 行为,如此一来,就可以参考上一代引擎的逻辑实现了:

其他代码略过不表,这样操作之后,把这个 TouchLayer 覆盖在键盘上方即可——当然,你也可以在你的 Button 子类里进行这样的操作,然后单独处理。这里我则盖在键盘上方,进行统一处理了。

为此,我不得不对键盘的业务逻辑进行了一番调整和重构……直到去年年底,我写了一篇文章 落格输入法 X 是如何处理屏幕边缘延迟问题的

总之,这就是现在线上版本 落格输入法 X 在使用的触控逻辑,目前来看,一切良好。如果说缺点,大概就是从二代升级三代代价实在是太大了,这几乎是 落格输入法 X 与 经典版 的重点区别之一了。

 

本文由 落格博客 原创撰写:落格博客 » 落格输入法是如何处理按键消息的

转载请保留出处和原文链接:https://www.logcg.com/archives/3205.html

发布者:落格博客

如非声明,本人所著文章均为原创手打,转载请注明本页面链接和我的名字。

加入对话

1条评论

电子邮件地址不会被公开。 必填项已用*标注

  1. 原来以前经常切窗口的时候会在输入框留下一个q字母是这个原因…
    不过这样看来,iOS原生输入法没有UX方面的问题,用的API和留给第三方的输入API很可能不是同一套?略坑
    第二代那套逻辑有点像做ACT游戏处理输入缓冲的思路