iOS之事件的传递和响应机制-原理篇

iOS之事件的传递和响应机制-原理篇,第1张

注 意 : 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

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

注 意 :默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互,需要设置UIImageView的userInteractionEnabled = YES。

1点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

2UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。

3窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。

例如,不想让蓝色的view接收事件,那么可以设置蓝色的view的userInteractionEnabled = NO;那么点击**的view或者蓝色的view所产生的事件,最终会由橙色的view处理,橙色的view就会成为最合适的view。

所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理!也就是说,如果蓝色视图不能处理事件,点击蓝色视图产生的触摸事件不会由被点击的视图(蓝色视图)处理!

注意:如果设置父控件的透明度或者hidden,会直接影响到子控件的透明度和hidden。如果父控件的透明度为0或者hidden = YES,那么子控件也是不可见的!

应用如何找到最合适的控件来处理事件?

1首先判断主窗口(keyWindow)自己是否能接受触摸事件

2触摸点是否在自己身上

3从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)

4如果没有符合条件的子控件,那么就认为自己最合适处理

详述:1主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上

2如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)

3遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1判断子控件能否接受事件,2点在不在子控件上)

4如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。

找到最合适的view后,就会调用该view的touches方法处理具体的事件。所以,只有找到最合适的view,把事件传递给最合适的view后,才会调用touches方法进行接下来的事件处理。找不到最合适的view,就不会调用touches方法进行事件处理。

注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。

两个重要的方法:

hitTest:withEvent: 方法

pointInside 方法

什么时候调用?

作用

注 意 :不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用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!

例如: whiteView有redView和greenView两个子控件。redView先添加,greenView后添加。如果要求无论点击那里都要让redView作为最合适的view(把事件交给redView来处理)那么只能在whiteView的hitTest:withEvent:方法中return selfsubViews[0];这种情况下在redView的hitTest:withEvent:方法中return self;是不好使的!

特殊情况:

谁都不能处理事件,窗口也不能处理。

只能有窗口处理事件。

return nil的含义:

hitTest:withEvent:中return nil的意思是调用当前hitTest:withEvent:方法的view不是合适的view,子控件也不是合适的view。如果同级的兄弟控件也没有合适的view,那么最合适的view就是父控件。

寻找最合适的view底层剖析之hitTest:withEvent:方法底层做法

/ hitTest:withEvent:方法底层实现/

hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。

pointInside:withEvent:方法判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

屏幕上现在有一个viewA,viewA有一个subView叫做viewB,要求触摸viewB时,viewB会响应事件,而触摸viewA本身,不会响应该事件。如何实现?

1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理

响应者链条: 在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。在iOS中响应者链的关系可以用下图表示:

响应者对象: 能处理事件的对象,也就是继承自UIResponder的对象

作用: 能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。

如何判断上一个响应者

响应者链的事件传递过程:

事件处理的整个流程总结:

1触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

2UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。

3主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

4最合适的view会调用自己的touches方法处理事件

5touches默认做法是把事件顺着响应者链条向上抛。

touches的默认做法:

事件的传递与响应:

1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。

2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃

3、在事件的响应中,如果某个控件实现了touches方法,则这个事件将由该控件来接受,如果调用了[supertouches…];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches…方法

如何做到一个事件多个对象处理:

因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。

事件的传递和响应的区别:

事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。

UIScrollView是依靠与其子视图(subview)之间的约束来确定ContentSize的大小 ,如果不设置好子view的宽高度约束的话,就会造成UISCrollView显示异常。

