iOS 如何精准放置输入框光标
status
category
date
summary
slug
icon
tags
password
细节满满的 QQ 聊天输入框
从 iOS 7.0 开始,iOS 系统默认会对输入内容进行单词识别,在输入框中通过点击来调整光标位置时,系统会自动将光标放置在单词之前或之后。因此想要将光标放置在单词中间时,需要长按并拖拽光标来完成,系统输入法也提供了长按空格的方式,还有部分第三方输入法提供了键盘滑动等解决方案。但默认情况下,光标是无法通过点击来放置到单词中间的,大多数 App 也没有做处理。下图分别为微信聊天输入框和 QQ 聊天输入框:


这在大多数场景下并不是问题,但是它引起的并发症就非常值得关注,很多 App 也并没有解决。例如我最近遇到的,如果行首的字符是诸如“@”之类的符号,那么想通过点击将光标放置到行首就非常困难。原因并不清楚,但个人猜测是系统将该符号与行首符号或换行符号识别为一个单词整体,导致光标无法放置到单词中间。
为了验证这个猜想,我测试了包括系统备忘录在内的几个常用 App,发现只有 QQ 聊天输入框和微博发布输入框做了特殊处理。这两个 App 可以屏蔽单词识别,通过点击将光标放置到任意位置,包括单词中间和以符号开头的行首,于是猜想成立。
有关 UITextInput
在输入框控件(实现 UITextInput 协议的控件)中,具体的位置信息 UITextPosition 进行描述,它没有暴露任何属性或者方法:
但是 UITextFieldInput 提供了一个方法,用于计算两个 UITextFieldPosition 的距离,并用 NSInteger 输出:
位置范围则通过 UITextRange 描述:
因此 UITextPosition 可以看做是 NSInteger 和 UITextRange 的中间桥梁,光标的位置就是通过修改 UITextRange 的 selectedTextRange 属性进行设置的,要注意区别以下几个相似的属性:
selectedTextRange
:UITextInput 协议的属性,UITextRange 类型,描述了光标选中的文字范围,任何输入都是直接在被选中的文字范围上操作的,例如删除实际上就是输入了空字符“”。如果没有选中多个字(字是输入框计算光标位置的最小单位,单词由字组成),那么长度为零,start 和 end 就可以同时表示光标的位置。
markedTextRange
:UITextInput 协议的属性,UITextRange 类型,用于描述待进一步操作的文字范围,待进一步操作指的是某些特殊语言输入时需要多阶段的操作,例如英语的输入只需要输入字母即可,但中文输入的第一步是输入拼音,第二步才会选择真正输入到文本框的汉字。在输入拼音的阶段,就可以看到输入框中的拼音,这部分拼音的范围就用这个属性来表示。
selectedRange
:UITextView 特有的属性,NSRange 类型,在 UITextView 中作用同selectedTextRange
。实际操作时修改这两个属性都可以完成需求,但本文以selectedTextRange
为例,一是方便从 UITextPosition 转换,二是使用 UITextInput 协议的属性具有更好的普适性,例如 UITextField 遵循了协议,但并没有selectedRange
属性。
How
由于在点击移动光标时,系统自动“帮助”我们将光标移动到了最近的单词之前或之后,所以解决思路就是先拿到点击坐标,然后计算出离它最近的位置,最后手动把光标移动过去。幸运的是系统为我们提供了一个方法,可以接受一个 CGPoint 参数,然后返回离它最近的 UITextPosition,于是我们可以通过重写 UITextView 的 hitTest 拿到这个坐标点,并完成计算。
特别注意:
hitTest
传递的参数是针对控件的,但是closestPositionToPoint
接受的参数是针对内容 content 的。也就是说,point.y
的值不会超过控件高度,这样就导致了一个问题,也是我自己测试时遗漏的 case:文字高度超过控件高度时,closestPositionToPoint
返回的位置最多只能到第一屏的最后一行。例如这个 TextView 的高度限制最多只能展示 10 行文字,那么永远不可能通过点击将光标移到 11 行及之后,因此需要根据 contentOffset 进行调整:When
最理想的方案是只监听输入框光标的移动,然后在这个过程中将光标放到为我们希望的位置。除了通过 KVO 监听 selectedTextRange 属性之外,UITextViewDelegate 提供了一个在 selectedTextRange 改变之后执行的回调。在这个回调中将 selectedTextRange 再次修改为我们想要的值,就可以移动光标:
这样简单的做法会有一些问题:
- 多选失效,因为多选时长度被修改,但是在回调中又被我们强制修改为 0
- 输入时光标位置不更新,因为光标在输入时会自动更新位置,但在回调中又被我们修改回原来的值
因此点击移动光标的回调需要一个严格的执行条件,以保证 selectedTextRange 属性因为其他操作改变时,可以达到预期效果而不被我们影响。我的做法是加入了一个 BOOL 记录状态,在输入和点击时更新这个状态。另外为了不影响多选功能,可以仅在选择长度为 0 时移动光标。
完整代码
特别鸣谢
bers @ PCG 手机QQ团队
Loading...