iOS 觸控事件 UITouch 和手勢識別 UIGestureRecognizer

  • 觸控事件 UITouch

  • 使用 UIResponder 和響應者鏈處理Touch事件

響應者鏈bash

在 iOS 中事件響應的處理對象都是 UIResponder 對象,它的子類包括 UIView, UIViewController, UIApplication 等。當一個觸發事件被 App 檢測到時它會找一個合適的 UIResponder 對象作爲 firstResponder 第一響應者,事件要麼被它處理,要傳遞給另一個響應者(或者最後不處理)。而一個事件被一個響應者傳遞給其它響應者的過程就是 響應者鏈app

app 的響應者鏈以下所示:ide

chain

響應者鏈條的傳遞規則以下:ui

  • 對於 UIView 對象。若是他是 UIViewController 的根視圖,那麼的下一個響應者就是它的 UIViewController,不然就是它的 superView
  • 對於 UIViewController 對象。若是它的 view 是 Window 的根視圖,那麼它的下一個響應者就是 Window,不然若是是: AViewController.present(BViewController), 那麼 BViewController 的下一個響應者就是 AViewController
  • 對於 UIWindow 對象。它的下一個響應者是 UIApplication
  • 對於 UIApplication 對象,它的下一個響應者是 AppDelegate (此時 AppDelegate 必須是 UIResponder 直接子類,不能是UIView 或者 UIViewController 的子類,固然更不多是第一個響應者對象)

第一個響應者的的檢測this

當一個觸控事件觸發時,UIkit 使用 hitTest(_:with:) 返回觸發對象是哪個。spa

好比若是點擊了一個 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個方法上面有一個基本註釋:code

若是要處理本身的觸控事件,那麼就應該4個方法都覆蓋。事件處理要麼成功,要麼失敗。orm

  • touchesBegan

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

手指或者觸控筆檢測到一個觸控事件會調用 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 事件切斷了(若有錯誤,歡迎指正😄)。

相關文章
相關標籤/搜索