阅读 2158

iOS 触控事件 UITouch 和手势识别 UIGestureRecognizer

  • 触控事件 UITouch

  • 使用 UIResponder 和响应者链处理Touch事件

响应者链

在 iOS 中事件响应的处理对象都是 UIResponder 对象,它的子类包括 UIView, UIViewController, UIApplication 等。当一个触发事件被 App 检测到时它会找一个合适的 UIResponder 对象做为 firstResponder 第一响应者,事件要么被它处理,要传递给另外一个响应者(或者最后不处理)。而一个事件被一个响应者传递给其它响应者的过程就是 响应者链

app 的响应者链如下所示:

chain

响应者链条的传递规则如下:

  • 对于 UIView 对象。如果他是 UIViewController 的根视图,那么的下一个响应者就是它的 UIViewController,否则就是它的 superView
  • 对于 UIViewController 对象。如果它的 view 是 Window 的根视图,那么它的下一个响应者就是 Window,否则如果是: AViewController.present(BViewController), 那么 BViewController 的下一个响应者就是 AViewController
  • 对于 UIWindow 对象。它的下一个响应者是 UIApplication
  • 对于 UIApplication 对象,它的下一个响应者是 AppDelegate (此时 AppDelegate 必须是 UIResponder 直接子类,不能是UIView 或者 UIViewController 的子类,当然更不可能是第一个响应者对象)

第一个响应者的的检测

当一个触控事件触发时,UIkit 使用 hitTest(_:with:) 返回触发对象是哪一个。

比如如果点击了一个 view 对象,那么这个 view 对象就是 firstResponder,如果点击的是 view 的 subView,那么这个 subView 就是 firstResponder。如果点击了 view 之外的区域,那么这个 view 和它的 subView 都不会是 firstResponder(即使view的subView在view的Frame之外也不是)。

  • UIResponder 的实际代码调用

    // Generally, all responders which do custom touch handling should override all four of these methods.
    // Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
    // touch it is handling (those touches it received in touchesBegan:withEvent:).
    // *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
    // do so is very likely to lead to incorrect behavior or crashes.
    open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)

    open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)

    open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)

    open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
复制代码

在这个4个方法上面有一个基本注释:

如果要处理自己的触控事件,那么就应该4个方法都覆盖。事件处理要么成功,要么失败。

  • touchesBegan

open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)

手指或者触控笔检测到一个触控事件会调用 touchesBegan。默认的实现如下

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
    }
复制代码

它会直接把事件传递给下一个响应者。如果要处理自定义的触控事件就不要调用 super.touchesBegan(touches, with: event)

  • touchesMoved

open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)

和上面的 touchesBegan 一样,如果处理自定义触控就不要调用 super 方法了。当手指之类的触控事件移动的时候会调用这个方法。它会持续更新相同的移动的 UITouch 对象里面的数据。在 UITouch 里面你可以查看触发事件的 window 和当前的 View 相关属性。

你还可以查看 Touch 的当前状况:

    public enum Phase : Int {
    
        case began        // 触摸开始

        case moved        // 接触点移动

        case stationary   // 接触点无移动

        case ended        // 触摸结束

        case cancelled    // 触摸取消
    }
复制代码

一个简单的例子就是你可以实现一个 View 跟随手指移动效果。代码如下(不断更新 View 的中心点):

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let touch = touches.first {
            let current = touch.location(in: self)
            let previous = touch.previousLocation(in: self)
            var currentCenter = center
            currentCenter.x += current.x - previous.x
            currentCenter.y += current.y - previous.y
            self.center = currentCenter
        }
    }
复制代码
  • touchesEnded

手指或者触控笔的触控事件结束

  • touchesCancelled

触控事件取消了。一些系统事件的打断会触发事件取消,比如此时电话来了。

  • 触控事件不被处理情况

  • view.isUserInteractionEnabled = false,关掉了事件响应功能。此时这个 view 和它的所有的 subView 都不会成为响应者

  • view.isHidden = true,view 都隐藏了,当然也就不会处理了

  • view.alpha = 0.0 ~ 0.01 透明度为0或者太小了。

  • 或者不能正常调用 touchesEnded 处理,触控事件被 UIGestureRecognizer 对象截取(下面会说明)。

  • 手势 UIGestureRecognizer

iOS 在很久以前其实也是用 touch 处理相关手势的,后来为了简便开发,所以就推出了 手势识别功能 UIGestureRecognizer

UIGestureRecognizer 是一个抽象类,实际使用的是它的子类(或者自定义子类)

下面是预定义的系统手势:

  • UITapGestureRecognizer (点击)
  • UILongPressGestureRecognizer (长按)
  • UISwipeGestureRecognizer (轻扫)
  • UIPanGestureRecognizer (拖拽)
  • UIPinchGestureRecognizer (捏合,用于缩放)
  • UIRotationGestureRecognizer (旋转)

