iOS中手势(UIGestureRecognizer)的玩法

UIGestureRecognizer

class UIGestureRecognizer : NSObject

UIGestureRecognizer是具体手势识别器的基类,UIGestureRecognizer包括了以下具体的手势识别器

UIGestureRecognizer类中定义了一些通用的行为来配置具体的手势识别器。它能与代理对象(delegate)进行交互(该对象遵循代理协议UIGestureRecognizerDelegate),并进行一些自定义的行为

  • 创建手势识别器
// Valid action method signatures:
// -(void)handleGesture;
// -(void)handleGesture:(UIGestureRecognizer*)gestureRecognizer;
public init(target: Any?, action: Selector?) // designated initializer

init方法是指定构造器,用于创建一个手势对象并添加对应的触发事件。target对象是action实现的容器,action即响应事件

  • 管理手势交互
weak open var delegate: UIGestureRecognizerDelegate? // the gesture recognizer's delegate

UIGestureRecognizerDelegate协议中有一系列手势相关的方法

public protocol UIGestureRecognizerDelegate : NSObjectProtocol {

    @available(iOS 3.2, *)
    optional func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool


    @available(iOS 3.2, *)
    optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                                    shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool

    @available(iOS 7.0, *)
    optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                                    shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool

    @available(iOS 7.0, *)
    optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                                    shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool

    @available(iOS 3.2, *)
    optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                                    shouldReceive touch: UITouch) -> Bool

    @available(iOS 9.0, *)
    optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                                    shouldReceive press: UIPress) -> Bool
}

1、gestureRecognizerShouldBegin

开始进行手势识别时调用的方法,返回NO则结束识别,不再触发手势,用处:可以在控件指定的位置使用手势识别

2、shouldRecognizeSimultaneouslyWith

是否支持多手势触发,返回YES,则可以多个手势一起触发方法,返回NO则为互斥

是否允许多个手势识别器共同识别,一个控件的手势识别后是否阻断手势识别继续向下传播,默认返回NO;
如果为YES,响应者链上层对象触发手势识别后,如果下层对象也添加了手势并成功识别也会继续执行,否则上层对象识别后则不再继续传播

3、shouldRequireFailureOf

这个方法返回YES,第一个手势和第二个互斥时,第一个会失效

4、shouldBeRequiredToFailBy

这个方法返回YES,第一个和第二个互斥时,第二个会失效

5、 shouldReceive touch

手指触摸屏幕后回调的方法,返回NO则不再进行手势识别,方法触发等

此方法在window对象有触摸事件发生时,调用gesture recognizertouchesBegan:withEvent:方法之前调用,如果返回NO,则gesture recognizer不会看到此触摸事件。(默认情况下为YES)

6、shouldReceive press

  • 添加和去除Targets和Actions
open func addTarget(_ target: Any, action: Selector) // 给一个手势对象添加监听事件,可以指定多个target-action
open func removeTarget(_ target: Any?, action: Selector?) // 移除一个手势的监听事件
  • 获取手势的Touches和Location
open func location(in view: UIView?) -> CGPoint 
open var numberOfTouches: Int { get } 
open func location(ofTouch touchIndex: Int, in view: UIView?) -> CGPoint 

location(in view: UIView?)

获取当前触摸在指定视图上的点

numberOfTouches

获取触摸手指数

location(ofTouch touchIndex: Int, in view: UIView?)

多指触摸的触摸点相对于指定视图的位置

  • 获取识别器的状态和视图(Getting the Recognizer’s State and View)
open var state: UIGestureRecognizer.State { get } // the current state of the gesture recognizer
open var isEnabled: Bool // 手势识别是否可用,默认是YES,设置为NO,将不能接受触摸事件
open var view: UIView? { get } // 当前获取手势触摸的View视图,该视图是 addGestureRecognizer:方法添加的

state是当前手势状态,拥有以下值

public enum State : Int {
    case possible // 尚未识别是何种手势操作(但可能已经触发了触摸事件),默认状态
    case began // 手势已经开始,此时已经被识别,但是这个过程中可能发生变化,手势操作尚未完成
    case changed  // 手势状态发生改变
    case ended // 手势识别操作完成(此时已经松开手指)  
    case cancelled // 手势被取消,恢复到默认状态 
    case failed // 手势识别失败,恢复到默认状态
    public static var recognized: UIGestureRecognizer.State { get }
}
  • 取消和延迟触摸(Canceling and Delaying Touches)
