iOS14開發-觸摸與手勢識別

觸摸

概念

UITouch

用於描述觸摸的窗口、位置、運動和力度。一個手指觸摸屏幕,就會生成一個 UITouch 對象,若是多個手指同時觸摸,就會生成多個 UITouch 對象。swift

  • 屬性

(1)window:觸摸時所處的 UIWindow。 (2)view:觸摸時所處的 UIView。 (3)tapCount:短期內點按屏幕的次數。可據此判斷單擊和雙擊操做。 (4)timestamp:時間戳,單位秒。記錄了觸摸事件產生或變化時的時間。 (5)phase:觸摸事件的週期,即觸摸開始、觸摸點移動、觸摸結束和中途取消。markdown

  • 方法
// 返回一個CGPoint類型的值,表示觸摸在view上的位置。
// 返回的位置是針對view的座標系。
// 調用時傳入的view參數爲空的話,返回的是觸摸點在整個窗口的位置 。
open func location(in view: UIView?) -> CGPoint

// 該方法記錄了前一個座標值,返回值的含義與上面同樣。
open func previousLocation(in view: UIView?) -> CGPoint
複製代碼

UIEvent

一個完整的觸摸操做是一個 UIEvent,它包含一組相關的 UITouch 對象,能夠經過 UIEvent 的allTouches屬性得到 UITouch 的集合。ide

UIResponder

  • 響應者對象。
  • 只有繼承了 UIResponder 的對象才能接收並處理觸摸事件。
  • AppDelegate、UIApplication、UIWindow、UIViewController、UIView 都繼承自 UIResponder,所以它們都是響應者對象,都可以接收並處理觸摸事件。
  • 響應者經過下列幾個方法來響應觸摸事件。
// 手指觸碰屏幕,觸摸開始
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?)
複製代碼

觸摸事件傳遞與響應

當觸摸事件產生之後,App 裏有不少的 UIView 或 UIViewController,到底應該誰去響應這個事件呢?在響應以前,必需要找到那個最合適的對象(最佳響應者),這個過程稱之爲事件傳遞或尋找最佳響應者(Hit-Testing)。ui

事件傳遞

  1. 當 iOS 程序中發生觸摸事件後,系統會將事件加入到 UIApplication 管理的一個任務隊列中。
  2. UIApplication 取出最前面的事件傳遞給 UIWindow。
  3. UIWindow 接收到事件後,首先判斷本身可否響應觸摸事件。若是能,那麼 UIWindow 會從後往前遍歷本身的子 UIView,將事件向下傳遞。
  4. 遍歷每個子 UIView 時,都會重複上面的操做(判斷可否響應觸摸事件,能則繼續遍歷子 UIView,直到找到一個 UIView)直到找到最合適的 UIView。若是沒有找到合適的,那麼事件再也不往下傳遞,而當前 UIView 就是最合適的對象。

兩個方法

尋找最佳響應者的原理是什麼?須要藉助如下兩個方法。spa

// 尋找最佳響應者的核心方法,傳遞事件的橋樑
// 1. 判斷點是否在當前view的內部(即調用第二個方法)
// 2. 若是在(即返回true)則遍歷其子UIView繼續
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
}

// 判斷點是否在這個View的內部
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
}
複製代碼
  • UIApplication 調用 UIWindow 的hitTest方法將觸摸事件傳遞給 UIWindow,若是 UIWindow 可以響應觸摸事件,則調用其子 UIView 的hitTest方法將事件傳遞給其子 UIView,這樣循環尋找與傳遞下去,直到獲取最佳響應者。
  • 經過這兩個方法能夠作不少事情,其中一個經典的案例是自定義中間有凸起按鈕的 UITabBar。此時須要重寫 UITabBar 的point方法,判斷當前觸摸位置是否在中間凸起按鈕的座標範圍內,若是在返回 true。這樣可讓觸摸事件傳遞到凸起按鈕,並讓其成爲最佳響應者。

事件響應

  1. 當找到最合適的響應者以後,響應者對於觸摸事件,有如下 3 種操做:

(1)不攔截,事件會沿着默認的響應鏈自動傳遞。(默認操做) (2)攔截,事件再也不往上傳遞,重寫touchesBegan方法,但不調用父類的touchesBegan方法。 (3)不攔截,事件繼續往上傳遞,重寫touchesBegan方法,並調用父類的touchesBegan方法,即super.touchesBegan(touches, with: event)代理

  1. 響應者對於觸摸事件的響應和傳遞都是在touchesBegan方法中完成的。該方法默認是將事件順着響應者鏈向上傳遞,即將事件交給上一個響應者進行處理。每個響應者對象都有一個next屬性,用來獲取下一個響應者。默認的next對象爲:

(1)UIView:若當前響應者是 UIViewController 的view,則next是 UIViewController,不然上一個響應者是其父 UIView。 (2)UIViewController:若當前響應者是 UIWindow 的rootViewController,則next是 UIWindow;如果被 present 顯示的則nextpresentingViewController。 (3)UIWindow:next爲 UIApplication。 (4)UIApplication:next爲 AppDelegate。 (5)AppDelegate:next爲 nil。code

事件不響應的緣由

  1. 觸摸點不在當前範圍內。
  2. alpha < 0.01,透明度小於 0.01。
  3. hidden = true,隱藏不可見。
  4. userInteractionEnabled = false,不容許交互。

手勢識別

類型

  • UITapGestureRecognizer:輕點手勢識別。
  • UILongPressGestureRecognizer:長按手勢識別。
  • UIPinchGestureRecognizer:捏合手勢識別。
  • UIRotationGestureRecognizer:旋轉手勢識別。
  • UISwipeGestureRecognizer:輕掃手勢識別。
  • UIPanGestureRecognizer:拖動手勢識別。
  • UIScreenEdgePanGestureRecognizer:屏幕邊緣拖動手勢識別。

使用步驟

  1. 建立手勢實例,指定回調方法,當手勢開始,改變、或結束時,回調方法被調用。
  2. 將手勢添加到須要的 UIView 上。每一個手勢只對應一個 UIView,當屏幕觸摸在當前 UIView 裏時,若是手勢和預約的同樣,回調方法就會調用。
  3. 手勢能夠經過 storyboard 或者純代碼使用。
class ViewController: UIViewController {
    @IBOutlet var blueView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // 建立手勢
        let tap = UITapGestureRecognizer(target: self, action: #selector(gesture))
        // UITapGestureRecognizer能夠設置tap次數
        tap.numberOfTapsRequired = 2

        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(gesture))

        let pinch = UIPinchGestureRecognizer(target: self, action: #selector(gesture))

        let rotate = UIRotationGestureRecognizer(target: self, action: #selector(gesture))

        let swipe = UISwipeGestureRecognizer(target: self, action: #selector(gesture))
        // UISwipeGestureRecognizer須要設置direction
        swipe.direction = .right

        let pan = UIPanGestureRecognizer(target: self, action: #selector(gesture))

        let edgePan = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(gesture))
        // UIScreenEdgePanGestureRecognizer須要設置edges
        edgePan.edges = UIRectEdge.all

        // 添加手勢
        blueView.addGestureRecognizer(edgePan)
    }

    @objc func gesture(gestureRecognizer: UIGestureRecognizer) {
        print(#function)
    }
}
複製代碼

代理

class ViewController: UIViewController {    
    @IBOutlet weak var blueView: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(gesture))
        // 設置代理
        gestureRecognizer.delegate = self
        // 添加手勢
        blueView.addGestureRecognizer(gestureRecognizer)
    }
    
    @objc func gesture(gestureRecognizer:UIGestureRecognizer){
        print(#function)
    } 
}

extension ViewController: UIGestureRecognizerDelegate {
    // 手勢識別器是否解釋這次手勢
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer.state == .possible {
            print("手勢開始")          
            return true
        }
        else if gestureRecognizer.state == .cancelled {
            print("手勢結束")
            return true
        }      
        return true
    }
}
複製代碼

注意

  1. 一個手勢只能對應一個 UIView,可是一個 UIView 能夠有多個手勢。
  2. 繼承自 UIControl 的 UIView 均可以經過 Target-Action 方式添加事件,若是同時給它們添加手勢識別, 則 Target-Action 的行爲會失效,由於手勢識別的優先級更高。
相關文章
相關標籤/搜索