iOS 深刻理解手勢識別器

1,手勢識別器簡介

在iOS中因爲手勢識別器的存在,咱們能夠很是容易的識別出用戶的交互手勢。 系統提供的手勢識別器以下:html

你們對上面的手勢識別器確定不陌生, 那麼問題來了:ios

1,手勢識別器是怎樣識別出用戶手勢的? 2,如何使用手勢識別器? 3,各手勢識別器狀態,及各狀態間如何進展? 4,多個手勢識別器做用在同一個UIView會發生什麼? 5,如何經過繼承現有手勢識別器來自定義?git

圍繞着這幾個問題, 我們一塊兒深刻的學習一下 GestureRecognizer。github


2, 解釋觸摸

2.1手勢識別和Touch event 的關係:

手勢識別與Touch event 關係

手勢識別經過分析 Touch events 中的數據, 識別出當前當前手指的動做。 成功識別出手勢後,發送 Action message。瞭解手勢識別器如何解釋觸摸仍是有好處的: 你能夠利用Touch event中的數據直接解釋觸摸; 繼承現有的手勢識別器知足特定的識別需求。swift

2.2經過幾段代碼解釋如何解釋觸摸:

自定義一個view, 而後重寫下面的 touches... 方法。 而後將這個view 添加到superView中。 爲了簡單,只考慮一個手指。bash

2.2.1,手指拖動視圖

override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let loc = (touches as NSSet).anyObject()?.locationInView(self.superview)
        let oldP = (touches as NSSet).anyObject()?.previousLocationInView(self.superview)
        let deltaX = (loc?.x)! - (oldP?.x)!
        let deltaY = (loc?.y)! - (oldP?.y)!
        var c = self.center
        c.x += deltaX
        c.y += deltaY
        self.center = c
}
複製代碼

2.2.2, 添加限制, 只能夠水平或者垂直的拖動視圖。

//定義兩個屬性,存儲識別時的狀態
var decided = false  //是否肯定移動方向了
var horiz = false    //是否水平移動

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        decided = false
 }
    
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        if !decided {  //第一次調用, 肯定移動的方向。
            decided = true
            let then = (touches as NSSet).anyObject()?.previousLocationInView(self.superview)
            let now  = (touches as NSSet).anyObject()?.locationInView(self.superview)
            let deltaX = fabs((now?.x)! - (then?.x)!)
            let deltaY = fabs((now?.y)! - (then?.y)!)
            horiz = deltaX>=deltaY
        }
        
        let loc = (touches as NSSet).anyObject()?.locationInView(self.superview)
        let oldP = (touches as NSSet).anyObject()?.previousLocationInView(self.superview)
        let deltaX = (loc?.x)! - (oldP?.x)!
        let deltaY = (loc?.y)! - (oldP?.y)!
        var c = self.center
        if horiz{
            c.x += deltaX
        }else{
            c.y += deltaY
        }
        self.center = c
}

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
複製代碼

2.2.3,區分長按和短按

區分長按和短按主要根據 touchesBegan,touchesEnded之間的時間間隔肯定。 UITouch 對象的timestamp屬性可以獲得點擊時的時間間隔。 代碼以下:app

var time:Double = 0  //記錄時間開始時的時間
  override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        time =  (touches.first?.timestamp)!
    }
    
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
    }
    
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let diff = (touches.first?.timestamp)! - time //計算間隔
        if diff > 0.5{
            print("long")
        }else{
            print("short")
        }
    }
複製代碼

注意: 上面的代碼存在一個問題: 只有當touchesEnded 時才能判斷出是長按仍是短按。事實是: 若是時間超過了0.5, 就能夠作出判斷了,不必再等了。 代碼:ide

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        time =  (touches.first?.timestamp)!
        performSelector("longPress", withObject: nil, afterDelay: 0.5) //延遲執行
    }
    
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        
    }
    
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let diff = (touches.first?.timestamp)! - time
        
        //當時間間隔<=0.5時,判斷爲短按。另外還要取消 performSelector...指定的延遲消息。 否則longPress()總會調用
        if diff <= 0.5{
            print("short")
            NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "longPress", object: nil)
        }
    }
    
    func longPress(){
        print("longPress")
    }
複製代碼

2.2.4,區分單擊和雙擊