open var cancelsTouchesInView: Bool // default is YES. 
open var delaysTouchesBegan: Bool // default is NO. 
open var delaysTouchesEnded: Bool // default is YES

cancelsTouchesInView

表示当手势被识别的时候是否触摸事件应该被传递给视图(可理解为是否取消触摸控件的响应)。

该属性值默认为YES,这种情况下当手势识别器识别到触摸之后,会发送touchesCancelled给触摸到的控件以取消控件viewtouch的响应,这个时候只有手势识别器响应touch,当设置成NO时,手势识别器识别到触摸之后不会发送touchesCancelled给控件,这个时候手势识别器和控件view均响应touch。

注意:手势识别和触摸事件是同时存在的,只是因为touchesCancelled导致触摸事件失效

看个简单例子来理解

func testCancelsTouchesInView() {
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
    panGesture.cancelsTouchesInView = false
    self.view.addGestureRecognizer(panGesture)
}

@objc func handleGesture(_ gesture: UIPanGestureRecognizer) {
    print("handle gesture")
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    print("moved")
}

上面把cancelTouchesInView设置为false时,在屏幕上滑动,会触发滑动手势和触摸两种方式,打印如下:

moved
handle gesture
moved
handle gesture
handle gesture
moved
handle gesture
moved
handle gesture
moved
handle gesture
....

panGesture.cancelsTouchesInView = false注释掉。当手势触发时,将取消触摸消息的触发即默认情况下,打印如下

handle gesture
handle gesture
handle gesture
handle gesture
handle gesture
handle gesture
handle gesture
...

delaysTouchesBegan

上面的例子,我们知道,在一个手势触发之前,是会一并发消息给事件传递链的,delaysTouchesBgan属性用于控制这个消息的传递时机,是否延迟发送触摸事件给触摸到的控件

默认是NO,这种情况下当发生一个触摸时,手势识别器先捕捉到到触摸,然后发给触摸到的控件,两者各自做出响应。如果设置为YES,手势识别器在识别的过程中(注意是识别过程),不会将触摸发给触摸到的控件,即控件不会有任何触摸事件。只有在识别失败之后才会将触摸事件发给触摸到的控件,这种情况下控件view的响应会延迟约0.15ms

delaysTouchesEnded

这个属性设置手势识别结束后,是立刻发送touchesEnded消息到事件传递链或者延迟一个很短的时间后,如果没有接收到新的手势识别任务,再发送

  • 指定手势器之间的依赖
open func require(toFail otherGestureRecognizer: UIGestureRecognizer)

指定一个手势需要另一个手势执行失败才会执行,同时触发多个手势使用其中一个手势的解决办法

有时手势是相关联的,如单机和双击,点击和长按,点下去瞬间可能只会识别到单击无法识别其他,该方法可以指定某一个 手势,即便自己已经满足条件了,也不会立刻触发,会等到该指定的手势确定失败之后才触发

  • 识别不同的手势
@available(iOS 9.0, *)
open var allowedTouchTypes: [NSNumber] // Array of UITouchTypes as NSNumbers.

@available(iOS 9.0, *)
open var allowedPressTypes: [NSNumber] // Array of UIPressTypes as NSNumbers.

@available(iOS 9.2, *)
open var requiresExclusiveTouchType: Bool // defaults to YES
  • 调试手势识别器
@available(iOS 11.0, *)
open var name: String? // name for debugging to appear in logging

状态机(state machine)

手势识别器由状态机来驱动,UIKit通过状态机来确保合理的处理事件。状态机确定了一些重要的行为

当实现自定义手势识别器的时候,必须在合适的事件更新手势的状态。手势状态经常开始于UIGestureRecognizer.State.possible状态,表示准备开始处理事件。对于离散和连续的手势识别器都有着不同的状态路径,一直到 UIGestureRecognizer.State.ended, UIGestureRecognizer.State.failed, or UIGestureRecognizer.State.cancelled。手势识别器最终的状态,将是上面的几种状态之一,后面UIKit将重置手势状态

  • 离散手势识别器状态的过渡

对于离散手势识别器,你可以过渡的状态有两种 UIGestureRecognizer.State.ended 或者 UIGestureRecognizer.State.failed。如下图:

