iOS 事件(UITouch、UIControl、UIGestu

2019-08-07  本文已影响0人  luonaerduo

gitHub地址 :**** 响应链Demo

文章有点长,如果只是想了解大概过程的,可以直接看后面的总结

一.触摸、事件、响应者

1. UITouch

源起触摸

// 触摸的各个阶段状态 // 例如当手指移动时,会更新phase属性到UITouchPhaseMoved;// 手指离屏后,更新到UITouchPhaseEndedtypedef NS_ENUM(NSInteger, UITouchPhase) {    UITouchPhaseBegan,             // whenever a finger touches the surface.    UITouchPhaseMoved,             // whenever a finger moves on the surface.    UITouchPhaseStationary,        // whenever a finger is touching the surface but hasn't moved since the previous event.    UITouchPhaseEnded,             // whenever a finger leaves the surface.    UITouchPhaseCancelled,         // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)};

2.UIEvent

事件的真身

触摸的目的是生成触摸事件供响应者响应,一个触摸事件对应一个UIEvent对象,其中的type属性标识了事件的类型,事件有如下几种类型:

typedef NS_ENUM(NSInteger, UIEventType) {    UIEventTypeTouches,    UIEventTypeMotion,    UIEventTypeRemoteControl,    UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),};

这里我们所说的事件具体指的是触摸事件。

3.UIResponder

UIResponder是iOS中用于处理用户事件的API,可以处理触摸事件、按压事件(3D touch)、远程控制事件、硬件运动事件。可以通过touchesBegan、pressesBegan、motionBegan、remoteControlReceivedWithEvent等方法,获取到对应的回调消息。UIResponder不只用来接收事件,还可以处理和传递对应的事件,如果当前响应者不能处理,则转发给其他合适的响应者处理。

应用程序通过响应者来接收和处理事件,响应者可以是继承自UIResponder的任何子类,例如UIView、UIViewController、UIApplication等。当事件来到时,系统会将事件传递给合适的响应者,并且将其成为第一响应者。

第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIResponder的nextResponder决定,可以通过重写该属性来决定传递规则。当一个事件到来时,第一响应者没有接收消息,则顺着响应者链向后传递。

二.寻找事件的第一响应者

App接收到触摸事件后,会被放入当前应用程序的UIApplication维护的事件队列中。

由于事件一次只有一个,但是能够响应的事件的响应者众多,所以这就存在一个寻找第一响应者的过程。

1. 事件自下而上传递

查找第一响应者时,有两个非常关键的API,查找第一响应者就是通过不断调用子视图的这两个API完成的。

调用方法,获取到被点击的视图,也就是第一响应者。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

hitTest:withEvent:方法内部会通过调用pointInside:这个方法,来判断点击区域是否在视图上,是则返回YES,不是则返回NO。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

具体流程:

举个例子:

如图所示:

image

视图层级如下(同一层级的视图越在下面,表示越后添加):

image

现在假设在E视图所处的屏幕位置触发一个触摸,应用接收到这个触摸事件事件后,先将事件传递给UIWindow,然后自下而上开始在子视图中寻找第一响应者。事件传递的顺序如下所示:

2. hitTest函数本质

上面讲到了事件在响应者之间传递的规则,视图通过判断自身能否响应事件来决定是否继续想子视图传递。

这里涉及到两个问题:

针对第一个问题:

首先我们要知道,以下几种状态的视图是无法响应事件的:

其次,如果当前视图可以响应事件,还必须通过pointInside函数判断,触摸点是否在当前视图的坐标范围内,如果不在当前视图的坐标范围内,则无法响应,如果在坐标范围内,并且该视图可以响应事件,就进入下一步事件的传递。

针对第二个问题:

hitTest:withEvent: 方法返回一个UIView对象,作为当前视图层次中的响应者。默认实现是:

依据以上的描述我们可以推测出hitTest:WithEvent:的默认实现大致如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{    //3种状态无法响应事件     if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil;     //触摸点若不在当前视图上则无法响应事件    if ([self pointInside:point withEvent:event] == NO) return nil;     //从后往前遍历子视图数组     int count = (int)self.subviews.count;     for (int i = count - 1; i >= 0; i--)     {         // 获取子视图        UIView *childView = self.subviews[i];         // 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标        CGPoint childP = [self convertPoint:point toView:childView];         //询问子视图层级中的最佳响应视图        UIView *fitView = [childView hitTest:childP withEvent:event];         if (fitView)         {            //如果子视图中有更合适的就返回            return fitView;         }    }     //没有在子视图中找到更合适的响应视图,那么自身就是最合适的    return self;}

我们分别在上述示例的视图层次中的每个视图实现文件添加如下方法:

#pragma mark -------------------------- Override Methods- (void)touchesBegan:(NSSet<uitouch *> *)touches withEvent:(UIEvent *)event {    NSLog(@"%s",__func__);    [super touchesBegan:touches withEvent:event];}- (void)touchesMoved:(NSSet<uitouch *> *)touches withEvent:(UIEvent *)event {    NSLog(@"%s",__func__);    [super touchesMoved:touches withEvent:event];}- (void)touchesEnded:(NSSet<uitouch *> *)touches withEvent:(UIEvent *)event {    NSLog(@"%s",__func__);    [super touchesEnded:touches withEvent:event];}- (void)touchesCancelled:(NSSet<uitouch *> *)touches withEvent:(UIEvent *)event {    NSLog(@"%s",__func__);    [super touchesCancelled:touches withEvent:event];}- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{    NSLog(@"%s",__func__);    return [super hitTest:point withEvent:event];}- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{    NSLog(@"%s",__func__);    return [super pointInside:point withEvent:event];}</uitouch *></uitouch *></uitouch *></uitouch *>

然后单点E视图,打印如下:

 -[AView hitTest:withEvent:] -[AView pointInside:withEvent:] -[CView hitTest:withEvent:] -[CView pointInside:withEvent:] -[FView hitTest:withEvent:] -[FView pointInside:withEvent:] -[EView hitTest:withEvent:] -[EView pointInside:withEvent:] -[AView hitTest:withEvent:] -[AView pointInside:withEvent:] -[CView hitTest:withEvent:] -[CView pointInside:withEvent:] -[FView hitTest:withEvent:] -[FView pointInside:withEvent:] -[EView hitTest:withEvent:] -[EView pointInside:withEvent:] -[EView touchesBegan:withEvent:] -[CView touchesBegan:withEvent:] -[AView touchesBegan:withEvent:] -[EView touchesEnded:withEvent:] -[CView touchesEnded:withEvent:] -[AView touchesEnded:withEvent:]

从打印结果我们可以看到最终EView视图先对事件进行了响应,同时将事件沿着响应链进行传递。

以上打印结果我们会发现单机E视图后,从[AView hitTest:withEvent:]到 [EView pointInside:withEvent:] 的过程会执行两遍,这个问题我查找了一些资料,但都没有好的答案,苹果那边的回复是这样的:

Yes, it’s normal. The system may tweak the point being hit tested between the calls. Since hitTest should be a pure function with no side-effects, this should be fine.

具体详见:https://lists.apple.com/archives/cocoa-dev/2014/Feb/msg00118.html

意思就是说hitTest是一个没有副作用的纯函数,进行多次调用也不会对外产生影响,因此系统可以多次调整调用之间被测试的点。

这里并没有给出具体的调用两次的原因,你也可以理解为系统为了精确触摸的点,而进行了多次调用,但为什么是两次,我也没找到相关答案。

3.事件拦截

实际开发中我们经常会遇到如下需求

image

事件拦截.gif

在Tabbar的Item上面添加提示视图tipView,当点击提示视图tipview,对应的Item也进行响应,并且提示视图tipView消失。

很明显,这里的提示视图tipView是添加在Tabbar上面的,但是提示视图tipView的位置又超出了Tabbar的区域,这时我们点击提示视图tipView,会发现提示视图tipView得不到响应。

我们看一下调用的堆栈:

image image image

从堆栈中我们得出如下分析:

从这边的分析我们可以看出事件没有传递到提示视图tipView,在Tabbar这里就直接返回了,因为Tabbar判断点击位置不在自己的坐标范围内。

因此我们需要做的就是修改Tabbar的hitTest:withEvent:函数里面判断点击位置是否在Tabbar坐标范围的的判断条件,也就是需要重写TabBard的 pointInside:withEvent:方法,判断如果当前触摸坐标在子视图tipView上面,就返回YES,否则返回NO;这样一来时间就会最终传递到tipView上面,最终事件就会由tipView来响应。

代码如下:

#import "FJFTabbar.h"@implementation FJFTabbar//TabBar- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {    //将触摸点坐标转换到在CircleButton上的坐标    CGPoint pointTemp = [self convertPoint:point toView:self.indicateView];    //若触摸点在CricleButton上则返回YES    if ([self.indicateView pointInside:pointTemp withEvent:event]) {        return YES;    }    //否则返回默认的操作    return [super pointInside:point withEvent:event];}@end

三.事件的响应及传递

经过Hit-Testing的过程后,UIApplication已经知道了第一响应者是谁,接下来要做的事情就是:

A. 将事件传递给第一响应者:

由于第一响应者具有处理事件的最高优先级,因此UIApplication会先将事件传递给它供其处理。首先,UIApplication将事件通过 sendEvent: 传递给事件所属的window,window同样通过 sendEvent: 再将事件传递给hit-tested view,即第一响应者。过程如下:

UIApplication ——> UIWindow ——> hit-tested view

以点击EView视图为例,在EView的 touchesBegan:withEvent:上断点查看调用栈就能看清这一过程:

image

从这调用堆栈我们可以看出,UIApplication对于将事件传递给那个UIWindow是很明确的,UIWindow对于将事件传递给哪个视图也是很明确的。因为这些信息都放在了UIEvent的Touch事件里面。

但是这些信息又是什么时候放入到UIEvent内部的呢?

可想而知因为Hit-Testing和SendEvent两者中的UIEvent是同一个UIEvent,所以这应该是在Hit-Testing寻找第一响应者的过程中,填入UIEvent内部的。

B.将事件沿着响应链传递:

因为每个响应者必定都是UIResponder对象,通过4个响应触摸事件的方法来响应事件。每个UIResponder对象默认都已经实现了这4个方法,但是默认不对触摸事件做任何处理,单纯只是将事件沿着响应链传递。若要截获事件进行自定义的响应操作,就要重写相关的方法。

第一响应者接收到触摸事件后,就具有对触摸事件的处理权,它可以选择自己处理这个事件,也可以将这个事件沿着响应链传递给下一个响应者,这个由响应者之间构成的视图链就称之为响应链。

需要注意的是,上一节所说的事件传递的目的是为寻找事件的最佳响应者,是自下而上的传递;这里的事件传递目的是响应者做出对事件的响应,这个过程是自上而下的。前者为“寻找”,后者为“响应”。

响应者对于事件的操作方式:

响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent:方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。

响应者对于接收到的事件有3种操作:

响应链中的事件传递规则:

每一个响应者对象(UIResponder对象)都有一个nextResponder方法,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的第一响应者确定了,这个事件所处的响应链就确定了。

对于响应者对象,默认的 nextResponder 实现如下:

举个例子:

image

事件响应示例.png

如上图所示,响应者链如下:

UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegation

图中虚线箭头是指若该UIView是作为UIViewController根视图存在的,则其nextResponder为UIViewController对象;若是直接add在UIWindow上的,则其nextResponder为UIWindow对象。

可以用以下方式打印一个响应链中的每一个响应对象,在第一响应者的 touchBegin:withEvent: 方法中调用即可(别忘了调用父类的方法)

- (void)printResponderChain {    UIResponder *responder = self;    printf("%s",[NSStringFromClass([responder class]) UTF8String]);    while (responder.nextResponder) {        responder = responder.nextResponder;        printf(" --> %s",[NSStringFromClass([responder class]) UTF8String]);    }}

以点击EView为例,重写EView的touch Begin:WithEvent:

- (void)touchesBegan:(NSSet<uitouch *> *)touches withEvent:(UIEvent *)event {    NSLog(@"%s",__func__);    [self printResponderChain];    [super touchesBegan:touches withEvent:event];}</uitouch *>

响应链如下:

EView --> CView --> AView --> UIView --> FJFFirstViewController --> UIViewControllerWrapperView --> UINavigationTransitionView --> UILayoutContainerView --> UINavigationController --> UIViewControllerWrapperView --> UITransitionView --> UILayoutContainerView --> FJFTabBarViewController --> FJFWindow --> FJFApplication --> AppDelegate

另外如果有需要,完全可以重写响应者的 nextResponder 方法来自定义响应链。

四.UIGestureRecognizer、UIControl

上面我们讲述了UIResponder响应触摸事件的过程,但除了UIResponder之外,UIGestureRecognizer、UIControl同样具备对事件的处理能力。

以下将通过结合具体的示例来讲解UIGestureRecognizer和UIControl是如何处理触摸事件的。

举个例子:

image

代码:

#pragma mark -------------------------- Life Circle- (void)viewDidLoad {    [super viewDidLoad];    self.title = @"分类";    // view tap    FJFTapView *tmpContainerView = [[FJFTapView alloc] initWithFrame:CGRectMake(50, 80, 260, 300)];    tmpContainerView.backgroundColor = [UIColor redColor];    FJFTapGestureRecognizer *tapGesture = [[FJFTapGestureRecognizer alloc] initWithTarget:self action:@selector(viewTap:)];    [tmpContainerView addGestureRecognizer:tapGesture];    [self.view addSubview:tmpContainerView];    // view longPress    FJFLongPressView *tmpLongPressView = [[FJFLongPressView alloc] initWithFrame:CGRectMake(50, 400, 260, 200)];    tmpLongPressView.backgroundColor = [UIColor grayColor];    FJFLongPressGestureRecognizer *longPressGesture = [[FJFLongPressGestureRecognizer alloc] initWithTarget:self action:@selector(viewlongPress:)];    [tmpLongPressView addGestureRecognizer:longPressGesture];    [self.view addSubview:tmpLongPressView];    // button    FJFButton *tmpButton = [[FJFButton alloc] initWithFrame:CGRectMake(100, 50, 120, 80)];    tmpButton.backgroundColor = [UIColor greenColor];    [tmpButton setTitle:@"UIButton" forState:UIControlStateNormal];    [tmpButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];    [tmpButton addTarget:self action:@selector(tmpButtonClicked:) forControlEvents:UIControlEventTouchUpInside];    [tmpContainerView addSubview:tmpButton];    // imageControl    FJFImageControl *imageControl = [[FJFImageControl alloc] initWithFrame:CGRectMake(100, 150, 120, 80) title:@"imageControl" iconImageName:@"ic_red_box.png"];    imageControl.backgroundColor = [UIColor blueColor];    [imageControl addTarget:self action:@selector(imageControlTouch:) forControlEvents:UIControlEventTouchUpInside];    [tmpContainerView addSubview:imageControl];}#pragma mark -------------------------- Response Event// tap- (void)viewTap:(UITapGestureRecognizer *)tap {    NSLog(@"%s", __FUNCTION__);}// longPress- (void)viewlongPress:(UILongPressGestureRecognizer *)longPress {    NSLog(@"%s", __FUNCTION__);}// buttonClicked- (void)tmpButtonClicked:(UIButton *)sender {    NSLog(@"%s", __FUNCTION__);}// controlTouch- (void)imageControlTouch:(FJFImageControl *)imageControl {     NSLog(@"%s", __FUNCTION__);}

如代码所示:

观察各种情况的日志:

1.点击FJFTapView:

[FJFTapGestureRecognizer touchesBegan:withEvent:][FJFTapView touchesBegan:withEvent:][FJFTapGestureRecognizer touchesEnded:withEvent:][FJFThreeViewController viewTap:][FJFTapView touchesCancelled:withEvent:]

2.长按FJFLongPressView:

[FJFLongPressGestureRecognizer touchesBegan:withEvent:][FJFLongPressView touchesBegan:withEvent:][FJFThreeViewController viewlongPress:][FJFLongPressView touchesCancelled:withEvent:][FJFLongPressGestureRecognizer touchesEnded:withEvent:][FJFThreeViewController viewlongPress:]

3.点击UIButton:

[FJFTapGestureRecognizer touchesBegan:withEvent:][FJFButton touchesBegan:withEvent:][FJFTapGestureRecognizer touchesEnded:withEvent:][FJFButton touchesEnded:withEvent:][FJFThreeViewController tmpButtonClicked:]

4.点击FJFImageControl:

[FJFTapGestureRecognizer touchesBegan:withEvent:][FJFImageControl touchesBegan:withEvent:][FJFTapGestureRecognizer touchesEnded:withEvent:][FJFThreeViewController viewTap:][FJFImageControl touchesCancelled:withEvent:]

接下来我们一一解释这些现象:

1. UIGestureRecognizer:

手势分为离散型手势(discrete gestures)和持续型手势(continuous gesture)。系统提供的离散型手势包括点按手势(UITapGestureRecognizer)和轻扫手势(UISwipeGestureRecognizer),其余均为持续型手势。

两者主要区别在于状态变化过程:

离散型:

    识别成功:Possible —> Recognized    识别失败:Possible —> Failed

持续型:

    完整识别:Possible —> Began —> [Changed] —> Ended    不完整识别:Possible —> Began —> [Changed] —> Cancel

A. 离散型手势

从点击FJFTapView的日志可以分析:

[FJFTapGestureRecognizer touchesBegan:withEvent:][FJFTapView touchesBegan:withEvent:][FJFTapGestureRecognizer touchesEnded:withEvent:][FJFThreeViewController viewTap:][FJFTapView touchesCancelled:withEvent:]

这里我们可以得出:UIGestureRecognizer比UIResponder具有更高的事件响应的优先级

这个结论我们也可以从官方文档中得出:

A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEndedproperties.

还有一点需要注意的是:

UIGestureRecognizer对事件的响应也是通过touch相关的4个方法来实现的,而这4个方法声明在UIGestureRecognizerSubclass.h中。

image

而这里UIWindow之所以知道要把事件传递给哪些手势识别器,主要还是通过UIEvent里面的gestureRecognizers数组来获取的,而数组里面的手势识别器是在Hit-Test View寻找第一响应者过程中填充的。

image

这里UIWindow会取出UIEvent里面的gestureRecognizers数组的手势识别器,将事件传递给各个手势识别器,如果有一个手势识别器识别了事件,其他的手势识别器就不会响应该事件。

注意:这里取出gestureRecognizers数组的手势识别器,没有按照特定的顺序,比如说从前往后或是从后往前,可以通过hook掉UIGestureRecognizer的touch相关方法,去追踪得出。

因此我们可以分析日志:

B. 持续型手势

从点击FJFLongPressView日志分析:

[FJFLongPressGestureRecognizer touchesBegan:withEvent:][FJFLongPressView touchesBegan:withEvent:][FJFThreeViewController viewlongPress:][FJFLongPressView touchesCancelled:withEvent:][FJFLongPressGestureRecognizer touchesEnded:withEvent:][FJFThreeViewController viewlongPress:]

从日志我们可以看出长按手势回调了两次,我们通过分析两次调用的堆栈:

第一次调用堆栈:

image

第二次调用堆栈

image

我们可以看出第一次调用是在runloop中通知监听的手势识别器的观察者,来通知长按手势识别器对长按事件进行响应,此时手势识别器的state为UIGestureRecognizerStateBegan。

第二次调用是UIWindow 先将事件传递给UIEvent的gestureRecognizers数组里的手势识别器,然后长按手势识别器FJFLongPressGestureRecognizer识别成功进行回调,此时手势识别器的state为UIGestureRecognizerStateEnded。

这里的调用逻辑其实跟单击手势识别器FJFTapGestureRecognizer相似,主要区别在于长按手势识别器FJFLongPressGestureRecognizer调用了两次。

C. 总结

当触摸发生或者触摸的状态发生变化时,UIWindow都会传递事件寻求响应。

-UIWindow先将触摸事件传递给响应链上绑定的手势识别器,再发送给触摸对象对应的第一响应者。

D. 拓展

手势识别器的3个属性:

@property(nonatomic) BOOL cancelsTouchesInView;@property(nonatomic) BOOL delaysTouchesBegan;@property(nonatomic) BOOL delaysTouchesEnded;

a. cancelsTouchesInView:

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

以点击FJFTapView为例,将tapGesture.cancelsTouchesInView = NO;输出日志如下:

[FJFTapGestureRecognizer touchesBegan:withEvent:][FJFTapView touchesBegan:withEvent:][FJFTapGestureRecognizer touchesEnded:withEvent:][FJFThreeViewController viewTap:][FJFTapView touchesEnded:withEvent:]

从日志我们可以看出,即便FJFTapGestureRecognizer识别了点击手势,UIApplication也依旧将事件发送给FJFTapView.

b. delaysTouchesBegan:

默认为NO。默认情况下手势识别器在识别手势期间,当触摸状态发生改变时,Application都会将事件传递给手势识别器和第一响应者;若设置成YES,则表示手势识别器在识别手势期间,截断事件,即不会将事件发送给第一响应者。

以点击FJFTapView为例,将tapGesture.delaysTouchesBegan = YES;输出日志如下:

[FJFTapGestureRecognizer touchesBegan:withEvent:][FJFTapGestureRecognizer touchesEnded:withEvent:][FJFThreeViewController viewTap:]

从日志可以看出,手势识别器识别手势期间,事件不会传递给FJFTapView,因此FJFTapView的touchesBegan:withEvent:不会被调用;而手势识别器成功识别手势后,独吞了事件,不会再传递给FJFTapView,因此只打印手势识别器识别成功后手势的绑定函数。

c. delaysTouchesEnded:

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

2.UIControl

UIControl是系统提供的能够以target-action模式处理触摸事件的控件,iOS中UIButton、UISegmentedControl、UISwitch等控件都是UIControl的子类。

值得注意的是,UIConotrol是UIView的子类,因此本身也具备UIResponder应有的身份。

UIControl作为控件类的基类,它是一个抽象基类,我们不能直接使用UIControl类来实例化控件,它只是为控件子类定义一些通用的接口,并提供一些基础实现,以在事件发生时,预处理这些消息并将它们发送到指定目标对象上。

关于UIControl,此处介绍两点:

Target-Action机制

Target-action是一种设计模式,直译过来就是”目标-行为”。当我们通过代码为一个按钮添加一个点击事件时,通常是如下处理:

[button addTarget:self action:@selector(tapButton:) forControlEvents:UIControlEventTouchUpInside];
image

注:图片来源于官方文档****Cocoa Application Competencies for iOS – Target Action

即当事件发生时,事件会被发送到控件对象中,然后再由这个控件对象去触发target对象上的action行为,来最终处理事件。因此,Target-Action机制由两部分组成:即目标对象Target和行为Selector。目标对象指定最终处理事件的对象,而行为Selector则是处理事件的方法。

UIControl作为能够响应事件的控件,必然也需要待事件交互符合条件时才去响应,因此也会跟踪事件发生的过程。不同于UIResponder以及UIGestureRecognizer通过touches系列方法跟踪,UIControl有其独特的跟踪方式:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event {    NSLog(@"%s",__func__);    return YES;}- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event {    NSLog(@"%s",__func__);    return YES;}- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event {    NSLog(@"%s",__func__);}- (void)cancelTrackingWithEvent:(nullable UIEvent *)event {    NSLog(@"%s",__func__);}

这4个方法和UIResponder的那4个方法几乎吻合,只不过UIControl只能接收单点触控,因此接收的参数是单个UITouch对象。这几个方法的职能也和UIResponder一致,用来跟踪触摸的开始、滑动、结束、取消。不过,UIControl本身也是UIResponder,因此同样有touches系列的4个方法。事实上,UIControl的 Tracking 系列方法是在touch 系列方法内部调用的。比如 beginTrackingWithTouch 是在 touchesBegan 方法内部调用的, 因此它虽然也是UIResponder,但touches 系列方法的默认实现和UIResponder本类还是有区别的。

我们来分析下FJFButton的日志输出以及调用堆栈:

日志输出:

[FJFTapGestureRecognizer touchesBegan:withEvent:][FJFButton touchesBegan:withEvent:][FJFTapGestureRecognizer touchesEnded:withEvent:][FJFButton touchesEnded:withEvent:][FJFThreeViewController tmpButtonClicked:]

调用堆栈:

image

FJFButton调用堆栈.png

从以上信息,我们可以分析:

通过这个结果,我们会疑问:UIControl比其父视图上的手势识别器具有更高的事件响应优先级?

接下来我们看下继承自UIControl的FJFImageControl的日志和调用堆栈:

日志输出:

[FJFTapGestureRecognizer touchesBegan:withEvent:][FJFImageControl touchesBegan:withEvent:][FJFTapGestureRecognizer touchesEnded:withEvent:][FJFThreeViewController viewTap:][FJFImageControl touchesCancelled:withEvent:]

调用堆栈:

image

从以上信息,我们又可以得出::UIControl比其父视图上的手势识别器的优先级来的低?

经验证系统提供的有默认action操作的UIControl,例如UIbutton、UISwitch等的单击,UIControl的响应优先级比手势识别器高,而对于自定义的UIControl,响应的优先级比手势低。

至于为什么会这样,没找到具体原因,但测试的结果,推测系统应该是依据UITouch的touchIdentifier来进行区别处理。

Target-Action的管理:

UIControl通过addTarget方法和removeTarget方法来添加和删除Target-Action的操作。


如果想获取控件对象所有相关的target对象,则可以调用allTargets方法,该方法返回一个集合。集合中可能包含NSNull对象,表示至少有一个nil目标对象。

而如果想获取某个target对象及事件相关的所有action,则可以调用actionsForTarget:forControlEvent:方法。

不过,这些都是UIControl开放出来的接口。我们还是想要探究一下,UIControl是如何去管理Target-Action的呢?

实际上,我们在程序某个合适的位置打个断点来观察UIControl的内部结构,可以看到这样的结果:

image

从图中我们可以看出,UIControl内部实际上是有一个可变数组(_targetActions)来保存Target-Action,数组中的每个元素是一个UIControlTargetAction对象。UIControlTargetAction类是一个私有类,内部维护

@interface UIControlTargetAction : NSObject {    SEL _action;    BOOL _cancelled;    unsigned int _eventMask;// 事件类型,比如:UIControlEventTouchUpInside    id _target;}

这四个变量,UIControl正是依据UIControlTargetAction来对事件进行处理。

五.事件完整响应链

六. 延伸阅读

上一篇 下一篇

猜你喜欢

热点阅读