上面判斷長按和短按的方法也能夠應用在 單擊和雙擊。雖然 UITouch的tapCount屬性能夠實現這個目標,可是並不能對單擊,雙擊作出不一樣的響應。函數

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let ct = touches.first!.tapCount
        if(ct==2){ //取消點擊一次的延時執行函數
            NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "singTap", object: nil)
        }
    }
    
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
    }
    
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let ct = touches.first!.tapCount
        if ct==1{// 點第一下0.3秒後 不點擊第二下, 就會執行singTap()
            self.performSelector("singTap", withObject: nil, afterDelay: 0.3)
        }
        if ct==2{
            print("doubleTap")
        }
    }
    
    func singTap(){
        print("singTap")
    }
複製代碼

如今能夠實現: 長按,單擊,雙擊,拖動,在水平, 垂直方向移動。 每種模式的代碼有些很差理解了。 那麼將上面這些代碼組和起來,使view能夠同時進行 長按,單擊,雙擊,拖動,在水平, 垂直方向移動。。。。這是至關可怕的。寫出來的代碼徹底不具有可讀性,還有難以理解的邏輯。這就是發明手勢識別器的緣由之一。性能

3,手勢識別器

3.1 手勢識別器類的介紹

  • UIGestureRecognizer : 手勢識別器抽象類
// 系統提供的手勢識別器都是繼承 UIGestureRecognizer類。

- initWithTarget:action: //初始化方法,識別到手勢後發送消息action到target
- addTarget:action:     //添加 target-action
- removeTarget:action:  //移除 target-action
- locationInView:       //觸摸點在指定view上的座標。若是是單點觸摸,就是這個點;若是是多點觸摸,是這幾個點的中點。(重心)
- numberOfTouches       //識別到的手勢對象中包含 UITouch對象的個數
 - locationOfTouch:inView: //指定touch在指定view上的座標。touch經過index指定。
- requireGestureRecognizerToFail: //只有當指定的識別器識別失敗,本身才去識別

state                  //當前的狀態
delegate               //代理 
enabled                //關閉手勢識別器
view                   //手勢識別器所從屬的view
cancelsTouchesInView   // 默認爲YES,這種狀況下當手勢識別器識別到touch以後,會發送touchesCancelled給hit-testview以取消hit-test view對touch的響應,這個時候只有手勢識別器響應touch。當設置成NO時,手勢識別器識別到touch以後不會發送touchesCancelled給hit-test,這個時候手勢識別器和hit-test view均響應touch。
delaysTouchesBegan     //默認是NO,這種狀況下當發生一個touch時,手勢識別器先捕捉到到touch,而後發給hit-testview,二者各自作出響應。若是設置爲YES,手勢識別器在識別的過程當中(注意是識別過程),不會將touch發給hit-test view,即hit-testview不會有任何觸摸事件。只有在識別失敗以後纔會將touch發給hit-testview,這種狀況下hit-test view的響應會延遲約0.15ms。
delaysTouchesEnded     //默認爲YES。這種狀況下發生一個touch時,在手勢識別成功後,發送給touchesCancelled消息給hit-testview,手勢識別失敗時,會延遲大概0.15ms,期間沒有接收到別的touch纔會發送touchesEnded。若是設置爲NO,則不會延遲,即會當即發送touchesEnded以結束當前觸摸。
複製代碼
  • UITapGestureRecognizer
numberOfTapsRequired      //點擊次數
numberOfTouchesRequired   //觸摸點數
複製代碼
  • UIPinchGestureRecognizer
scale    //縮放比例
velocity //縮放速度
複製代碼
  • UIRotationGestureRecognizer
rotation //旋轉角度
velocity //縮放速度
 
複製代碼
  • UISwipeGestureRecognizer
direction                 //容許的方向
numberOfTouchesRequired   //觸摸點數
複製代碼
  • UIPanGestureRecognizer
maximumNumberOfTouches  //最多觸摸點數
minimumNumberOfTouches  //最少觸摸點數
複製代碼
  • UILongPressGestureRecognizer
minimumPressDuration     //手勢識別的最小時長
numberOfTouchesRequired
numberOfTapsRequired
allowableMovement        //補償用戶手指在長期按壓沒法保持平穩的事實。eg:確認長按後能夠進行拖動。
複製代碼