对于UIScrollView的subview来说,它的leading/trailing/top/bottom的space是相对于UIScrollView的contentSize而不是bounds来确定的,换句话说:UIScrollView与其subview之间相对位置的约束并不会直接用于frame的计算,而是会转化为对ContentSize的计算。(摘自

: >

自定义一个类BMNavigationBar继承UINavigationBar,重写layoutSubviews方法(记得初始化的地方改成自定义的BMNavigationBar)

override func layoutSubviews() {

superlayoutSubviews()

if #available(iOS110, ) {

for view in selfsubviews{

if(viewisKind(of:NSClassFromString("_UIBarBackground")!)) {

viewframe=selfbounds

}else if(viewisKind(of:NSClassFromString("_UINavigationBarContentView")!)){

var frame= viewframe

frameoriginy=20

viewframe= frame

}}}}

        如今我们但凡看到一块屏幕我们都会忍不住去点击,几乎每一块屏幕都能多点触控。我们用多点触控屏幕是那么自然,就像生来就有的技巧。那么在我们手指触碰屏幕的一瞬间,到底发生了什么呢?首先我们需要先了解事件类型。

        当我们 *** 作手机时,一般有触摸屏幕、摇晃手机、远程遥控三种方式,分别对应的事件类型是:

        1  触摸事件 (Multitouch events)

        2 运动事件  (Accelerometer events)

        3 远程控制  (Remote control events)

        当这些事件发生时,iOS会生成对应的响应链, 来查找第一响应对象并进行事件的分发,最后处理事件,完成相应 *** 作。下面我们接着看关于响应链的概念。

        响应链,顾名思义,就是有一系列响应对象的集合成的一个层次结构。那什么又是响应对象呢?Cocoa里面规定:凡是继承于UIResponder或者UIResponder的子类的对象都可以作为 响应对象 ,比如UIApplication、UIViewController和UIView。

        在响应用户触摸等事件中,APP具体会通过下面三步来完成 *** 作:

        1   生成事件 。当用户点击屏幕时,会产生一个触摸事件,并放入由Application管理的事件队列中,然后在队列中取出最前面的事件交给Window处理。

        2   查找第一响应对象 。Window收到事件后会在视图层次结构中找到最适合的一个视图来处理事件,通常一个窗口中最适合处理当前事件的对象称为第一响应对象。

        3   处理事件 。通常最后是第一响应对象处理事件,如果第一响应对象无法处理事件,就会把事件传递给下一个响应对象,直到Application。如果Application也无法处理,那就丢弃掉此事件。

        在上述系列 *** 作中,所参与到的UIApplication、UIViewController和UIView就作为响应对象构成这次事件的 响应链 。

        当Window收到事件后,会用一种类似二分法的方式来查找第一响应对象,通常就是用户点击处最上层的一个View。

        Window实例对象会首先在它的内容视图上调用hitTest:withEvent:,此方法会在其视图层级结构中的每个视图上调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果pointInside:withEvent:返回true,则继续逐级调用,直到找到touch *** 作发生的位置,这个视图也就是要找的hit-test view。

        hitTest:withEvent:方法的处理流程如下:

        首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内,若返回false,则对应hitTest:withEvent:返回nil; 若返回true, 则向当前视图的所有子视图发送hitTest:withEvent:消息,直到有子视图返回非空对象或者全部子视图遍历完毕;若最后一层某个子视图pointInside:withEvent:方法返回true,则对应hitTest:withEvent:方法返回此对象,直到把此对象依次向上返回到Application则处理结束。

       我们结合一个实例来加深理解:

假设View A 是Window的根视图,用户点了View E之后:

1 Window首先会对View A进行 hit-test ,具体表现为View A调用hitTest:withEvent:,而此方法进而会调用pointInside:withEvent:方法,显然返回true,并对View A所有的子视图(View B,View C)进行hit-test;

2 View B调用pointInside:withEvent:方法,返回false,对应hitTest:withEvent:返回nil;

3 View C调用pointInside:withEvent:方法,返回true,则对View C所有的子视图(View D,View E)进行hit-test;

4 View D调用pointInside:withEvent:方法,返回false,对应hitTest:withEvent:返回nil;

5 View E调用pointInside:withEvent:方法,返回true,而且View E没有子视图了,则hitTest:withEvent:返回View E,再往回溯,View C对应hitTest:withEvent:也返回View E,View A也返回View E,这样Application就知道了View E是第一响应对象。

        当Application就知道了第一响应对象后,就会把事件交给第一响应对象来处理,如果第一响应对象能顺利处理事件,则整个响应结束,但是第一响应对象如果无法处理事件,就会把事件传递给下一个响应对象(nextResponder),一直沿着响应链向上回溯。那么第一响应对象的下一响应对象是谁呢?我们结合下图进行解释:

