iOS 事件响应链机制

iOS中的事件的产生和传递

首先要知道 事件传递和响应过程 相反的。

事件的传递

当你点击了屏幕会产生一个触摸事件,系统会将该事件加入到一个由UIApplication管理的事件队列中,UIApplication会从消息队列里取事件分发下去,首先传给UIWindow,UIWindow会使用hitTest:withEvent:方法找到此次触摸事件初始点所在的视图,找到这个视图之后他就会调用视图的touchesBegan:withEvent:方法来处理事件。以消息的形式将事件发送给第一响应者,使其有机会首先处理事件。如果第一响应者没有进行处理,系统就将事件(通过消息)传递给响应者链中的下一个响应者,看看它是否可以进行处理。(这是一个完整的事件响应链流程),如下图:

为什么是队列而不是栈?
因为队列的特定是先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。

iOS 事件响应链机制_第1张图片
image.png

举例:
1.点击一个UIView,产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件队列中取出最前面的触摸事件A,并且把事件A传递给应用程序的主窗口(keyWindow)。
3.窗口会在视图层次结构中找到的第一响应者,也就是我们点击的UIView。

响应者链

如果hitTest:withEvent:找到的第一响应者initial view没有处理该事件,那么事件会沿着响应者链向上传递:第一响应者 -> 父视图 -> 视图控制器,如果传递到最顶级视图还没处理事件,那么就传递给UIWindow处理,若window对象也不处理->交给UIApplication处理,如果UIApplication对象还不处理,就丢弃该事件。

响应者链(Responder Chain):
       响应者链就是由一系列的响应者对象构成的一个层次结构。响应者对象(Responder Object),就是有响应和处理事件能力的对象。
       UIResponder是所有响应对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的UIApplication、 UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象。

左图: initial view -> view -> view的Controller -> window  -> application

右图: initial view -> view -> view的controller -> view -> viewController  -> window  -> application 

iOS 事件响应链机制_第2张图片
image.png

UIResponder存在着一个方法:- (nullable UIResponder *)nextResponder,通过方法可以获取当前view的下一个响应者。上图流程:

  1. 当 initial view 调用其nextResponder会返回其superView;
  2. 如果当前的 view 为UIViewController的 view 被添加到其他 view 上,那么调用nextResponder 会返回当前的UIViewController,而这个UIViewController的nextResponder为view的superView;
  3. 如果当前的UIViewController的view没有添加到任何其他view上,当前的UIViewController的nextResponder为nil,不管它是keyWinodw或UINavigationController的rootViewController,都是如此;
  4. 如果当前application的keyWindow的rootViewController为UINavigationController(或UITabViewController),那么通过调用UINavigationController(或UITabViewController)的nextResponder得到keyWinodw;
  5. keyWinodw的nextResponder为UIApplication,UIApplication的nextResponder为AppDelegate,AppDelegate的nextResponder为nil。

注 意
UIView不能接收触摸事件的三种情况:
不允许交互:userInteractionEnabled = NO
隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
透明度:如果设置一个控件的透明度
如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO,所以如果希望UIImageView可以交互,需要userInteractionEnabled = YES

UIResponder一般响应以下几种事件:触摸事件(touch handling)、点按事件(press handling)、加速事件和远程控制事件:

触摸事件(touch handling)
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches NS_AVAILABLE_IOS(9_1);
点按事件(press handling) NS_AVAILABLE_IOS(9_0)
- (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
加速事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
远程控制事件
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);

hitTest:withEvent方法解析

寻找最合适的view,两个重要的方法,:

只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法
作用,寻找并返回最合适的view(能够响应事件的那个最合适的view)
注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;   //寻找并返回最合适的view
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;  //方法判断点在不在当前view上

那如何寻找最合适的view来处理事件呢?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.触摸点是否在自己身上
3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)
4.如果没有符合条件的子控件,那么就认为自己最合适处理

详述:
1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上
2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)
3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
4.如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
5.找到最合适的view后,就会调用该view的touches方法处理具体的事件。所以,只有找到最合适的view,把事件传递给最合适的view后,才会调用touches方法进行接下来的事件处理。找不到最合适的view,就不会调用touches方法进行事件处理。
注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。

拦截事件的处理

正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。事件传递给谁,就会调用谁的hitTest:withEvent:方法。
注 意:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。

所以事件的传递顺序是这样的:
产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view

事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。
不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。

技巧:想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!

但是:在自己的hitTest:withEvent:方法中返回自己有时候会出现问题,因为会存在这么一种情况,当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,还要要求返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view,也就是B。这就导致了返回的不是自己而是点真正所在的view。所以还是建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!根据情况而定

例如:ViewA有ViewB和ViewC两个子控件。ViewB先添加,ViewC后添加。如果要求无论点击那里都要让ViewB作为最合适的view,那么只能在ViewA的hitTest:withEvent:方法中return self.subViews[0];这种情况下在ViewB的hitTest:withEvent:方法中return self;是不好使的!

当一个视图被另一个视图遮盖时,或者视图越界,如果想要点击被遮盖或者越界的视图响应,如下图:

iOS 事件响应链机制_第3张图片
图片越界.png

// 在父类ViewA中实现
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *view = [super hitTest:point withEvent:event];
    if([self pointInside:point withEvent:event]){
        if (view == nil) {
            for (UIView *subView in self.subviews) {
                //把父视图中的point坐便转换成subView内坐标
                CGPoint myPoint = [subView convertPoint:point fromView:self];
                //判断当前转换后的坐标是否在subView中
                if (CGRectContainsPoint(subView.bounds, myPoint)) {
                    return subView;
                }
            }
        }
    }
    return view;
}

附带了一个网上找的NSObject 子类关系图:


iOS 事件响应链机制_第4张图片
timg (1).jpeg

你可能感兴趣的:(iOS 事件响应链机制)