iOS事件,原来如此

精简地说:iOS事件分为传递和响应两个部分。

事件传递(建立传递链):

iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为hit-test view。
hittest的目的就是找到最终的传递链

hitTest:withEvent:流程如下:

  1. 先判断当前视图hidden=YES,userInteractionEnabled=NO,alpha<0.01等属性,如果满足其中之一,返回nil。
  2. 再看当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
  3. 若返回NO,则hitTest:withEvent:返回nil;
  4. 若返回YES,则向当前视图的【一级子视图(subviews)递归发送】hitTest:withEvent:消息,所有子视图的遍历顺序是【从subviews数组的末尾向前遍历】,直到有子视图返回非空对象或者全部子视图遍历完毕;
  5. 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;如所有子视图都返回非,则hitTest:withEvent:方法返回自身。

提醒:
hittest返回nil表示该条传递链已经终止,不是正确的传递链。
hittest返回非nil对象表示已经找到传递链的叶子节点,即找到正确的传递链。

hitTest:withEvent:遇到以下会返回nil。
1.hidden=YES的视图。
2.userInteractionEnabled=NO的视图(注意userInteractionEnabled是影响子视图事件传递,但不影响兄弟视图)
3.alpha<0.01的视图。
4.显示区域超过父视图bounds区域的视图。那么超出区域不能识别。当然,可以重写pointInside:withEvent:方法来识别。

hitTest:底层实现

- (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;
}

结合例子分析:


例子.png

举例分析:用户点击了View D,下面结合上图介绍hit-test view的流程:
1、A是UIWindow的根视图,因此,UIWindwo对象会首先对A进行hit-test;
2、显然用户点击的范围是在A的范围内,因此,pointInside:withEvent:返回了YES,这时会继续检查A的子视图;
3、这时候会有两个分支,B和C:
C在subviews的末尾,先递归遍历C。点击的范围在C内,即C的pointInside:withEvent:返回YES;
4、这时候有D和E两个分支:
点击的范围不再E内,因此E的pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;
点击的范围在D内,即D的pointInside:withEvent:返回YES,由于D没有子视图,因此,D的hitTest:withEvent:会将D返回,再往回回溯,就是C的hitTest:withEvent:返回D--->>A的hitTest:withEvent:返回D。
即A->C->E(返回nil)->D(返回D)->C(返回D)->A(返回D)
至此,本次点击事件的第一响应者就通过响应者链的事件分发逻辑成功的找到了。
不难看出,这个处理流程有点类似二分搜索的思想,这样能以最快的速度,最精确地定位出能响应触摸事件的UIView。
另外hittest可能会被调用2-3次,这里请不要再hittest作业务相关操作,否则会导致执行多次。

事件响应(回溯响应链):

经过hittest后,我们已经找到了链尾【第一响应者】,这时候开始回溯响应操作。响应链的关系如图:
请注意:响应链在传递链的基础上增加了UIViewController。

UIResponder.png

NextResponder:
1.当一个view被添加到superView上的时候,它的nextResponder就会被指向它的superView;
2.当vc被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller;
(概括前两者就是:如果当前这个view是控制器的self.view,那么控制器就是上一个响应者 如果当前这个view不是控制器的view,那么父控件就是上一个响应者)
3.vc的nextResponder会被指向self.view的superView。
4.最顶级的vc的nextResponder指向UIWindow。
5.UIWindow的nextResponder指向UIApplication

在事件响应对象UIResponder(UIView和UIViewController都是继承UIResponder)中有对应的方法来分别处理这几个阶段的事件:
touchesBegan:NSArray<UITouch *>>withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:
Touch默认流程(以单击为例)是:从响应链叶子节点开始回溯,先是TouchBegan回溯,然后是TouchEnded回溯。