左边为例:

1 如果接收到事件的初始View无法处理事件, 那么这个事件会交给他的SuperView, 因为他不是viewController等级中的最高级View。

2 SuperView尝试处理事件,如果SuperView无法处理,则这个事件会交给他的SuperView,因为他不是viewController等级中的最高级View。

3 这样事件就传递到viewController等级中的最高级View,如果最高级View不能处理就会彻底给viewController。

4 viewController尝试处理事件,如果无法处理就传递给Window,Window尝试处理,无法处理就传递给Application。

5 Application尝试处理,如果无法处理就就丢弃该事件。

总之,当view无法处理事件时,如果是最高级view,并存在viewController,则传递给viewController,否则传递给SuperView,继续往上尝试处理事件。

view -> ViewController -> window -> Application -> 丢弃

1 遍历查找最佳响应者时,从所有子视图的最上层view往下遍历(从subviews数组最后一个元素往前便利)。

2 遍历查找最佳响应者时,当一个子视图告诉OS没有被点击时,则它的子视图不会被检查(类似二分法)。

3 子视图在父视图边界外时,并且父亲的clipsToBounds属性为false时,子视图接受不到事件。

4 一个UIWindow对象在某一时刻只能有一个响应者对象可以成为第一响应者。

5 成为第一响应者必须要canBecomeFirstResponder,才能becomeFirstResponder。

6 手动设置某个view becomeFirstResponder时,当有事件发生时,该view不一定最先响应。比如点击button时会触发自身响应,而不管有无其他becomeFirstResponder的view。

7 第一响应者主要体现在,事件发生时没有响应者出来处理事件,这时候第一响应者就会尝试处理事件。

版权声明:本文为博主原创文章,未经博主允许不得转载。

for (int>0;>3; i++) {

UIView v = [[UIView alloc] initWithFrame:CGRectMake(30i, 30i+50, 80, 80)];

vbackgroundColor =[[UIColor alloc] initWithRed:arc4random()%256/2550 green:arc4random()%256/2550 blue:arc4random()%256/2550 alpha:arc4random()%1001/9990];

[selfwindow addSubview:v];

[v release];

}

UIView v =[[UIView alloc] initWithFrame:CGRectMake(20, 20, 20, 80)];

vbackgroundColor =[UIColor yellowColor];

//插入子视图 index就是数组的下标 (实际上是在selfwindowsubview 视图中添加子视图)

[selfwindow insertSubview:v atIndex:1];

//插入子视图 放在某视图上面

UIView v1 =[[UIView alloc] initWithFrame:CGRectMake(40, 20, 20, 70)];

v1backgroundColor = [UIColor orangeColor];

[selfwindow insertSubview:v1 aboveSubview:selfwindowsubviews[2]];

//插入子视图 放在某视图下面

UIView v2 =[[UIView alloc] initWithFrame:CGRectMake(0, 20, 20, 100)];

v2backgroundColor = [UIColor greenColor];

[selfwindow insertSubview:v2 belowSubview:selfwindowsubviews[0]];

//把某视图置顶(不进行插入)调用完毕后 该视图在最顶层

[selfwindow bringSubviewToFront:selfwindowsubviews[3]];

//把某视图置底(不进行插入)调用完毕后 盖世兔在最底层

[selfwindow sendSubviewToBack:selfwindowsubviews[5]];

//某视图交换位置后 索引值也会发生变化

//把某个视图调换位置

[selfwindow exchangeSubviewAtIndex:0 withSubviewAtIndex:4];

//判断视图是否是另一个视图的子视图

NSLog(@"%d",[selfwindowsubviews[0] isDescendantOfView:selfwindow]);

以上就是关于iOS之事件的传递和响应机制-原理篇全部的内容,包括:iOS之事件的传递和响应机制-原理篇、UIScrollView 与 SnapKit 的使用、ios11 Swift 自定义导航条上移20点解决方法等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/9566313.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-29
下一篇 2023-04-29

发表评论

登录后才能评论

评论列表(0条)

保存