3.2 status

識別器狀態

識別器能夠分爲兩類: 離散的, 連續的。狀態之間的轉換上圖中很明瞭。

3.3 多重手勢識別器

一個視圖上面能夠添加多個手勢識別器。同時當觸摸一個視圖時,不只本視圖的識別器在進行識別操做,視圖層次結構中的父視圖中的識別器也在工做。因此能夠認爲一個視圖被不少手勢識別器包圍。 在這些手勢識別器中, 一旦一個識別器識別到了它的手勢,任何和它的觸摸關聯的其餘手勢識別器強制設置爲失敗狀態。 有時這不是咱們想要的,好比 識別單擊和雙擊。

override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.blueColor()
        
        let t2 = UITapGestureRecognizer(target: self, action: "doubleTap")
        t2.numberOfTapsRequired = 2
        addGestureRecognizer(t2)
        
        let t1 = UITapGestureRecognizer(target: self, action: "singleTap")
        t1.requireGestureRecognizerToFail(t2) //只有t2識別失敗後,t1才進行識別操做
        addGestureRecognizer(t1)
    }

    func doubleTap(){
        print("doubleTap")
    }
    
    func singleTap(){
        print("singleTap")
    }
複製代碼

4 繼承手勢識別器

繼承手勢識別器,就是重寫touches,和相關方法。 在方法中改變手勢狀態信息和一些屬性信息。 通常都會調用父類的touches方法,畢竟父類的touches中作了大量的計算識別工做。 下面經過一個只能識別水平移動的手勢來舉例:

//
//  HerizonalPanGestureRecognizer.swift
//  GestureRecognizerDemo
//
//  Created by 賀俊孟 on 16/5/13.
//  Copyright © 2016年 賀俊孟. All rights reserved.
//  只可以水平拖動

import UIKit
import UIKit.UIGestureRecognizerSubclass  //這個extension可使手勢能夠繼承。不然你無法重寫必要的方法。

class HerizonalPanGestureRecognizer: UIPanGestureRecognizer {
    
    var origLoc:CGPoint! //記錄開始時的座標
    
    override init(target: AnyObject?, action: Selector) {
        super.init(target: target, action: action)
    }
    
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent) {
        origLoc = touches.first?.locationInView(view?.superview)
        super.touchesBegan(touches, withEvent: event)
    }
    
    //全部的識別邏輯都是在這裏進行。第一次調用時狀態是 Possible
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent) {
        if self.state == .Possible{
            let loc:CGPoint! = touches.first?.locationInView(view?.superview)
            let deltaX = fabs(loc.x-origLoc.x)
            let deltaY = fabs(loc.y - origLoc.y)
            
            //開始識別時, 若是豎直移動距離>水平移動距離,直接Failed
            if deltaX <= deltaY{
                state = .Failed
            }
        }
        
        super.touchesMoved(touches, withEvent: event)
    }
    
    //經過重寫。如今只有x 產生偏移。
    override func translationInView(view: UIView?) -> CGPoint {
        var proposedTranslation = super.translationInView(view)
        proposedTranslation.y = 0
        return proposedTranslation
    }
}

// 使用

import UIKit

class BlueView: UIView {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.blueColor()
        
        //添加水平手勢識別器
        let herizonalPan = HerizonalPanGestureRecognizer(target: self, action: "herizonalPan:")
        addGestureRecognizer(herizonalPan)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 進行水平移動
    func herizonalPan(hp:HerizonalPanGestureRecognizer){
        if hp.state == .Began || hp.state == .Changed{
            let delta = hp.translationInView(superview)
            var c = center
            c.x += delta.x
            c.y += delta.y
            center = c
            hp.setTranslation(CGPoint.zero, inView: superview)  //將移動的值清零
        }
    }
}
複製代碼

5 手勢識別器委託

1,  gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool   //在手勢識別器超越Possible狀態前發送給委託。返回No,強制識別器進入Failed狀態。

2,gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool  //當一個手勢識別器打算聲明他識別的手勢時,若是這個手勢會強制使另外一個手勢識別器失敗,失敗的手勢識別器會發送這個消息給他的代理。 返回yes,阻止這個失敗。這時兩個識別器同時工做。

3, gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool //在手勢識別器開始touchesBegan:withEvent:以前調用。 返回false,忽略這個手勢。

4,gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOfGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool //協調兩個同時發生的手勢識別

5,gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailByGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool //識別兩個同時發生的手勢識別。
複製代碼

下面舉個例子: 使用委託消息來組合 UILongPressGestureRecognizer和UIPanGestureRecognizer. 經過長按使一個view抖動, 只有在抖動的過程當中才能夠拖動該view。

//
//  BlueView.swift
//  GestureRecognizerDemo
//
//  Created by 賀俊孟 on 16/5/12.
//  Copyright © 2016年 賀俊孟. All rights reserved.
//

import UIKit

class BlueView: UIView {
    var longP:UILongPressGestureRecognizer!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.blueColor()
        
        //拖動
        let pan = UIPanGestureRecognizer(target: self, action: "panning:")
        pan.delegate = self
        addGestureRecognizer(pan)
        
        //長按
        longP = UILongPressGestureRecognizer(target: self, action: "longPress:")
        longP.delegate = self
        addGestureRecognizer(longP)
    }
    
    func longPress(lp:UILongPressGestureRecognizer){
        if lp.state == .Began{  //開始動畫
            let anim = CABasicAnimation(keyPath: "transform")
            anim.toValue = NSValue(CATransform3D: CATransform3DMakeScale(1.1, 1.1, 1.1))
            anim.fromValue = NSValue(CATransform3D:CATransform3DIdentity)
            anim.repeatCount = HUGE
            anim.autoreverses = true
            lp.view?.layer.addAnimation(anim, forKey: nil)
            return
        }
        
        if lp.state == .Ended || lp.state == .Cancelled{  //結束動畫
            lp.view?.layer.removeAllAnimations()
        }
    }
    
    func panning(p:UIPanGestureRecognizer){
        if p.state == .Began || p.state == .Changed{
            let delta = p.translationInView(superview)
            var c = center
            c.x += delta.x
            c.y += delta.y
            center = c
            p.setTranslation(CGPoint.zero, inView: superview)  //將移動的值清零
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension BlueView:UIGestureRecognizerDelegate{
    
    //長按和拖動能夠同時進行
    func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
    
    //只有在長按狀態時,才能夠進行拖拽
    override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer == longP{
            return true
        }else{
            if longP.state == .Possible || longP.state == .Failed{
                return false
            }
            return true
        }
    }
}

複製代碼

上面的代碼僅僅是爲了說明代理如何使用。要實現上面的效果, 只使用UILongPressGestureRecognizer就夠了:

//長按
        let longP = UILongPressGestureRecognizer(target: self, action: "longPress:")
        addGestureRecognizer(longP)

 func longPress(lp:UILongPressGestureRecognizer){
        
        //開始動畫
        if lp.state == .Began{
            let anim = CABasicAnimation(keyPath: "transform")
            anim.toValue = NSValue(CATransform3D: CATransform3DMakeScale(1.1, 1.1, 1.1))
            anim.fromValue = NSValue(CATransform3D:CATransform3DIdentity)
            anim.repeatCount = HUGE
            anim.autoreverses = true
            lp.view?.layer.addAnimation(anim, forKey: nil)
            
            //獲取觸摸點相對於center的偏移
            origOffset = CGPointMake(CGRectGetMidX(bounds)-lp.locationInView(self).x, CGRectGetMidY(bounds)-lp.locationInView(self).y)
        }
        
        //進行移動
        if lp.state == .Changed{
            var c = lp.locationInView(superview)
            c.x += origOffset.x
            c.y += origOffset.y
            center = c
        }
        
        //結束動畫
        if lp.state == .Ended || lp.state == .Cancelled{
            lp.view?.layer.removeAllAnimations()
        }
    }
複製代碼

Demo在這裏

上面的抖動動畫使用了 Core Animation. 內容挺多的,這裏就不說了。 提供一下學習資料: 官方文檔

OK ,關於手勢識別,就說這麼多。 這裏只是拋磚引玉,爲定製更加複雜的識別說一下思路。若是哪裏存在問題,歡迎提出。 若是感受這篇博文對你有幫助,記得點贊喲!

相關文章
相關標籤/搜索