屏幕快照 2019-04-17 下午2.27.27.png

由上图可知,当即将到来的事件成功匹配手势的时候,手势的状态将会过渡到 UIGestureRecognizer.State.ended,并且调用关联的响应事件。当事件不能匹配对应的手势,状态将变为UIGestureRecognizer.State.failed,并不会触发相关的事件

  • 连续手势识别器状态过渡

下图展示了连续手势识别器状态之间的过渡情况,主要分为3个阶段:

1、初始事件将手势状态移动到UIGestureRecognizer.State.began或者UIGestureRecognizer.State.failed
2、进一步的事件将手势状态变为UIGestureRecognizer.State.changed或者UIGestureRecognizer.State.cancelled
3、最终事件手势状态将变为UIGestureRecognizer.State.ended

屏幕快照 2019-04-17 下午2.32.04.png

子类手势识别器

在六种手势识别中,只有一种手势是离散型手势,他就是UITapGestureRecognizer

离散型手势的特点就是:一旦识别就无法取消,而且只会调用一次手势操作事件(初始化手势时指定的回调方法)。

​换句话说其他五种手势是连续型手势,而连续型手势的特点就是:会多次调用手势操作事件,而且在连续手势识别后可以取消手势。下图可以看出两者调用操作事件的次数是不同的:

251202009061611.png

UITapGestureRecognizer

UITapGestureRecognizerUIGestureRecognizer子类,如类名所表达的意思一样,点击手势识别器,专门处理点击事件,该类很简单只有两个属性

@available(iOS 3.2, *)
open class UITapGestureRecognizer : UIGestureRecognizer {
    open var numberOfTapsRequired: Int  // Default is 1. The number of taps required to match
    open var numberOfTouchesRequired: Int // Default is 1. The number of fingers required to match
}
  • numberOfTapsRequired

识别到手势的最少的轻触次数(默认为1)

  • numberOfTouchesRequired

识别到手势的最少的手指的个数(默认为1)

简单使用

创建一个UIViewController,并添加一个测试视图(testView),实现如下

class GestureViewController: UIViewController {

    lazy var testView: UIView = {
        let view = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
        view.backgroundColor = UIColor.red
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.white
        view.addSubview(testView)

        // add code here...
    }
}

testView添加点击手势并实现响应事件,当点击testView的时候修改背景颜色

func addTapGesture() {
   // 创建点击手势对象
   let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
   // 设置点击次数
   tapGesture.numberOfTapsRequired = 1;
   // 设置触摸手指个数
   tapGesture.numberOfTouchesRequired = 1;
   // 为视图添加点击手势
   testView.addGestureRecognizer(tapGesture)
}

@objc func handleTapGesture(_ gesture: UITapGestureRecognizer) {
    testView.backgroundColor = UIColor.yellow
}

界面效果

tap.gif

UIPinchGestureRecognizer

UIPinchGestureRecognizer捏合手势,缩放用

@available(iOS 3.2, *)
open class UIPinchGestureRecognizer : UIGestureRecognizer {
    open var scale: CGFloat // scale relative to the touch points in screen coordinates
    open var velocity: CGFloat { get } // velocity of the pinch in scale/second
}
  • scale 设置缩放比例

  • velocity 获取捏合速度,单位:缩放比/秒

简单使用

func addPinchGesture() {
    let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
    testView.addGestureRecognizer(pinchGesture)
}

@objc func handlePinchGesture(_ gesture: UIPinchGestureRecognizer) {
    gesture.view?.transform = CGAffineTransform(scaleX: gesture.scale, y: gesture.scale)
}

界面效果

pinch.gif

UIRotationGestureRecognizer

UIRotationRecognizer旋转手势

@available(iOS 3.2, *)
open class UIRotationGestureRecognizer : UIGestureRecognizer {
    open var rotation: CGFloat // rotation in radians
    open var velocity: CGFloat { get } // velocity of the pinch in radians/second
}
  • rotation 旋转的角度

  • velocity 旋转速度,单位:度/秒

简单使用

