事件分发、响应者链条 201905更新

iOS的三种事件:触摸事件/运动事件/远程控制事件

系列词语:事件传递、响应者链条、hitTest和pointInside

 
  1. typedef enum {  
  2. UIEventTypeTouches,  
  3. UIEventTypeMotion,  
  4. UIEventTypeRemoteControl,  
  5. } UIEventType;  

只有继承UIResponder类的对象才能处理事件,如UIView、UIViewController、UIApplication都继承自UIResponder,都能接收并处理事件。UIResponder中定义了上面三类事件相关的处理方法:

传递过程详解:

keyWindow会在它的内容视图上调用hitTest:withEvent:(该方法返回的就是处理此触摸事件的最合适view)来完成这个找寻过程。
hitTest:withEvent:在内部首先会判断该视图是否能响应触摸事件,如果不能响应,返回nil,表示该视图不响应此触摸事件。然后再调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内)。如果pointInside:withEvent:返回NO,那么hiteTest:withEvent:也直接返回nil。
如果pointInside:withEvent:返回YES,则向当前视图的所有子视图发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历。直到有子视图返回非空对象或者全部子视图遍历完毕;若第一次有子视图返回非空对象,则 hitTest:withEvent:方法返回此对象,处理结束;如所有子视图都返回非,则hitTest:withEvent:方法返回该视图自身。

举例说明

窗口A 包含子视图B B里面包含子视图C.   当 触摸C的时候  系统先将这一动作告诉  a  a再告诉b b再告诉c   c一看 我处理不了  于是c又反过来告诉b   b一看也蒙了  说我也来不了  有事  睡觉呢 。 于是 b 又给了 a  如果 a 也不响应  那么 这个事件就会被系统 丢弃

不接收触摸事件的三种情况

(1)不接收用户交互 userInteractionEnabled = NO
(2)隐藏 hidden = YES
(3)透明 alpha = 0.0 ~ 0.01

hitTest:底层实现

// point是该视图的坐标系上的点
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 1.判断自己能否接收触摸事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    // 2.判断触摸点在不在自己范围内
    if (![self pointInside:point withEvent:event]) return nil;
    // 3.从后往前遍历自己的子控件,看是否有子控件更适合响应此事件
    int count = self.subviews.count;
    for (int i = count - 1; i >= 0; i--) {
        UIView *childView = self.subviews[i];
        CGPoint childPoint = [self convertPoint:point toView:childView];
        UIView *fitView = [childView hitTest:childPoint withEvent:event];
        if (fitView) {
            return fitView;
        }
    }
    // 没有找到比自己更合适的view
    return self;
}

只有弄清楚了hitTest:方法的底层实现,才能更容易理解事件传递机制。



下面主要讨论触摸事件。

  1. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;     
  2. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;    
  3. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;     
  4. - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;  

事件分发:Hit-Testing View

 

hitTest:withEvent:具体的实现:

hitTest:withEvent:方法首先会调用pointInside:withEvent:方法,如果触摸点在View的内部,该方法返回YES。接下来会递归的调用所有pointInside:withEvent:方法返回YES的子view的hitTest:withEvent:方法。

如果触摸点不在View里面,调用pointInside:withEvent:方法会返回NO,hitTest:withEvent:会返回nil。如果子view在调用pointInside:withEvent:时返回NO,那么以这个子view为父视图的所有子view都不必再检查。也就是说如果触摸点没在一个子view上,那肯定也不可能在该子view的子view上面。也就是说在父view外面的任何子view的部分永远不可能接收到触摸事件。

当触摸最上面绿色控件的红色框框的那部分永远无法接收到触摸事件。这种情况在view的clipsToBounds属性设置为NO时会出现。

下面的例子展示了hitTest:withEvent:函数的过程

假设用户触摸了图中的view E。iOS通过如下顺序查找hit-test view

1.触摸点在A里面,因此检测子view B和C

2.触摸点不在B里面,但是在C里面。因此检测C的子View D和E。

3.触摸点不在D里面,但是在E里面,并且E是在最外层的包含触摸点的view,因此E就是要找的hit-test view

hitTest:withEvent:函数的实现代码:

1.能否自己处理?不能,return nil;

2.点在不在当前控件上?没在,return nil;

