iOS触摸事件、手势、UIControl二

一、Gesture Recognizer
       4.1、当前view添加手势
       4.2、当前view和superview同时添加手势
二、UIControl
       5.1、UIControl重写touch方法
       5.2、UIControl添加手势
       5.3、UIControl的父视图添加手势


占位图.jpg

一、Gesture Recognizer

       这里不着重讲解手势,如果想了解可以参考官方文档,这里主要分析手势和touch事件响应先后的问题。

       手势识别器会将底层的事件处理代码转换为更高级别的动作。它们是可以附加到视图上的对象,使得视图能够像控件一样对用户的操作做出响应。
Gesture recognizers convert low-level event handling code into higher-level actions. They are objects that you attach to a view, which allows the view to respond to actions the way a control does

       手势识别器是处理视图上触摸事件和按压事件最简单的方式。可以将一个或多个手势附加在视图上。 手势识别器包含了视图上必要的处理逻辑,当检测到一个手势时,手势会派发给指定的目标,这个目标可以是view controller ,view 或app中其它的对象。

手势的模型图:
手势.png
手势有离散和持续两种
image.png
手势识别器工作原理:

手势识别器的工作原理其实就是一个状态机。手势识别器从预设的状态中从一个状态转换到另一个状态。对于每一个状态,只要满足合适的条件就可以转换到下一步众多状态中的一个。根据是否手势是否是离散的,状态机有下面两种:


状态机.png

上图解释如下:

  • 手势识别器都是以Possible (UIGestureRecognizerStatePossible)状态为起始。
  • 手势识别器分析接收到的触摸事件,如果失败状态变为Failed(UIGestureRecognizerStateFailed)
  • 对于离散的手势如果识别成功状态变为Recognized (UIGestureRecognizerStateRecognized),至此识别结束
  • 对于连续的手势,当第一次被识别的时候状态从Possible状态转换到Began (UIGestureRecognizerStateBegan),然后从Began状态转换到Changed (UIGestureRecognizerStateChanged),当手势发生的时候开持续的从Changed状态转换到Changed状态。
  • 如果连续的手势不再满足相应的模式,那么它会 从Changed 转换到 Canceled (UIGestureRecognizerStateCancelled)状态,比如在触摸的时候,突然来了一个电话,这里就会进入 Canceled.
  • 如果连续的手势识别成功就会进入 Recognized状态,并重置状态机到Possible状态
手势和触摸事件的优先级:

Gesture Recognizers Get the First Opportunity to Recognize a Touch

手势优先获取事件

A window delays the delivery of touch objects to the view so that the gesture recognizer can analyze the touch first. During the delay, if the gesture recognizer recognizes a touch gesture, then the window never delivers the touch object to the view, and also cancels any touch objects it previously sent to the view that were part of that recognized sequence.
Window 对象会延迟将“触摸对”象发送给视图,从而让手势识别器最先对“触摸” 进行分析处理。在延迟期间,如果识别器成功识别触摸手势,window 对象就不会再将“触摸对象”传递给视图对象,并取消本应在手势序列中而且可以接受触摸事件的触摸对象

也就是说view上触摸事件的优先级要比view上的手势的优先级低。再放一张图:


图片.png

1.1、手势的基本操作

添加一个点击手势

extension ViewController {
    func addTapGesture() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(gesture:)))
        gestureView.addGestureRecognizer(tap)
    }
    
    @objc private func tapAction(gesture: UITapGestureRecognizer) {
        print("tap")
    }
}
  • 🔥注: 如果同一个视图添加多个相同的手势,经测试最后一个生效

1.2、view和superview同时添加手势

image.png
extension ViewController {
    func addTapGesture() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(gesture:)))
        gestureView.addGestureRecognizer(tap)
    }
    
    @objc private func tapAction(gesture: UITapGestureRecognizer) {
        print("tap")
    }
    
    func addSubViewTapGesture() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(subViewtapAction(gesture:)))
        subView.addGestureRecognizer(tap)
    }
    
    @objc private func subViewtapAction(gesture: UITapGestureRecognizer) {
        print("subView tap")
    }
}