func addRotationGesture() {
    let rotationGesture = UIRotationGestureRecognizer(target: self,
                                                      action: #selector(handleRotationGesture(_:)))

    testView.addGestureRecognizer(rotationGesture)
}

@objc func handleRotationGesture(_ gesture: UIRotationGestureRecognizer) {
     switch gesture.state {
     case .began:
         print("began")
     case .changed:
         print("changed")
     case .ended:
         print("ended")
     default:
         print("default")
     }
     print("rotation: \(gesture.rotation)")
     guard let recognizedView = gesture.view else { return }
     // 设置transform进行旋转
     recognizedView.transform = CGAffineTransform(rotationAngle: gesture.rotation)

     // 方法2 实现旋转
     // recognizedView.transform = recognizedView.transform.rotated(by: gesture.rotation)
     // gesture.rotation = 0 // 重置旋转
}

界面效果

rotation.gif

UISwipeGestureRecognizer

UISwipeRecognizer轻扫手势

@available(iOS 3.2, *)
open class UISwipeGestureRecognizer : UIGestureRecognizer {
    open var numberOfTouchesRequired: Int // default is 1. the number of fingers that must swipe
    open var direction: UISwipeGestureRecognizer.Direction 
}
  • numberOfTouchesRequired

最少触摸手指个数,默认为1

  • direction

设置轻扫手势支持的方向,默认为向右滑,其他值如下结构体

public struct Direction : OptionSet {
    public init(rawValue: UInt)
    public static var right: UISwipeGestureRecognizer.Direction { get } // 向右滑
    public static var left: UISwipeGestureRecognizer.Direction { get } // 向左滑
    public static var up: UISwipeGestureRecognizer.Direction { get } // 向上滑
    public static var down: UISwipeGestureRecognizer.Direction { get } // 向下滑
}

简单使用

同样是上面的例子,我们为testView添加扫动手势

func addSwipeGesture() {
    let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipeGesture(_:)))
     // 向左滑动
    leftSwipe.direction = .left
    testView.addGestureRecognizer(leftSwipe)

    let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipeGesture(_:)))
    // 向右滑动
    rightSwipe.direction = .right
    testView.addGestureRecognizer(rightSwipe)
}

@objc func handleSwipeGesture(_ gesture: UISwipeGestureRecognizer) {
    let direction = gesture.direction
    if direction == .left {
         print("left")
    } else if direction == .right {
         print("right")
    }
}

UIPanGestureRecognizer

UIPanGestureRecognizer

@available(iOS 3.2, *)
open class UIPanGestureRecognizer : UIGestureRecognizer {
    open var minimumNumberOfTouches: Int // default is 1. the minimum number of touches required to match
    open var maximumNumberOfTouches: Int // default is UINT_MAX. the maximum number of touches that can be down
    open func translation(in view: UIView?) -> CGPoint // translation in the coordinate system of the specified view
    open func setTranslation(_ translation: CGPoint, in view: UIView?)
    open func velocity(in view: UIView?) -> CGPoint // velocity of the pan in points/second in the coordinate system of the specified view
}
  • minimumNumberOfTouches

设置触发拖拽最少手指数,默认为1

  • maximumNumberOfTouches

设置触发拖拽最多手指数,默认为 UINT_MAX 无限大

  • translation(in view: UIView?)

获取当前拖拽位置

  • setTranslation(_ translation: CGPoint, in view: UIView?)

设置当前拖拽位置

  • velocity(in view: UIView?)

拖拽速度,单位:像素/秒

简单使用

func addPanGesture() {
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
    testView.addGestureRecognizer(panGesture)
}

@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
    // 获取手指拖拽的时候的值
   let translation = gesture.translation(in: gesture.view)
   // 让当前控件平移
   gesture.view?.transform = CGAffineTransform(translationX: translation.x, y: translation.y)
}

界面效果

pan.gif

UILongGestureRecognizer

UILongGestureRecognizer长按手势

@available(iOS 3.2, *)
open class UILongPressGestureRecognizer : UIGestureRecognizer {
    open var numberOfTapsRequired: Int // Default is 0. The number of full taps required before the press for gesture to be recognized
    open var numberOfTouchesRequired: Int // Default is 1. Number of fingers that must be held down for the gesture to be recognized
    open var minimumPressDuration: TimeInterval // Default is 0.5. Time in seconds the fingers must be held down for the gesture to be recognized
    open var allowableMovement: CGFloat // Default is 10. Maximum movement in pixels allowed before the gesture fails. Once recognized (after minimumPressDuration) there is no limit on finger movement for the remainder of the touch tracking
}
  • numberOfTapsRequired

