问题
-
事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)
-
找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)
-
其中重点和难点是:
- 1.如何寻找最合适的view
- 2.寻找最合适的view的底层实现(hitTest:withEvent:底层实现)
iOS中的事件
- 触摸事件 【本文只讨论触摸事件】
- 加速计事件
- 远程控制事件
-
响应者对象(UIResponder)
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”
。以下都是继承自UIResponder的,所以都能接收并处理事件。 -
UIViewController
@interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>
- UIView
@interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate>
事件的处理往往需要 UITouch
对象
事件的产生和传递
- 事件的产生
点击一个UIView或产生一个触摸事件A,那么这个事件就产生了,这个触摸事件A会被添加到由UIApplication管理的事件队列
中(即,首先接收到事件的是UIApplication,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列)
- 事件的产生
-
- 事件的传递
-
2.1 触摸事件的传递就是把事件传递到处理该事件最合适的view上
注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
-
2.2 如何找到最合适的控件来处理事件
1)UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)2)判断主窗口(keyWindow)自己是否能接受触摸事件,如果能,则判断触摸点是否在keyWindow身上,如果在,则UIWindow将事件向下分发,即UIView
3)判断触摸点是否在自己身上,若果在,怎执行第4步
4)子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行3步骤)
4)view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
6)如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
-
2.3 寻找最合适的view底层剖析
-
两个最重要的方法
hitTest:withEvent:方法, pointInside方法- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
hitTest:withEvent 的说明
- 调用的条件 :只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
-
作用 :寻找并返回最合适的view
能够响应事件的那个最合适的view
注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法
- 拦截事件的处理
正因为hitTest:withEvent:方法
的返回值为:响应事件的那个最合适的view,所以,通过 复写 该方法可以 拦截事件
新建一个MView,在.m 文件实现如下:
// 开始触摸时就会调用一次这个方法 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"摸我干啥!"); } // 手指移动就会调用这个方法 // 这个方法调用非常频繁 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"哎呀,不要拽人家!"); } // 手指离开屏幕时就会调用一次这个方法 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"手放开还能继续玩耍!"); } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { NSLog(@"point.x = %f",point.x); NSLog(@"point.y = %f",point.y); NSLog(@"%@",event); return nil; }
上述创建的view 对象点击 输出如下
point.x = 70.000000 point.y = 48.666672 <UITouchesEvent: 0x600001df1dd0> timestamp: 1620.54 touches: {()} point.x = 70.000000 point.y = 48.666672 <UITouchesEvent: 0x600001df1dd0> timestamp: 1620.54 touches: {()}
因为:
hitTest:withEvent:
方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。-
所以事件的传递顺序是这样的
产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view -
变形 - 返回 self.superview
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { NSLog(@"point.x = %f",point.x); NSLog(@"point.y = %f",point.y); NSLog(@"%@",event); return self.superview; }
输出与上面的一样
总结: 即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。- hitTest:withEvent:方法底层实现
- (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] == NO) return nil; // 3.从后往前遍历子控件数组 int count = (int)self.subviews.count; for (int i = count - 1; i >= 0; i--) { // 获取子控件 UIView *childView = self.subviews[i]; // 坐标系的转换,把窗口上的点转换为子控件上的点 // 把自己控件上的点转换成子控件上的点 CGPoint childP = [self convertPoint:point toView:childView]; UIView *fitView = [childView hitTest:childP withEvent:event]; if (fitView) { // 如果能找到最合适的view return fitView; } } // 4.没有找到更合适的view,也就是没有比自己更合适的view return self; } // 作用:判断下传入过来的点在不在方法调用者的坐标系上 // point:是方法调用者坐标系上的点 //- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event //{ // return NO; //}
-
pointInside:withEvent:方法
pointInside:withEvent:方法判断点在不在当前view上(方法调用者的坐标系上)
如果返回YES,代表点在方法调用者的坐标系上;
返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件
-
-
UIView不能接收触摸事件的三种情况:
-
不允许交互:
userInteractionEnabled = NO -
隐藏:
如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件 -
透明度
:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
-
-
3.事件的响应
-
触摸事件处理的整体过程
- 用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
- 找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…
- 这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理
-
如何判断上一个响应者
如果当前这个view是控制器的view,那么控制器就是上一个响应者
如果当前这个view不是控制器的view,那么父控件就是上一个响应者 -
响应者链的事件传递过程:
- 如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
- 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
- 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
- 如果UIApplication也不能处理该事件或消息,则将其丢弃
//只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件 // 上一个响应者可能是父控件 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ // 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理 [super touchesBegan:touches withEvent:event]; // 注意不是调用父控件的touches方法,而是调用父类的touches方法 // super是父类 superview是父控件 }
-
总结
-
事件的传递与响应:
-
事件的传递
当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。 -
事件的响应
首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃 -
在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[supertouches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法
-
如何做到一个事件多个对象处理:
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ // 1.自己先处理事件... NSLog(@"do somthing..."); // 2.再调用系统的默认做法,再把事件交给上一个响应者处理 [super touchesBegan:touches withEvent:event]; }
-
事件的传递和响应的区别:
事件的传递是 从上到下(父控件到子控件)
事件的响应是 从下到上(顺着响应者链条向上传递:子控件到父控件。
-
网友评论