手动实现一个 GestureRecognizer

作者: 庄msia | 来源:发表于2018-05-16 09:46 被阅读11次

    可能会有人问了:"手动实现一个 GestureRecognizer 有什么用呢?"

    举个栗子,基本上所有地图APP(比如系统的地图)都有单指双击然后滑动实现缩放,个人非常喜欢这个手势,这其实就能用一个自定义的手势实现,非常的实用;又或者可以实现一个手势,自定义一个叫 touchGestureRecognizer 的手势,实现按下去的瞬间就可以触发事件而不是等到手指抬起来才触发事件(tapGestureRecognizer)

    那么怎么实现一个手势呢,这里我们以单指双击滑动的手势为例子

    首先是创建继承 UIGestureRecognizer 的类,这里命名为MDKTapPanGesture

    虽然是 UIGestureRecognizer 的子类,但实际上处理触摸事件的方法是不能直接用到的,这些方法存在于 UIGestureRecognizerSubclass.h 中,这个文件的方法不会被 XCode 提示,而且没有必要调用 super(UIGestureRecognizer) 对应的方法,所以需要手动引入 UIGestureRecognizerSubclass.h

    swift直接

    import UIKit.UIGestureRecognizerSubclass
    

    就好啦

    实现单指双击滑动实现缩放的基本思路

    UIGestureRecognizerSubclass 中的私有方法其实就是我们很熟悉的 touchesBegan 方法, touchesMoved 方法等等,所以双击就是判断是不是第二次触发 touchesBegan ,滑动就是 touchesMoved

    要获取滑动距离我们一般是用 UIPanGestureRecognizer 的 translation 方法,我们可以自己写了一套translation方法,但是在缓存 touchesBegan 的 UITouch 对象时发现,只要手指不离开屏幕,系统调用哪个 touches 方法传入手指对应的 UITouch 其实都是同一个实例,而且在处理 setTranslation 方法时想起,系统没用提供任何修改 UITouch 的方法和初始化方法,用 class-dump 查看 UITouch 的头文件可以看到苹果内部封装了一个初始化方法:

    + (id)_createTouchesWithGSEvent:(struct __GSEvent *)arg1 phase:(long long)arg2 view:(id)arg3;
    

    emmmmmmm...... __GSEvent 是个啥...估计是 UIEvent 的底层实现类型吧,算了算了改这个就有点偏题了,所以我决定把父类改成 UIPanGestureRecognizer ,直接用 UIPanGestureRecognizer 的 touches 方法。至此,思路基本成型

    具体实现过程

    创建公有变量

    open var tapCount = 2//需要按多少次生效
    open var maxTimeTimeInterval:TimeInterval = 0.25//允许的最大按下间隔
    

    创建私有变量

    private var tapingCount = 0//按了多少次
    private var lastTapTime:TimeInterval = 0//上次按下的时间
    

    实现 UIGestureRecognizerSubclass 的方法

    这里就遇到个 swift 的坑了,swift 重载函数需要把函数设置为 open ,但我又不希望这些 touches 方法被外部使用,那怎么办呢?看到这里可能有人会想到,把头文件去掉,把重载符号去掉不就好了,这是个好主意,如果父类还是 UIGestureRecognizer ,手动实现 translation 方法的话,是可以这样做的,但现在要使用 UIPanGestureRecognizer 的 translation 方法,就必须调用 UIPanGestureRecognizer 的 touches 方法才能修改 UIPanGestureRecognizer 的内部信息,所以嘛,这就是 swift 的坑了

    OC 的话,只要在.m 文件内引入 UIGestureRecognizerSubclass.h 就行了,外部还是访问不到 touches 等方法
    不过一般也不会有人像这样弄一个可以到达却不会自动引入的头文件在 swift 中使用,我暂时想不到其他办法禁止了

    继续实现UIGestureRecognizerSubclass.h的方法

    按照上面的思路,需要在touchesBegan中记录点击的次数,如果两次点击的时间间隔太长,那就把tapingCount重置为0,调用 super.touchesBegan是为了使用UIPanGestureRecognizer的translation方法

    open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let now = NSDate().timeIntervalSince1970
        if now - lastTapTime > maxTimeTimeInterval {
            finishTapPress()
        }
    
        tapingCount += 1
        lastTapTime = now
    
        if tapCount == tapingCount {
            super.touchesBegan(touches, with: event!)
        }
    }
    private func finishTapPress() -> () {
        tapingCount = 0
        lastTapTime = 0
    }
    

    这里不需要用到 touchesEnd 方法,但touchesCancelled还是要调用一下的,否则在非正常结束的时候状态可能会出错

    open override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesCancelled(touches, with: event!)
        finishTapPress()
    }
    

    touchesMoved方法需要根据是否是tapCount,如果是就改一下_state 的状态,调用一下 targets 的 action

    open override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        if tapingCount == tapCount {
            super.touchesMoved(touches, with: event!)
        }
    }
    

    调用 super.touchesMoved 的时候会自动通知 target 的 action

    至此一个双击才能 pan 滑动的手势就完成了

    那么另一个问题来了,如果是继承于UIGestureRecognizer的类呢,比如开头提到的自定义一个touchGestureRecognizer,怎么通知 target 的 action呢?

    用 class-dump 或者用 runtime 可以看到, UIGestureRecognizer 有个属性叫 _targets ,用 kvc 拿出来看看可以发现是个装着 UIGestureRecognizerTarget 的 Array

    接着用 class-dump 或者 runtime 查看一下UIGestureRecognizerTarget长什么样

    小 tips,虽然是 swift,但在运行到取出 _targets 的地方打个断点,用 OC 运行 po [_targets[0] _ivarDescription] 和 po [_targets[0] _shortMethodDescription] 就能知道 UIGestureRecognizerTarget 长什么样了

    总之可以发现, UIGestureRecognizerTarget 里有两个属性:target 和 action,有一个调用 target 的 action 的方法:

    _sendActionWithGestureRecognizer:
    

    这就好办了,但具体实现的时候发现,: UIGestureRecognizerTarget 里装的不一定是 target - action ,而是 (target - action) - action ,也就是说 target 装的也是一个UIGestureRecognizerTarget 对象,要做一个判断处理一下,具体如下:

    private func notiTargetPerformAction() -> () {
        guard let _targets = self.value(forKey: "_targets") as? Array<AnyObject> else { return }
    
        for  _targetActionPair in _targets {//UIGestureRecognizerTarget
            var targetActionPair = _targetActionPair
            let selector = NSSelectorFromString("_sendActionWithGestureRecognizer:");
    
            guard let target = targetActionPair.value(forKey: "_target") as? AnyObject else {return}
            if target.isKind(of: NSClassFromString("UIGestureRecognizerTarget")!){
                //判断一下
                targetActionPair = target
            }
    
            if (targetActionPair.responds(to: selector  )) {
                targetActionPair.perform(selector, with: self)
            }
        }
    }
    

    在调用之前,最好先把 state 设置为 began,调用完再设置为 possible:

    state = .began
    notiTargetPerformAction()
    state = .possible

    因为引入了UIGestureRecognizerSubclass,所以 state 变成了可以读写

    如果结束的时候没有把 state 改为UIGestureRecognizerStatePossible,会导致其他手势不能被同时调用,而 UIGestureRecognizer 的state 是只读的,所以这里手动实现了一个_state 属性,或者这里用 kvc 修改 state 也是可以的

    以上代码 都有上传至github

    相关文章

      网友评论

        本文标题:手动实现一个 GestureRecognizer

        本文链接:https://www.haomeiwen.com/subject/oaujdftx.html