能识别到手势的最少的轻触次数(默认为1)

  • numberOfTouchesRequired

能识别到手势的最少的手指的个数(默认为1)

  • minimumPressDuration

能识别到长按手势的最短的长按时间,单位:秒,默认为0.5

  • allowableMovement

设置长按时允许移动的最大距离,单位:像素,默认为10像素

简单使用

func addLongPressGesture() {
    let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture(_:)))
    // 长按手势的最小的长按时间2秒,,达到2秒触发action
    longPressGesture.minimumPressDuration = 2
    testView.addGestureRecognizer(longPressGesture)
}

@objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
    switch gesture.state {
    case .began:
        print("began: \(Date())")
    case .changed:
       print("changed")
    case .ended:
       print("ended: \(Date())")
    default:
       print("default")
   }
}

UIScreenEdgePanGestureRecognizer

UIScreenEdgePanGestureRecognizer看起来像pan手势,它是检测屏幕边缘的pan手势的。系统在某些controller转场的时候会使用这个手势。你也可以使用这个手势做其他的事情。创建UIScreenEdgePanGestureRecognizer手势之后需要设置edges属性,确定滑动的方向

@available(iOS 7.0, *)
open class UIScreenEdgePanGestureRecognizer : UIPanGestureRecognizer {
    open var edges: UIRectEdge //< The edges on which this gesture recognizes, relative to the current interface orientation
}

简单使用

func addScreenEdgePanGesture() {
   testView.frame = view.frame

   let edgePanGesture = UIScreenEdgePanGestureRecognizer(target: self,
                                                         action: #selector(handleScreenEdgePanGesture(_:)))
    // 屏幕右侧边缘响应滑动
    edgePanGesture.edges = .right
    view.addGestureRecognizer(edgePanGesture)
}

@objc func handleScreenEdgePanGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
    let translation = gesture.translation(in: view)
    switch gesture.state {
    case .began, .changed:
        testView.center = CGPoint(x: view.center.x + translation.x, y: testView.center.y)
    default:
        testView.center = CGPoint(x: view.center.x, y: testView.center.y)
    }
}

实现效果

screen.gif

自定义手势(Custom Gestures)

当UIKit所提供的手势识别器不能满足我们的需求时候,我们可以自定义手势。自定义手势可以分为两类,离散和连续。为了实现自定义效果,需要子类化UIGestureRecognizer,然后重写 UIGestureRecognizerSubclass.h中的方法

  • 实现离散手势识别器

如果手势涉及了具体的事件模式,考虑实现离散手势识别器。实现如下图的手势,自定义一个打勾(确认)手势

aa0c0736-0d22-4521-ad21-938181d1e784.png

定义手势成功的条件

在我们实现手势代码之前,应该先考虑好什么情况下手势识别成功。对于打勾手势:

  • 仅仅是手指首次触摸屏幕被记录,其余忽略
  • 手指触摸是由左到右
  • 触摸开始是向下,然后改变方向向上移动
  • 向上移动到最后位置应该高于最初的位置

功能实现

enum CheckmarkPhases {
    case notStarted
    case initialPoint
    case downStroke
    case upStroke
}

// 自定义打勾手势识别器
class CheckmarkGestureRecognizer: UIGestureRecognizer {

    // 填充阶段
    var storkPhase: CheckmarkPhases = .notStarted
    // 初始位置(点)
    var initialTouchPoint: CGPoint = CGPoint.zero
    var trackedTouch: UITouch? = nil

    // 在touchesBegan方法中创建初始条件用于手势识别
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesBegan(touches, with: event)

        // 如果初始的触摸事件不为1,那么设置状态为failed
        if touches.count != 1 {
            self.state = .failed
        }

