关于响应者链
关于响应者链
在IOS应用中,一般有三种接收用户操作的方式:
1、触屏事件(Touch Event)
2、运动事件(Motion Event)如:摇一摇
3、远端控制事件(Remote-Control Event)如:点击耳机上面的按钮
今天主要介绍关于第一种“触摸事件”中的事件传递模式。
从UIButton说起,UIButton继承与UIControl可以接受的事件有:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
typedef NS_OPTIONS ( NSUInteger , UIControlEvents) { UIControlEventTouchDown = 1 << 0, // on all touch downs UIControlEventTouchDownRepeat = 1 << 1, // on multiple touchdowns (tap count > 1) UIControlEventTouchDragInside = 1 << 2, UIControlEventTouchDragOutside = 1 << 3, UIControlEventTouchDragEnter = 1 << 4, UIControlEventTouchDragExit = 1 << 5, UIControlEventTouchUpInside = 1 << 6, UIControlEventTouchUpOutside = 1 << 7, UIControlEventTouchCancel = 1 << 8, UIControlEventValueChanged = 1 << 12, // sliders, etc. UIControlEventEditingDidBegin = 1 << 16, // UITextField UIControlEventEditingChanged = 1 << 17, UIControlEventEditingDidEnd = 1 << 18, UIControlEventEditingDidEndOnExit = 1 << 19, // 'return key' ending editing UIControlEventAllTouchEvents = 0x00000FFF, // for touch events UIControlEventAllEditingEvents = 0x000F0000, // for UITextField UIControlEventApplicationReserved = 0x0F000000, // range available for application use UIControlEventSystemReserved = 0xF0000000, // range reserved for internal framework use UIControlEventAllEvents = 0xFFFFFFFF }; |
通过继承UIControl都可以接受这些用户事件,当然UIButton也可以。如果要在UIControl的父类UIView中捕捉用户事件,一般会或重写这几个方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
-( void )touchesBegan:( NSSet *)touches withEvent:(UIEvent *)event { } -( void )touchesMoved:( NSSet *)touches withEvent:(UIEvent *)event { } -( void )touchesEnded:( NSSet *)touches withEvent:(UIEvent *)event { } -( void )touchesCancelled:( NSSet *)touches withEvent:(UIEvent *)event { } |
我们用的非常多,但是大家知道这4个方法是谁的实例方法吗?如果你一下就说出是UIView的,那么为什么我们在UIViewController中也可以用呢,他们不是继承关系。
注意这4个实例方法来自UIView与UIViewController的共同父类:UIResponder。它是我们今天的主角。
所有的IOS中关于的界面的Class都直接或间接地继承与UIResponder,点开UIResponder.h发现以上四个方法就在其中声明。对于用户事件的分发全部都取决于这个Class。IOS中的事件响应链与UIResponder有紧密关系
在IOS视图结构中,是呈现出来一个N叉数,一个视图可以有N个子视图,每个视图只有一个父视图,如下图:
当用户点击某一个视图或者按钮的时候会首先响应application中UIWindow一层一层的向下查找,直到找到用户指定的view为止,主要通过以下方法:
1 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system 2 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; // default returns YES if point is in bounds
在上图中用户点击视图中的ViewD时,UIWindow首先接收到响应,此响应包括用户点击的区域和一个封装好的UIEvent对象,然后UIWindow通过这些信息利用以下方法查找:
1. UIWindow会通过调用pointInside:withEvent:方法返回的YES得知用户点击的范围在ViewA中;
2. ViewA调用hitTest:withEvent:方法,在方法中遍历所有的subView(ViewB、ViewC)调用hitTest:withEvent:方法;
3. 在遍历中发现使用ViewC调用pointInside:withEvent:方法时返回YES,得知用户点击在ViewC范围之内;
4. ViewC调用hitTest:withEvent:方法,在方法中遍历所有的subView(ViewD、ViewE)调用hitTest:withEvent:方法;
5. 在遍历中发现使用ViewD调用pointInside:withEvent:方法时返回YES,得知用户点击在ViewD范围之内;
6. 在ViewD调用hitTest:withEvent:方法之前发现View的subViews的count为0,故确定用户点击在ViewD之上。
UIWindow会用遍历subviews,使用每一个subview调用hitTest:withEvent:方法,如果用户点击在某一个subview上,pointInside:withEvent:方法返回YES,再用这个subview调用hitTest:withEvent:方法,依次类推,直到当前view没有子view或点击的位置没有在其任何子view之上,便确定用户点击在某view上
大致在某一个view中是这样实现的:
1 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ 2 for (UIView *view in self.subviews) { 3 if([view pointInside:point withEvent:event]){ 4 UIView *hitTestView = [view hitTest:point withEvent:event]; 5 if(nil == hitTestView){ 6 return view; 7 } 8 } 9 } 10 return nil; 11 }
通过以上这种递归的形式就能找到用户点击的是哪个view,其中还要注意的时当前的view是否开启了userIntercationEnabled属性,如果这个属性开启,以上递归也会在开启属性的view层终止。当然以上只是简单的实现,和API真正的实现还有所不用,比如没有用到event参数。
既然找到了用户点击的view,那么当前就应该响应用户的点击事件了,也就是利用上面提到的UIResponder了,这个响应点击事件的过程是上面的逆序操作,这就是用到了UIResponder的nextResponder方法了。
因为UIView和UIViewController都是继承于UIResponder,所以在调用nextResponder时有几条规则如下:
1. 当一个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。
通过知道了上述规则,便可以通过用户点击的ViewD,查看ViewD是否响应了点击事件,如果没有找它的nextResponder,如果没有再继续找,直到找到AppDelegate再没有响应,则此点击事件被系统丢弃,大致流程如下:
事件响应链的大致流程就是如此了,大概就是一个先向下找,再向上找的过程!