注 意 : 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
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点解决方法等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)