        // Capture the first touch and store some information about it.
        if self.trackedTouch == nil {
            self.trackedTouch = touches.first
            self.storkPhase = .initialPoint
            self.initialTouchPoint = (self.trackedTouch?.location(in: self.view))!
        } else {
            // Ignore all but the first touch.
            for touch in touches {
                if touch != self.trackedTouch {
                    self.ignore(touch, for: event)
                }
            }
        }
    }

    // 当触摸发生改变时,UIKit将调用该方法
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesMoved(touches, with: event)

        let newTouch = touches.first
        // There should be only the first touch. 确保是第一个触摸对象
        guard newTouch == self.trackedTouch else {
            self.state = .failed
            return
        }

        let newPoint = (newTouch?.location(in: self.view))!
        let previousPoint = (newTouch?.previousLocation(in: self.view))!

        // 分阶段处理各种情况,根据移动点的位置进行比较
        if self.storkPhase == .initialPoint {
             // Make sure the initial movement is down and to the right.
             //  确保初始移动是向右下方
            if newPoint.x >= initialTouchPoint.x && newPoint.y >= initialTouchPoint.y {
                self.storkPhase = .downStroke
            } else {
                self.state = .failed
            }

        } else if self.storkPhase == .downStroke { // 向下移动
            // Always keep moving left to right. 由左到右
            if newPoint.x >= previousPoint.x {
                // If the y direction changes, the gesture is moving up again.
                // Otherwise, the down stroke continues.
                if newPoint.y < previousPoint.y { // 开始向上移动
                    self.storkPhase = .upStroke
                }
            } else {
                // If the new x value is to the left, the gesture fails.
                self.state = .failed
            }
        } else if self.storkPhase == .upStroke {
            // If the new x value is to the left, or the new y value
            // changed directions again, the gesture fails.]
            if newPoint.x < previousPoint.x || newPoint.y > previousPoint.y {
                self.state = .failed
            }
        }
    }

    // 滑动(移动)结束,调用该方法,如果手势到目前为止还没有失败,该方法将做最后两点判断
    // 1 移动手势是否向上并且最终位置(点)小于初始位置(点),如果两个条件都满足,方法设置状态为可识别状态
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesEnded(touches, with: event)
        let newTouch = touches.first
        let newPoint = (newTouch?.location(in: self.view))!

        // There should be only the first touch.
        guard newTouch == self.trackedTouch else {
            self.state = .failed
            return
        }

        if self.state == .possible && self.storkPhase == .upStroke && newPoint.y < initialTouchPoint.y {
            self.state = .recognized
        } else {
            self.state = .failed
        }
    }

    // 取消
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesCancelled(touches, with: event)
        self.initialTouchPoint = CGPoint.zero
        self.storkPhase = .notStarted
        self.trackedTouch = nil
        self.state = .cancelled
    }

   // 重置手势识别器
    override func reset() {
        super.reset()
        self.initialTouchPoint = CGPoint.zero
        self.storkPhase = .notStarted
        self.trackedTouch = nil
    }
}

使用自定义手势

func addCheckmarkGesture() {
    let checkmarkGesture = CheckmarkGestureRecognizer(target: self, action: #selector(handleCheckmarkGesture(_:)))
    view.addGestureRecognizer(checkmarkGesture)
}

@objc func handleCheckmarkGesture(_ gesture: CheckmarkGestureRecognizer) {
    print("handle gesture")
}

具体详细解析Implementing a Discrete Gesture Recognizer

  • 实现连续手势识别器

如果手势并不能容易的匹配具体模式,或者当你想使用手势来聚集多个触摸输入事件,可以自定义一个连续手势识别器。

下面同样实现一个连续手势识别器,如下图所示

屏幕快照 2019-04-17 下午3.50.18.png

功能实现

struct StorkeSample {
    let location: CGPoint

    init(location: CGPoint) {
        self.location = location
    }
}

class TouchCapureGesture: UIGestureRecognizer, NSCoding {

    var trackedTouch: UITouch? = nil
    var samples = [StorkeSample]()

    required init?(coder aDecoder: NSCoder) {
        super.init(target: nil, action: nil)
        self.samples = [StorkeSample]()
    }

    func encode(with aCoder: NSCoder) {}

    // 处理触摸事件
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        // The gesture fails immediately if the initial event contains two touches
        if touches.count != 1 {
            self.state = .failed
        }

        // If there is only one touch, the touch object is saved in the trackedTouch property and the custom addSample helper method creates a new StrokeSample struct with the touch data
        if self.trackedTouch == nil {
            if let firstTouch = touches.first {
                self.trackedTouch = firstTouch
                self.addSample(for: firstTouch)
                self.state = .began
            }
        } else {
            // ignore all but the first touch
            for touch in touches {
                if touch != self.trackedTouch {
                    self.ignore(touch, for: event)
                }
            }
        }
    }

    func addSample(for touch: UITouch) {
        let newSample = StorkeSample(location: touch.location(in: self.view))
        self.samples.append(newSample)
    }

    // record each new sample and update the gesture recognizer’s state.
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        self.addSample(for: touches.first!)
        state = .changed
    }

    // Setting the state to UIGestureRecognizer.State.ended is equivalent to setting the state to recognized and results in a call to the gesture recognizer’s action method.
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        self.addSample(for: touches.first!)
        state = .ended
    }

    // Resetting the Gesture Recognizer

    // Always implement the touchesCancelled(_:with:) and [reset()] methods in your gesture recognizers and use them to perform any cleanup
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
        self.samples.removeAll()
        state = .cancelled
    }

    override func reset() {
        self.samples.removeAll()
        self.trackedTouch = nil
    }
}

