iOS 的事件可以分为三种
- Touch Events(触摸事件)
- Motion Events(运动事件,比如重力感应和摇一摇等)
- Remote Events(远程事件,比如用耳机上得按键来控制手机)
我们主要是研究Touch Events(触摸事件)
,Touch Events事件的整个过程可以分为 传递和响应 2 个阶段,
- 传递: 是当我们触摸屏幕时,为我们找出最适合的 View,
- 响应: 当我们找出最适合的 View 后,此时只是找到了最合适的 View,但未必 此 View 可以响应此事件,所以需要继续找出能响应此事件的 View
查找第一响应者时,有两个非常关键的API
,查找第一响应者就是通过不断调用子视图的这两个API
完成的
调用方法,获取到被点击的视图,也就是第一响应者。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
hitTest:withEvent:
方法内部会通过调用这个方法,来判断点击区域是否在视图上,是则返回YES
,不是则返回NO
。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
每当手指接触屏幕,操作系统会把事件传递给当前的 App
, 在 UIApplication
接收到手指的事件之后,就会去调用UIWindow的hitTest:withEvent:
,看看当前点击的点是不是在window内,如果是则继续依次调用其 subView
的hitTest:withEvent:
方法,直到找到最后需要的view。调用结束并且hit-test view
确定之后,便可以确定最合适的 View
。
应用如何找到最合适的控件来处理事件?
- 1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
- 2.判断触摸点是否在自己身上
- 3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
- 4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
- 5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
- 6、
UIViewController
没有hitTest:withEvent:
方法,所以控制器不参与查找响应视图的过程。但是控制器在响应者链中,如果控制器的View不处理事件,会交给控制器来处理。控制器不处理的话,再交给View的下一级响应者处理。
UIView不能接收触摸事件的三种情况:
- 不允许交互:userInteractionEnabled = NO
- 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
- 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明
下面是 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
方法的内部实现
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01 || ![self pointInside:point withEvent:event] || ![self _isAnimatedUserInteractionEnabled]) {
return nil;
} else {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event];
if (hitView) {
return hitView;
}
}
return self;
}
}
hitTest:withEvent:方法
- 什么时候调用?
- 只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法
- 作用
- 寻找并返回最合适的view(能够响应事件的那个最合适的view)
拦截事件的处理
- 正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
- 不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view
- 通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件
事件传递给谁,就会调用谁的hitTest:withEvent:方法。 注意:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。
在iOS中不是任何对象都能处理事件,只有继承了UIResponder
的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder
的,所以都能接收并处理事件。
- UIApplication
- UIViewController
- UIView
那么为什么继承自UIResponder的类就能够接收并处理事件呢? 因为UIResponder中提供了以下4个对象方法来处理触摸事件。
UIResponder内部提供了以下方法来处理事件触摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
当我们知道最合适的 View 后,事件会 【子view -> 父view,控制器view -> 控制器】
来找出合适响应事件的 View,来响应相关的事件。如果当前的 View 有添加手势,那么直接响应相应的事件,不会继续向下寻找了,如果没有手势事件,那么会看其是否实现了4个触摸方法
响应者链的事件传递过程:
- 1、如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
- 2、在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
- 3、如果window对象也不处理,则其将事件或消息传递给UIApplication对象
- 4、如果UIApplication也不能处理该事件或消息,则将其丢弃
示意图说明:白色 view 是蓝色 view 的父视图;蓝色 view 是橙色 view 的父视图。
点击重叠区,只有蓝色 view(既父视图)响应事件。
一个最简单的办法是将子视图的 isUserInteractionEnabled 设置为 false ;也可以在子视图的 hitTest(_:with:) 方法里面返回 nil 或 superview ,可以达到同样的效果。
需求二:点击屏幕上的任意地方;只有蓝色 view 响应事件。
一个最简单的办法是在蓝色 view 的 hitTest(_:with:) 方法里返回 self 。当事件传递到蓝色 view 时,返回自己做为最适合触发事件的控件。
为什么 父View 关闭了事件响应时,子View 就无法响应事件?
因为在事件传递的时,先到父view,当父view无法响应事,直接就跳过了遍历其子view,故只要父类关闭了事件,子 view 就已经没有机会响应事件了。
如何扩大 Button 的点击范围?
扩大点击范围,无非就是想本来没有点击 btn 但想让 btn 响应事件,那么可以在 hitTest 方法中做适当的操作,当满足xxx条件时,强行返回 btn 来达到最佳点击范围的效果,相关的实现可以自行 Google ,有一些较优雅而简洁的方式。
如何让 父View 和 子View 同时响应同一事件?
父View 和 子View同时响应同一事件,默认当点击子view时,如果ziview可以处理事件,那么其他父view 是不会响应的,但是在 父view 传到 子view 时我们在 hitTest 方法中是清楚知道的,使用可以在这里做相关的操作便实现了子view 和父view 同时响应事件的效果。
为什么子View 关闭了事件,但其 父View 开启事件的情况下,点击 子View 时,父View 可以响应事件
子view关闭了事件,事件的传递是 父view 到子view,在 父view时,父view可以响应,那么会继续访问其 子view是否可以响应,如果此时子view不可以响应,那么他会直接返回 父view,所以 子View 关闭了事件 父View 正常执行事件是必然的。