手势状态如下:

    public enum State : Int {

        case possible // 手势实际还没有识别,但是解析是 touch 事件。 默认状态

        case began

        case changed

        case ended

        case cancelled
        
        case failed
        
    }
复制代码
  • UITapGestureRecognizer

点击一个 view

    private func gestureSetup() {
        let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        colorView.backgroundColor = UIColor.red
        view.addSubview(colorView)

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(colorViewTap(gesture:)))
        colorView.addGestureRecognizer(tapGesture)
    }
    
    @objc private func colorViewTap(gesture: UITapGestureRecognizer) {
        switch gesture.state {
        case .began:
            print("tap began")
        case .changed:
            print("tap changed, move move move ....")
        case .cancelled:
            print("tap cancelled")
        case .ended:
            print("tap ended")
        case .failed:
            print("tap failed, not recognizer")
        default:
            print("tap default, it is possible enum state")
        }
    }
复制代码

正常成功操作输出:

tap ended
复制代码
  • UILongPressGestureRecognizer

长按一个 view

    private func gestureSetup() {
        let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        colorView.backgroundColor = UIColor.red
        view.addSubview(colorView)

        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(colorViewLongPress(gesture:)))
        colorView.addGestureRecognizer(longPress)
    }
    
    
    @objc private func colorViewLongPress(gesture: UILongPressGestureRecognizer) {
        switch gesture.state {
        case .began:
            print("long press began")
        case .changed:
            print("long press changed, move move move ....")
        case .cancelled:
            print("long press cancelled")
        case .ended:
            print("long press ended")
        case .failed:
            print("long press failed, not recognizer")
        default:
            print("long press default, it is possible enum state")
        }
    }
复制代码

正常成功操作输出:

long press began
long press changed, move move move ....
long press changed, move move move ....
...
long press changed, move move move ....
long press changed, move move move ....
long press ended
复制代码
  • UISwipeGestureRecognizer

轻扫手势有一个扫动方向 open var direction: UISwipeGestureRecognizer.Direction,默认是向右扫

    private func gestureSetup() {
        let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        colorView.backgroundColor = UIColor.red
        view.addSubview(colorView)

        let swipePress = UISwipeGestureRecognizer(target: self, action: #selector(colorViewSwipe(gesture:)))
        colorView.addGestureRecognizer(longPress)
    }
    
    
    @objc private func colorViewSwipe(gesture: UISwipeGestureRecognizer) {
        if gesture.direction == .left {
            print("swipe left")
        } else if gesture.direction == .right {
            print("swipe right")
        } else if gesture.direction == .up {
            print("swipe up")
        } else if gesture.direction == .down {
            print("swipe down")
        }
    }

复制代码
  • UIPanGestureRecognizer

实现 view 的拖动

    private func gestureSetup() {
        let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        colorView.backgroundColor = UIColor.red
        view.addSubview(colorView)

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(colorViewPan(gesture:)))
        colorView.addGestureRecognizer(panGesture)
    }
    
    @objc private func colorViewPan(gesture: UIPanGestureRecognizer) {
        if gesture.state == .began {
            print("pan began")
        } else if gesture.state == .changed {
            if let  panView = gesture.view {
                // 手势移动的 x和y值随时间变化的总平移量
                let translation = gesture.translation(in: panView)
                // 移动
                panView.transform = panView.transform.translatedBy(x: translation.x, y: translation.y)
                // 复位,相当于现在是起点
                gesture.setTranslation(.zero, in: panView)
            }
            
        } else if gesture.state == .ended {
            print("pan ended")
        }
    }
复制代码
  • UIPinchGestureRecognizer

实现 view 的缩放

    private func gestureSetup() {
        let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        colorView.backgroundColor = UIColor.red
        view.addSubview(colorView)

        let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(colorViewPinch(gesture:)))
        colorView.addGestureRecognizer(pinchGesture)
    }
    
    @objc private func colorViewPinch(gesture: UIPinchGestureRecognizer) {
        if let pinchView = gesture.view {
            // 缩放
            pinchView.transform = pinchView.transform.scaledBy(x: gesture.scale, y: gesture.scale)
            // 复位
            gesture.scale = 1
        }
    }
复制代码
  • UIRotationGestureRecognizer

实现 view 的旋转

    private func gestureSetup() {
        let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        colorView.backgroundColor = UIColor.red
        view.addSubview(colorView)

        let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(colorViewRotate(gesture:)))
        colorView.addGestureRecognizer(rotateGesture)
    }

    @objc private func colorViewRotate(gesture: UIRotationGestureRecognizer) {
        if let rotateView = gesture.view {
            // 旋转
            rotateView.transform = rotateView.transform.rotated(by: gesture.rotation)
            // 复位
            gesture.rotation = 0
        }
    }