实现与离散基本差不多,具体解释可以参考Implementing a Continuous Gesture Recognizer

实际场景问题

1、同时识别多个手势

碰到了多个手势的识别问题,需要实现UIGestureRecognizerDelegate协议中的 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool方法来实现多个手势识别器的共同识别。

func addGestures() {
     let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleGestures(_:)))
     panGesture.delegate = self
     view.addGestureRecognizer(panGesture)

     let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleGestures(_:)))
     swipeGesture.delegate = self
     view.addGestureRecognizer(swipeGesture)
}

@objc func handleGestures(_ gesture: UIGestureRecognizer) {
     print("gesture: \(gesture)")
}

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
 }

可以看到拖动的时候,还识别了轻扫手势:

gesture: <UIPanGestureRecognizer: 0x7fc3c853d6f0; state = Began; view = <UIView 0x7fc3c853cbf0>; target= <(action=handleGestures:, target=<UIKitDemo.GestureViewController 0x7fc3c853d250>)>>
gesture: <UISwipeGestureRecognizer: 0x7fc3c853d840; state = Ended; view = <UIView 0x7fc3c853cbf0>; target= <(action=handleGestures:, target=<UIKitDemo.GestureViewController 0x7fc3c853d250>)>; direction = right>

2、UIGestureRecognizer多层视图中的触发问题

场景:在一个Superview中,添加了一个SubviewTap一下Superview,将Subview颜色修改

lazy var testView: UIView = {
    let view = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
    view.backgroundColor = UIColor.red
    return view
}()

override func viewDidLoad() {
    super.viewDidLoad()
    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapEvent))
    view.addGestureRecognizer(tapGesture)
    view.addSubview(testView)
}

@objc func handleTapEvent() {
    print("chang color")
    testView.backgroundColor = UIColor.yellow
}

虽然功能实现很简单,但是当我点击Subview的时候,颜色同样会改变,这其实并不是我所希望的,为了解决这个问题,我们可以实现代理,实现接受触摸方法func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool

设置代理

 tapGesture.delegate = self

实现代理方法

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
     let touchPoint = touch.location(in: view)
     return !testView.frame.contains(touchPoint)
}

可以对每一个tap操作touchPoint进行判断,如果touchPointsubview上,返回false,让UIGestureRecognizer不做任何操作,不触发其action。该方法的默认返回值为true

完全可以对每一个tap操作touchPoint进行判断,如果touchPoint 在subview上,完全可以返回False,让UIGestureRecognizer不做任何操作,不触发其action。该方法的默认返回值为True。

3、UIControl类或者子类控件事件与手势重叠,先响应控件事件

对于 UIButton等控件进行单击操作,如果父视图有点击手势需要识别,依然会按照响应链来处理,先响应这些控件的单击事件。

func buttonAndGestureTestExample() {
    let tapGesturer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
    view.addGestureRecognizer(tapGesturer)

    let button = UIButton(type: .custom)
    button.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
    button.backgroundColor = UIColor.red
    button.addTarget(self, action: #selector(buttonClicked(_:)), for: .touchUpInside)
    view.addSubview(button)
}

@objc func buttonClicked(_ button: UIButton) {
    print("button clicked")
}

@objc func handleTapGesture(_ gesture: UITapGestureRecognizer) {
    print("handleTapGesture")
}

点击按钮,将直接触发按钮事件

button clicked

参考

UIGestureRecognizer
UIGestureRecognizer Tutorial: Getting Started

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341