UIGestureRecognizer
class UIGestureRecognizer : NSObject
UIGestureRecognizer
是具体手势识别器的基类,UIGestureRecognizer
包括了以下具体的手势识别器
UITapRecognizer
UIPinchRecognizer
UIRotationRecognizer
UISwipeRecognizer
UIPanRecognizer
UIScreenPanGestureRecognizer
UILongGestureRecognizer
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 recognizer
的touchesBegan: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
给触摸到的控件以取消控件view
对touch
的响应,这个时候只有手势识别器响应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.began
状态 - 是否离散手势识别器被允许进入
UIGestureRecognizer.State.ended
状态 - 什么时候触发对应的响应事件
当实现自定义手势识别器的时候,必须在合适的事件更新手势的状态。手势状态经常开始于UIGestureRecognizer.State.possible
状态,表示准备开始处理事件。对于离散和连续的手势识别器都有着不同的状态路径,一直到 UIGestureRecognizer.State.ended
, UIGestureRecognizer.State.failed
, or UIGestureRecognizer.State.cancelled
。手势识别器最终的状态,将是上面的几种状态之一,后面UIKit
将重置手势状态
- 离散手势识别器状态的过渡
对于离散手势识别器,你可以过渡的状态有两种 UIGestureRecognizer.State.ended
或者 UIGestureRecognizer.State.failed
。如下图:
由上图可知,当即将到来的事件成功匹配手势的时候,手势的状态将会过渡到 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
子类手势识别器
在六种手势识别中,只有一种手势是离散型手势,他就是UITapGestureRecognizer
。
离散型手势的特点就是:一旦识别就无法取消,而且只会调用一次手势操作事件(初始化手势时指定的回调方法)。
换句话说其他五种手势是连续型手势,而连续型手势的特点就是:会多次调用手势操作事件,而且在连续手势识别后可以取消手势。下图可以看出两者调用操作事件的次数是不同的:
UITapGestureRecognizer
UITapGestureRecognizer
是UIGestureRecognizer
子类,如类名所表达的意思一样,点击手势识别器,专门处理点击事件,该类很简单只有两个属性
@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
}
界面效果
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)
}
界面效果
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 // 重置旋转
}
界面效果
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)
}
界面效果
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)
}
}
实现效果
自定义手势(Custom Gestures)
当UIKit所提供的手势识别器不能满足我们的需求时候,我们可以自定义手势。自定义手势可以分为两类,离散和连续。为了实现自定义效果,需要子类化UIGestureRecognizer
,然后重写 UIGestureRecognizerSubclass.h
中的方法
- 实现离散手势识别器
如果手势涉及了具体的事件模式,考虑实现离散手势识别器。实现如下图的手势,自定义一个打勾(确认)手势
定义手势成功的条件
在我们实现手势代码之前,应该先考虑好什么情况下手势识别成功。对于打勾手势:
- 仅仅是手指首次触摸屏幕被记录,其余忽略
- 手指触摸是由左到右
- 触摸开始是向下,然后改变方向向上移动
- 向上移动到最后位置应该高于最初的位置
功能实现
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
- 实现连续手势识别器
如果手势并不能容易的匹配具体模式,或者当你想使用手势来聚集多个触摸输入事件,可以自定义一个连续手势识别器。
下面同样实现一个连续手势识别器,如下图所示
功能实现
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
中,添加了一个Subview
。Tap
一下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
进行判断,如果touchPoint
在subview
上,返回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