举例:对于触摸事件来说,UIApplication会首先把事件交给keyWindow,Window会将事件交给UIGestureRecognizer处理,如果UIGestureRecognizer识别了传递过来的事件,则交给相对应的target去处理,回溯终止,事件不会再传递!当然这里也可以重写touchBegan等方法手动让手势继续往superview传从而实现多级响应。
如果UIGestureRecognizer并没有识别传递过来的事件(可能是没有视图添加手势,也可能手势识别不成功),事件会传递到视图树形结构

touches方法实际上什么事都没做,UIView继承了它进行重写,就是把事件传递给nextResponder,相当于[self.nextResponder touchesBegan:touches withEvent:event]。所以当一个view没有重写touch事件,那么这个事件就会一直传递下去,直到UIApplication。如果重写了touch方法,这个view响应了事件之后,事件就被拦截了,它的nextResponder不会收到这个事件。这个时候如果想事件继续传递下去,可以调用[self.nextResponder touchesBegan:touches withEvent:event]

手势:

通过touches方法监听view触摸事件,有很明显的几个缺点:必须得自定义view、由于是在view内部的touches方法中监听触摸事件,因此默认情况下,无法让其他外界对象监听view的触摸事件、不容易区分用户的具体手势行为。
所以iOS把触摸事件做了封装, 对常用的手势进行了处理, 封装了6种常见的手势
UITapGestureRecognizer(敲击)
UILongPressGestureRecognizer(长按)
UISwipeGestureRecognizer(轻扫)
UIRotationGestureRecognizer(旋转)
UIPinchGestureRecognizer(捏合,用于缩放)
UIPanGestureRecognizer(拖拽)

UIControlEvent:

其实要了解UIControlEvent,必须简单说一下UITouch和UIEvent事件,都是和触摸相关,UIEvent是一系列UITouch的集合,在IOS中负责响应触摸事件。

UIControl是UIView的子类,当然也是UIResponder的子类。UIControl是诸如UIButton、UISwitch、UITextField等控件的父类,它本身也包含了一些属性和方法。

UIControl对象采用了一种新的事件处理机制,将触摸事件转换成简单操作,其实就是重写了UIResponder的方法中(如touchBegan:withEvent)中,即事件不再往上回溯响应。这样方便了事件处理,而不用每次都重写TouchBegan方法。
UIResponder可以参考这篇文章UIKit: UIResponder
比如我点击一个UIButton,即使你未添加UIControlEventTouchUpInside,它的父类touchBegan也不会被调用。因为UIControl重写的方法touchBegan:withEvent并未调用[super touchBegan:withEvent]

UITapGestureRecognzier:

UITapGestureRecognzier其实就是对各类复杂触摸操作响应过程的一个封装。
在六种手势识别中,只有一种手势是离散手势,它就是UITapGestureRecognzier。离散手势的特点就是一旦识别就无法取消,而且只会调用一次手势操作事件(初始化手势时指定的触发方法)。换句话说其他五种手势是连续手势,连续手势的特点就是会多次调用手势操作事件,而且在连续手势识别后可以取消手势。【下图是手势状态图】

手势状态.png

分别以UITap和UIPan两种手势说明流程:
UITap:TouchBegan回溯->Tap->TouchCancelled回溯。
UIPan:TouchBegan回溯->TouchMoved多次回溯->UIPanBegan-TouchCancelled回溯->UIPanChanged多次->UIPanEnded。

事件总结:
1.父视图不能接收事件,则子视图无法接受事件
2.子视图超出父视图的部分,不能接收事件
3.同一个父视图下,最上面的视图,首先遭遇事件,如果能够响应,就不向下传递事件。如果不能接收,事件向下传递

总结:

  1. iOS事件流程分为寻找响应链和响应链回溯,其中响应链回溯部分和android类似。
  2. 为了事件响应处理的方便,苹果又推出了UIControlEvent和UITapGestureRecognzier。它们都是针对响应链回溯过程。
  3. UITapGestureRecognzier和UIControlEvent不同,UIControlEvent不会响应父类的TouchBegan等操作,而UITapGestureRecognzier会响应。

参考:
iOS开发系列--触摸事件、手势识别、摇晃事件、耳机线控

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容