3.说明能处理触摸事件,并且在当前控件上,是合适的控件,但不一定是最合适的。从后往前遍历自己的子控件,是否是最合适的控件(包含该触摸点的View)。如果是,返回该View。

4.说明没找到比自己合适的View,返回自己。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event的实现:

  1. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event  
  2. {  
  3.     if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01)  
  4.     {  
  5.         return nil;  
  6.     }  
  7.     if (![self pointInside:point withEvent:event])  
  8.     {  
  9.         return nil;  
  10.     }  
  11.     __block UIView *hitView = self;  
  12.     [self.subViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOLBOOL *stop) {     
  13.   
  14.         CGPoint thePoint = [self convertPoint:point toView:obj];  
  15.   
  16.         UIView *theSubHitView = [obj hitTest:thePoint withEvent:event];  
  17.   
  18.         if (theSubHitView != nil)  
  19.         {  
  20.             hitView = theSubHitView;  
  21.   
  22.             *stop = YES;  
  23.         }  
  24.   
  25.     }];  
  26.   
  27.     return hitView;  
  28. }  

响应链

响应链是一系列响应对象,响应链中,所有对象的基类都是UIResponder从firstResponse开始到application对象结束。firstResponse意味着首先接收到事件,一般第一响应者是一个view,对于触摸事件来说就是hit-test view。

前面说过,UIKit首先会将该事件发送给最合适的对象来处理。对于触摸事件该对象指的是hit-test view,对于其他事件,该对象指的是第一响应者。

但是实际上响应链也用在触摸事件/运动事件/远程控制事件/等

如果hit-test view无法处理一个事件,事件就通过响应链往上传递(hitTestView算是第一个Responder),直到找到一个可以处理的Responder为止。

下图给出了沿着响应链传递的顺序。两个图的区别是视图的层次关系不一样。响应链从firstResponse开始接下来是它的父视图,如果没有父视图直到它的控制器(如果有的话)再到window和application。

initial object可能是hit-test view或者是first responder,没有处理事件。UIkit就会将该事件传递给next responder下一个响应者,每个响应者通过调用

-nextResponder方法决定是处理该事件还是向响应链的上层传递,直到某个响应者处理了该事件或者没有响应者了为止。

不建议直接给nextResponder发消息即:

  1. [self.nextResponder touchesBegan:touches withEvent:event];  

而建议调用父类的实现,让UIKit来帮我们做,因为默认的实现是将事件沿着响应链继续向上传递到下一个responder。

  1. [super touchesBegan:touches withEvent:event];  

当一个事件发生需要处理时,会让合适的对象去处理。如果是触摸事件的话,该对象就是hit-test view。如果是其他事件,该对象指的就是第一响应者(响应链中)。响应链是一个比较大的范畴,在触摸事件中,hit-test view就是响应链中的第一响应者。也就是说在触摸事件中通过hitTest:withEvent:方法找到的hit-test view就是第一响应者。

============= 201905更新===============

ios事件处理机制与安卓的对比 

在iOS里面事件会因为透明度为0而不能处理/传递事件,在android里面却可以。

iOS里面如果父控件设置不能交互那么上面的子控件也不能获得事件,而android里面即使服布局设置enable为false或者clickable设置为false,上面的子空间均可接收到事件。

iOS里面没有view和view容器之分,而android里面有View和ViewGroup之分,所以事件分发也稍麻烦,

=====================================

相关传送门:

iOS触摸事件

iOS触摸事件哦

https://www.jianshu.com/p/2f664e71c527

android里面事件传递的过程: Android之View和ViewGroup事件分发

ViewGroup传递的时候经历了dispatchTouchEvent--->onInterceptTouchEvent

从后往前遍历子View,如果子View可以处理事件返回true。传递到此结束

如果子View不能处理该事件,自己处理....onTouchEvent

View处理事件

dispatchTouchEvent—> onTouch –-> onTouchEvent

设置监听onTouch返回了true  或者返回了false但是onTouchEvent返回true都可以认为子View成功处理了事件

若返回了false,事件会往上抛,那么该ViewGroup会调用自己的onTouchEvent处理事件。

原文地址:https://www.cnblogs.com/isItOk/p/4876051.html