-
Notifications
You must be signed in to change notification settings - Fork 23
事件分发传递和响应链
我们在手机上的一个触摸或点击,在手机系统中是怎么样一个过程: 其实首先就是手机屏幕和底层相关软硬件对这个触摸一个解析过程,将这个触摸解析成event,然后iOS系统将这个event事件传递到相应的界面上,由界面来响应我们的操作,给出对应的反馈,这样一个交互过程。这其中传递的过程就是我们今天的要了解的主角。从图上可以看到,从window—view之间,具体是怎么样一个规律呢? 先从这个触摸或点击开始了解一下:
- 一次触摸将产生一个UITouch:一个手指离开屏幕前的一系列动作,包含时间戳、所在视图、 力度等信息。
- UIEvent:多个UITouch组成,也就是多个触摸组成。 一个event指的是第一个手指开始触摸到最后一个手指离开屏幕这段时间所有UITouch的总和。
那么这个UIEvent是如何在系统解析出来后,传递下去呢?哪些对象可以传递UIEvent呢?
响应者就是可以接收到UIEvent的对象,也是可以最终响应用户的操作的对象。iOS开发中主要与四种对象可以作为responder:
UIResponder作为响应者的基类,主要是定义了一些关于响应的属性和方法,用于子类判断或者执行关于响应的操作:
@interface UIResponder : NSObject <UIResponderStandardEditActions>
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL canBecomeFirstResponder; // default is NO
#else
- (BOOL)canBecomeFirstResponder; // default is NO
#endif
- (BOOL)becomeFirstResponder;
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL canResignFirstResponder; // default is YES
#else
- (BOOL)canResignFirstResponder; // default is YES
#endif
- (BOOL)resignFirstResponder;
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL isFirstResponder;
#else
- (BOOL)isFirstResponder;
#endif
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)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);
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(3_0);
// Allows an action to be forwarded to another target. By default checks -canPerformAction:withSender: to either return self, or go up the responder chain.
- (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(7_0);
@property(nullable, nonatomic,readonly) NSUndoManager *undoManager NS_AVAILABLE_IOS(3_0);
这些responder在接受上一个responder传递而来的响应UIEvent, 这样依次传递响应的顺序就是一个响应链 如果将响应链顺序反向:就是UIEvent通过hit-test遍历的过程链。 责任传递就是处于应该响应顺序上的上一个对象有责任处理这个UIEvent,如果他不处理,就轮到响应链上的下一个来处理 也就是说刚才UIEvent的传递过程和响应链是两个互逆的顺序
其实系统是通过hit-test方法来决定哪个view作为最终响应用户操作的载体。hit-test是UIView的一个扩展方法,与其搭配的就是pointInside方法, 二者功能可以确定点击或者触摸的点是否在view的范围内,通过hit-test的返回值告诉系统当前view是否可以作为响应者。
理解hit-test就是理解响应链的核心点:
iOS系统在判断哪个view来响应用户时 ,就是通过hit-test在所有view的“树”上遍历:当前内存中所有view的子类都会执行hit-test方法,逐个遍历每个叶子节点,如果叶子节点view上还有叶子,就进一步遍历下去。再啰嗦一句:即使某个view不会作为响应者,并且也不在点击范围内,只要它是当前显示的vc或者view的子视图,就会被httest遍历到,从而获取到是否可以作为响应者的信息。
序号25的栈帧上,主线程的runloop接收到UIEvent,然后经由分发器,首先分发到window上,然后传递给UIView,然后16--13正式遍历这个UIView的所有叶子的过程。在遍历过程中,如果子节点的任何一个view在执行hit-test时返回了self(当前子view),就表示找到了响应的视图,这时父类也会通过返回值将这个子view向上返回,一直返回到window,返回到application,也就让系统知道了应该让谁来处理这个触摸。
注意:以下三种情况下,view将不会执行hit-test:
- hidden=YES
- userInteractionEnabled=NO
- alpha<0.01
- 视图超出父视图的区域
通过上面的hit-test遍历后,已经找到了响应者responder,由于其必然继承自UIResponder,我们可以重写UIResponder的touchBegan方法,touchBegan方法中再通过nextResponder属性来遍历地查看响应者链上的每个响应者:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
我们以下图来具体描述下响应者链:
点击区域1和3,自然是不用说了。
那么,点击区域2,是响应哪个事件呢?
通过上面的hit-test遍历规则分析,我们该知道蓝色3区域是最后一个subview,后进先出,那么他应该是first reponder,所以点击2区域是响应蓝色按钮事件。
那么,既然我们的主题是事件穿透,那么肯定是希望红色2区域响应事件的。怎么做呢,肯定是要重写hit-test方法的,基本思路就是在点击2区域返回的是红色button,那么等于改写了hit-test规则,first responder就可以变成红色button。
那么,每个view都有hit-test,我们是在哪个view重写hit-test呢?
因为hit-test在传递事件遍历的时候是从父view到subviews,而subviews是从蓝色到红色。
然而在点击2区域的时候,遍历到蓝色的时候就已经返回了,根本不会调用红色button的hit-test。
所以,我们可以在蓝色view里重写,也可以在父view里重写hit-test。下面是在蓝色view里重写的hit-test
- (UIView *)hit-test:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint redBtnPoint = [self convertPoint:point toView:_redButton];
if ([_redButton pointInside:redBtnPoint withEvent:event]) {
return _redButton;
}
//如果希望严谨一点,可以将上面if语句及里面代码替换成如下代码
//UIView *view = [_redButton hit-test: redBtnPoint withEvent: event];
//if (view) return view;
return [super hit-test:point withEvent:event];
}
如此,点击2区域,就会响应红色按钮就会响应事件。 这种透传有个前提:你要能获取到被挡住的view对象。
如果是私有的view对象,甚至通过runtime都获取不到该view对象。比如说appstore的下载按钮以及其他subviews,那就不能用这种方案。
从上面可以看出,事件的传递方向是(hit-test就是事件的传递):
UIApplication -> UIWindow ->ViewController-> UIView -> initial view
而Responder传递方向是(还记得nextResponder吗):
Initial View -> Parent View -> ViewController -> Window -> Application