一.基本概念
-
UIEvent
一个UIEvent对象描述一次用户交互行为。例如:用户点击手机屏幕、摇晃手机以后,系统都会收到UIEvent事件。本文仅介绍Touch事件。
**事件类型(UIEventType)**:
UIEventTypeTouches触摸事件
UIEventTypeMotion加速计、传感器...
UIEventTypeRemoteControl耳机线控...
UIEventTypePresses遥控器或者其他设备(例如游戏控制器)...
-
UITouch
当用户用手指点击手机屏幕时,会产生UITouch对象。UITouch对象包括了触摸的时间、位置、阶段、所处的视图、窗口等信息。
-
UIResponder
可以响应和处理事件的对象。UIResponder之所以能处理事件,是因为其包含以下四个方法【此处的事件仅指触摸事件】:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//手指按下
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//手指移动
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//手指抬起
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//触摸事件被取消【手机来电话了系统会自动调用这个方法取消触摸事件,或者其他原因】
二.事件的产生和传递
-
产生和传递的过程
1.用户触摸屏幕,会产生一个触摸事件,该触摸事件会被加入到UIApplication管理的事件队列。【队列的特点是先入先出,先产生的事件系统会优先处理】
2.UIApplication取出最前面的事件进行分发【分发的顺序:父->子】,目的是为了寻找最适合处理此事件的对象。
-
如何寻找最适合处理此事件的对象
视图收到触摸事件以后,系统会调用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.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger 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;
}
}
// 循环结束,表示没有比自己更合适的view
return self;
}
UIView不同接收触摸事件的三种情况
a. userInteractionEnabled = NO
b.hidden = YES
c.alpha <= 0.01
-
实例
1.视图层级
视图层级.png
2.在每一个View和自定义的窗口中实现hitTest:withEvent方法,控制台输出如下:
CustomWindow-hitTest:withEvent://窗口
EventPropagattedView-hitTest:withEvent://ViewController_View
YView-hitTest:withEvent:
GView-hitTest:withEvent:
RView-hitTest:withEvent:
BView-hitTest:withEvent:
从而得知事件传递顺序如下:
UIWindow->ViewController_View->YView->GView->RView->BView
三.事件响应
-
响应链的响应顺序
响应者链:继承自UIResponder的对象组合以来的链条。
经过事件传递以后,系统找到了最适合处理此事件的View,就会沿着响应链进行传递,以对事件做出响应。响应链的传递顺序是【子到父】
响应顺序:如果触摸发生在UITextField上面->UIView->UIView->UIViewController->UIWindow->UIApplication->UIApplicationDelegate
事件的响应顺序为:子到父
-
响应事件的方法
UIResponder通过四个方法对事件进行响应:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//手指按下
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//手指移动
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//手指抬起
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//触摸事件被取消【手机来电话了系统会自动调用这个方法取消触摸事件,或者其他原因】
系统默认在这四个方法里面是不对事件进行处理的,只是默认实现事件的响应传递。若要实现自定义的响应操作,则需要重写这四个方法。每一个响应者对象(UIResponder对象)都有一个 nextResponder 方法,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的最佳响应者确定了,这个事件所处的响应链就确定了。nextResponder如下:
- UIView
若视图是控制器的根视图,则其nextResponder为控制器对象;否则,其nextResponder为父视图。- UIViewController
若控制器的视图是window的根视图,则其nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller。- UIWindow
nextResponder为UIApplication对象。- UIApplication
若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。
-
示例
示例.png
在图中的每一个View的touchsBegan:withEvent中实现:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@-%@",NSStringFromClass([self class]),NSStringFromSelector(_cmd));
[super touchesBegan:touches withEvent:event];
}
点击BlueView控制台输出如下:
BView-touchesBegan:withEvent:
RView-touchesBegan:withEvent:
YView-touchesBegan:withEvent:
EventPropagattedView-touchesBegan:withEvent:
EventPropagattedViewController-touchesBegan:withEvent:
CustomWindow-touchesBegan:withEvent:
四.总结
- 事件的传递顺序
从父到子,如果父不能接收触摸事件,那么子也不能接收触摸事件。
- 事件的响应顺序
从子到父
网友评论