复制代码
  • 无聊的实现所有手势

    private func gestureSetup() {
        let colorView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        colorView.backgroundColor = UIColor.red
        view.addSubview(colorView)

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(colorViewPan(gesture:)))
        let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(colorViewRotate(gesture:)))
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(colorViewTap(gesture:)))
        let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(colorViewLongPress(gesture:)))
        let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(colorViewSwipe(gesture:)))
        let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(colorViewPinch(gesture:)))
        
        // 设置 delegate 实现可识别多手势
        swipeGesture.delegate = self
        pinchGesture.delegate = self
        longPressGesture.delegate = self
        tapGesture.delegate = self
        panGesture.delegate = self
        rotateGesture.delegate = self
        
        colorView.addGestureRecognizer(swipeGesture)
        colorView.addGestureRecognizer(pinchGesture)
        colorView.addGestureRecognizer(longPressGesture)
        colorView.addGestureRecognizer(tapGesture)
        colorView.addGestureRecognizer(rotateGesture)
        colorView.addGestureRecognizer(panGesture)
    }

extension ViewController: UIGestureRecognizerDelegate {
    
    // 设置代理表明识别多个手势(默认 false)
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
    
}

复制代码

关键点是设置手势代理,实现多手势识别方法。

  • 触控事件与手势的一起应用

有下面一个需求 view 有一个 UITapGestureRecognizer, tableView 实现 tableView(didSelectRowAt:) 。

    override func viewDidLoad() {
        super.viewDidLoad()

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAction))
        view.addGestureRecognizer(tapGesture)
    }
    
    @objc private func tapAction() {
        print("tapAction")
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 20
    }

   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = "\(indexPath.row)"
        cell.textLabel?.textColor = .white

        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("didSelectRowAt indexPath = \(indexPath)")
    }
    

复制代码

此时点击 tableView。didSelectRowAt 是不会调用的,调用的是 tapAction 方法。 因为 UIView 和 UITableView 都是 UIResponder 的子类。它们方法触控事件的调用遵循响应者链。 实际默认的调用顺序是:

didSelectRowAt 的正常响应被点击事件切断了,导致点击 tableView 取消了。要是打印输出,大致就是这样:

   xxxxx tableView touchesBegan
   xxxxx view touchesBegan
   xxxxx view tapAction
   xxxxx tableView touchesCancelled
复制代码

解决办法

UIGestureRecognizer 里面有 touch 事件的逻辑处理属性

    open var cancelsTouchesInView: Bool // default is YES. causes touchesCancelled:withEvent: or pressesCancelled:withEvent: to be sent to the view for all touches or presses recognized as part of this gesture immediately before the action method is called.

    open var delaysTouchesBegan: Bool // default is NO.  causes all touch or press events to be delivered to the target view only after this gesture has failed recognition. set to YES to prevent views from processing any touches or presses that may be recognized as part of this gesture

    open var delaysTouchesEnded: Bool // default is YES. causes touchesEnded or pressesEnded events to be delivered to the target view only after this gesture has failed recognition. this ensures that a touch or press that is part of the gesture can be cancelled if the gesture is recognized
复制代码

cancelsTouchesInView:手势识别成功以后是否取消 touch,默认为 true。

在上面的例子中由于既需要手势事件,也需要 touch 事件。所以设置 tapGesture.cancelsTouchesInView = false 就OK了。表明当识别成功手势以后不要取消 touch 事件的传递,此时 tableView 的点击就会正常运行了。

delaysTouchesBegan:是否延迟识别 touch。

默认为 false,表明先触发 touch 事件,然后判断手势是否识别成功。如果设置为 true,则如果此时有手势事件判断成功,手势成功就不会再调用 touchesBegan 事件了。

delaysTouchesEnded :是否延迟识别 touch。

大概逻辑也就是先识别手势,手势失败再正常调用 touchesEnded。

一个实际开发中不会用到的情况是:

    private func buttonActionAndGesture() {
        let button = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        button.setTitle("Test", for: .normal)
        view.addSubview(button)
        
        button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
        
        let buttonTap = UITapGestureRecognizer(target: self, action: #selector(buttonGesture))
        button.addGestureRecognizer(buttonTap)
    }
复制代码

此时会调用 button.addGestureRecognizer 手势事件,不会调用 button.addTarget 点击事件。所以 button.addTarget 应该是 touch 事件的一个解析,此时 touch 事件被 gesture 事件切断了(如有错误,欢迎指正😄)。

文章分类
阅读
文章标签