去落格博客阅读完整排版的iOS 自定义键盘 左右划动移动光标 实现
我们都直到,第三方输入法比如搜狗输入法有个经典的手势操作——在键盘上左右划动即可移动光标。而这个功能我自己也十分的常用,所以,我想要自己来实现它。
首先我想到的就是
UISwipeGestureRecognizer,不过结果可想而知,划动一次只能移动一格光标,这可不是我想要的。
看来唯一的办法就是用
UIPanGestureRecognizer来自己实现了。
最一开始,我想到的就是像捕捉拖动一样来落去手指在键盘上的位置,我只需要获取 x 轴,如果是正的就往右,负的就往左。
代码看起来大概像这样:
@IBAction func moveCurser(sender: UIPanGestureRecognizer) { if sender.translationInView(keyboardView).x < 0 { self.textDocumentProxy.adjustTextPositionByCharacterOffset(-1) } else { self.textDocumentProxy.adjustTextPositionByCharacterOffset(1) } }
因为我只有按照相对方向移动光标的权限,所以只好如此。当然,缺点就是不论移动的快慢,光标的移动速度是与函数调用的频率一致的。
另一个致命的缺点是如果我手指向一个方向划动比较远,再移动回来——这个过程中光标依然会傻傻地超先前的方向继续移动!
好吧,这次我改用
UIPanGestureRecognizer内置的
velocityInView来获取加速度,因为加速度是有方向的!显然,光标傻傻的朝一个方向移动的问题解决了……可是,它还是依靠系统调用频率来决定移动的速度,这就导致了光标移动太快。
好在,系统的调用频率是固定的,这样的话我们可以轻易地实现滤波,加一个属性,用来储存调用的次数,当次数达到了指定的值比如调用了100次,我们才真正执行一次函数:
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倍比较合适,那么代码看起来就是这个样子的:
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 或者在图形配置里取消这个的勾选。
光这一点还不够,我们还得继续加大相应的阈值,在滤波器前边再获取拖动的绝对值,如果拖动小于一定的距离,就同样不执行函数,给用户留下一定的缓冲空间,只有用户真的想要划动来移动光标的时候才去移动光标,那么,最终的函数应该是这样的:
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) } }