我们都直到,第三方输入法比如搜狗输入法有个经典的手势操作——在键盘上左右划动即可移动光标。而这个功能我自己也十分的常用,所以,我想要自己来实现它。
首先我想到的就是 UISwipeGestureRecognizer ,不过结果可想而知,划动一次只能移动一格光标,这可不是我想要的。
看来唯一的办法就是用 UIPanGestureRecognizer 来自己实现了。
最初的想法
最一开始,我想到的就是像捕捉拖动一样来落去手指在键盘上的位置,我只需要获取 x 轴,如果是正的就往右,负的就往左。
代码看起来大概像这样:
1 2 3 4 5 6 7 |
@IBAction func moveCurser(sender: UIPanGestureRecognizer) { if sender.translationInView(keyboardView).x < 0 { self.textDocumentProxy.adjustTextPositionByCharacterOffset(-1) } else { self.textDocumentProxy.adjustTextPositionByCharacterOffset(1) } } |
因为我只有按照相对方向移动光标的权限,所以只好如此。当然,缺点就是不论移动的快慢,光标的移动速度是与函数调用的频率一致的。
另一个致命的缺点是如果我手指向一个方向划动比较远,再移动回来——这个过程中光标依然会傻傻地超先前的方向继续移动!
改用加速度而不是坐标作为判断
好吧,这次我改用 UIPanGestureRecognizer 内置的 velocityInView 来获取加速度,因为加速度是有方向的!显然,光标傻傻的朝一个方向移动的问题解决了……可是,它还是依靠系统调用频率来决定移动的速度,这就导致了光标移动太快。
对系统调用频率滤波
好在,系统的调用频率是固定的,这样的话我们可以轻易地实现滤波,加一个属性,用来储存调用的次数,当次数达到了指定的值比如调用了100次,我们才真正执行一次函数:
1 2 3 4 5 6 7 8 9 10 11 |
var recognizerCount = 0 @IBAction func moveCurser(sender: UIPanGestureRecognizer) { recognizerCount += 1 guard recognizerCount > 100 else {return} recognizerCount = 0 if sender.velocityInView(keyboardView).x < 0 { self.textDocumentProxy.adjustTextPositionByCharacterOffset(-1) } else { self.textDocumentProxy.adjustTextPositionByCharacterOffset(1) } } |
根据移动速度动态改变光标移动速度
这下光标的移动终于显得比较合适了,那么接下来的问题就是光标的移动速度改变了,现在移动速度是固定的,无论你是想要快一点移动还是慢一点移动,都不行。要达到这一目的,我们还得在加速度上做文章。
加速度会根据你手指在屏幕上移动的速度而改变,简而言之就是你划动的越快,那么加速度就越大——这么一来我们就有了两个方案:
- 根据加速度改变一次移动光标的字数;
- 根据加速度跟边函数调用的频率。
方案 1 比较简单,把加速度计算到个位数然后直接扔给 adjustTextPositionByCharacterOffset(_:) 就好了,可是这有个问题——定位精确度会下降。这里我选择了更类似搜狗输入法的后者,我依旧每次移动光标 1 个字符的距离,只需要改变函数的调用频率——也就是说,改变我们的滤波算法就好了。
现在的滤波只是单纯的每 100 次取 1 次,那么我们提前获取一次加速度,把它算进去试试,由于加速度有方向——你总不能给滤波器增量减回去吧?我们用 abs 全局数学函数来求一下绝对值,再进行处理,同时由于加速度变化巨大,经过多次测试,缩小10倍比较合适,那么代码看起来就是这个样子的:
1 2 3 4 5 6 7 8 9 10 11 12 |
var recognizerCount = 0 @IBAction func moveCurser(sender: UIPanGestureRecognizer) { let v = Int(sender.velocityInView(keyboardView).x) recognizerCount += Int(abs(v)/10) guard recognizerCount > 100 else {return} recognizerCount = 0 if sender.velocityInView(keyboardView).x < 0 { self.textDocumentProxy.adjustTextPositionByCharacterOffset(-1) } else { self.textDocumentProxy.adjustTextPositionByCharacterOffset(1) } } |
这样,函数实际的调用频率会由滤波器决定,而滤波器的增量又由你实际划动时在屏幕上产生的加速度来决定,从而实现了光标跟手指几乎同步移动的效果。
误操作兼容
最后,我们再来点收尾的工作。由于识别层在键盘的按钮下边,那么我们就要让它们合作无间才行—— UIPanGestureRecognizer 会立即取消 UIButton 的点击操作,也就是说,如果用户打字比较快,而手指又不小心在键盘上搓了一下,那么系统就会识别为微小但加速度很大的划动而不是点击。
所以,第一步,我们需要取消 cancelsTouchesInView ,给它赋值为 false 或者在图形配置里取消这个的勾选。
光这一点还不够,我们还得继续加大相应的阈值,在滤波器前边再获取拖动的绝对值,如果拖动小于一定的距离,就同样不执行函数,给用户留下一定的缓冲空间,只有用户真的想要划动来移动光标的时候才去移动光标,那么,最终的函数应该是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var recognizerCount = 0 @IBAction func moveCurser(sender: UIPanGestureRecognizer) { guard abs(sender.translationInView(keyboardView).x) > 70 else {return} let v = Int(sender.velocityInView(keyboardView).x) recognizerCount += Int(abs(v)/10) guard recognizerCount > 100 else {return} recognizerCount = 0 if sender.velocityInView(keyboardView).x < 0 { self.textDocumentProxy.adjustTextPositionByCharacterOffset(-1) } else { self.textDocumentProxy.adjustTextPositionByCharacterOffset(1) } } |
本文由 落格博客 原创撰写:落格博客 » iOS 自定义键盘 左右划动移动光标 实现
转载请保留出处和原文链接:https://www.logcg.com/archives/1987.html