🔥结论

  • 1、view和superview同时添加手势,点击子view时父视图的手势不生效
  • 2、view和superview同时添加不同的手势有时是不冲突的,比如tap和swipe手势

1.3、验证手势和触摸事件的优先级

class TouchView: UIView {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addTapGesture()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        addTapGesture()
    }
    
    func addTapGesture() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(gesture:)))
        addGestureRecognizer(tap)
    }
    
    @objc private func tapAction(gesture: UITapGestureRecognizer) {
        print("tap gesture")
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("Touch事件:Began")
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("Touch事件:End")
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("Touch事件:Cancelled")
    }
}

点击之后打印如下

Touch事件:Began
tap gesture
Touch事件:Cancelled

🔥结论: 手势在识别成功之后取消了触摸事件

1.4、两个不同手势设置优先级Pan和Swipe

先补充一下,swipe只能从左到右(手机系统是阿拉伯语另说,独自验证),从其它方向不生效,pan是可以从任何方向, 设置只有在swipe识别失败的时候,才让pan进行识别
,主要是使用 open func require(toFail otherGestureRecognizer: UIGestureRecognizer)

class TwoGestureView: UIView {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addGestures()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        addGestures()
    }
    
    func addGestures() {
        let gesture1 = UIPanGestureRecognizer(target: self, action: #selector(panAction(gesture:)))
        addGestureRecognizer(gesture1)
        let gesture2 = UISwipeGestureRecognizer(target: self, action: #selector(swipeAction(gesture:)))
        addGestureRecognizer(gesture2)
        gesture1.require(toFail: gesture2)
    }
    
    @objc private func panAction(gesture: UITapGestureRecognizer) {
        print("pan gesture")
    }
    
    @objc private func swipeAction(gesture: UITapGestureRecognizer) {
        print("swipe gesture")
    }
}

当然也可以使用delete中的@available(iOS 7.0, *) optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool

class TwoGestureView: UIView, UIGestureRecognizerDelegate {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addTapGesture()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        addTapGesture()
    }
    
    func addTapGesture() {
        let gesture1 = UIPanGestureRecognizer(target: self, action: #selector(panAction(gesture:)))
        addGestureRecognizer(gesture1)
        gesture1.delegate = self
        let gesture2 = UISwipeGestureRecognizer(target: self, action: #selector(swipeAction(gesture:)))
        addGestureRecognizer(gesture2)
        gesture1.shouldRequireFailure(of: gesture2)
    }
    
    @objc private func panAction(gesture: UITapGestureRecognizer) {
        print("pan gesture")
    }
    
    @objc private func swipeAction(gesture: UITapGestureRecognizer) {
        print("swipe gesture")
    }

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

🔥结论

  • 优先使用方法而不使用delegate,除非复杂的场景(比如一个view上有不同的操作)
  • delete方法即使返回true也可能失败,这由系统决定

1.5、取消手势

4.1、view添加手势

图层.png这个图片中,给ViewD添加一个手势,当前触摸ViewD的时候打印如下数据:

Window范围内查找

/*
省略无关代码
*/
Window-->hitTest:withEvent:-->FirstResponder:<ViewD: 0x105213720;***>
/**下面是添加手势之后打印的数据**/
ViewD -- touchesBegan
ViewD -- tapGesture
ViewD -- touchesCancelled
  • 注: 如果同一个视图如果添加多个相同的手势,经测试最后一个生效

从这里可以验证手势优先获取事件,并取消了触摸事件

4.2、view和superview同时添加手势

同时给ViewC和ViewD添加手势,执行的仍然是viewD的手势,而不会执行superview(ViewC)的手势。

/**下面是添加手势之后打印的数据**/
ViewD -- touchesBegan
ViewD -- tapGesture
ViewD -- touchesCancelled

如果不为ViewD添加手势执行的是superview(ViewC)的手势。

/**下面是添加手势之后打印的数据**/
ViewD -- touchesBegan
ViewC-- tapGesture
ViewD -- touchesEnded

注意打印数据对比

  • ViewD未添加手势,superview(ViewC)添加手势,先执行superview(ViewC)的手势,然后执行ViewD的触摸方法
  • ViewD添加手势,superview(ViewC)添加手势,执行ViewD的手势,并取消ViewD的触摸方法,并且不会执行superview(ViewC)的手势

4.3、view相关属性

cancelsTouchesInView
默认为YES。表示当手势识别器成功识别了手势之后,会通知Application取消响应链对事件的响应,并不再传递事件给hit-test view。若设置成NO,表示手势识别成功后不取消响应链对事件的响应,事件依旧会传递给hit-test view。

delaysTouchesBegan
默认为NO。默认情况下手势识别器在识别手势期间,当触摸状态发生改变时,Application都会将事件传递给手势识别器和hit-tested view;若设置成YES,则表示手势识别器在识别手势期间,截断事件,即不会将事件发送给hit-tested view。也就是说从touchesBegan后续的流程都不再调用。

delaysTouchesEnded
默认为YES。当手势识别失败时,若此时触摸已经结束,会延迟一小段时间(0.15s)再调用响应者的 touchesEnded:withEvent:;若设置成NO,则在手势识别失败时会立即通知Application发送状态为end的touch事件给hit-tested view以调用 touchesEnded:withEvent: 结束事件响应。

五、UIControl

UIControl是系统提供的能够以target-action模式处理触摸事件的控件,iOS中UIButton、UISegmentedControl、UISwitch等控件都是UIControl的子类。当UIControl跟踪到触摸事件时,会向其上添加的target发送事件以执行action。值得注意的是,UIConotrol是UIView的子类,故具有和UIResponder同样的行为。
下面是UIControl内部的四个方法,因为只能接收一个UITouch对象,所以UIControl只能是单点触摸。

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;

5.1、UIControl重写touch方法

image.png

代码如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    [super touchesBegan:touches withEvent:event]; //对于UIControl在其内部自动调用beginTrackingWithTouch,下面方法类似调用相应的方法
    NSLog(@"CustomButton -- touchesBegan");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    [super touchesEnded:touches withEvent:event];
    NSLog(@"CustomButton -- touchesEnded");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    [super touchesCancelled:touches withEvent:event];
    NSLog(@"CustomButton -- touchesCancelled");
}

打印结果:没有打印UIButton的点击方法(未调用super

CustomButton -- touchesBegan
CustomButton -- touchesEnded

如果touch方法调用super,打印结果如下:

CustomButton -- touchesBegan
CustomButton-- ButtonDidClick
CustomButton -- touchesEnded

可见touch内部自动调用了UIControl相应的方法,所以UIControl也是基于touch实现的

5.2、UIControl添加手势

结果如下:

CustomButton -- touchesBegan
CustomButton-- buttonTapGesture
CustomButton -- touchesCancelled

结果分析:手势取消了触摸事件,同样也取消了按钮的事件。如果将手势的cancelsTouchesInView 设置成false,则触摸,手势和按钮的事件同时响应,依然是手势优先执行。

5.3、UIControl的父视图添加手势

打印结果如下 :

CustomButton -- touchesBegan
CustomButton-- ButtonDidClick
CustomButton -- touchesEnded

结果分析:UIControl的事件屏蔽了父视图的手势,如果不想屏蔽父视图的手势 可将cancelsTouchesInView 设置成false,这时优先响应手势,再响应UIControl的事件

如果放cancelsTouchesInView设置成false,打印结果如下:

CustomButton -- touchesBegan
ViewB-- tapGesture
CustomButton-- ButtonDidClick
CustomButton -- touchesEnded

可以看出还是手势的优先级相对高

结论:默认情况下UIControl比其父视图上的手势识别器具有更高的事件响应优先级。

最后借用一下别人的图片描述整个触摸事件的整个流程

image.png
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容