iOS---事件传递和响应机制
iOS 中的事件
- 触摸事件
- 加速计事件
- 远程控制事件
响应者对象(UIResponder)
只有继承 UIResponder 的对象才能响应事件
- UIApplication
- UIViewControl
- UIView
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;
touchesBegan 这个方法,如果是两个手指同时触摸,这个方法只会调用一次,方法中包含两个 UITouch 的对象 , 如果是一前一后的触摸,就会调用两次这个方法,方法中包含一个 UITouch 对象
注意:
1.想要处理 UIView 的触摸事件,就要继承 UIView ,然后重写 UIView 的触摸事件方法
2.UIViewControl 在其触摸事件方法中就可以处理
UITouch 对象
UITouch 的属性
触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view
;
短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
记录了触摸事件产生或变化时的时间,单位是秒@property(nonatomic,readonly) NSTimeInterval timestamp;
当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;
UITouch 的方法
(CGPoint)locationInView:(UIView *)view;
// 返回值表示触摸在view上的位置
// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
// 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
(CGPoint)previousLocationInView:(UIView *)view;
// 该方法记录了前一个触摸点的位置
- 当用户用一根手指触摸屏膜时,就会创建一个 UITouch 对象
- 一根手指对应一个 UITouch 对象
- 当手指移动时,系统会更新 UITouch 对象的位置等信息
- 当手指离开时,相应的 UITouch 对象就会销毁
IOS 的事件产生和传递
事件产生
- 发生触摸事件后,系统会把事件加入到由 UIApplication 管理的事件队列
- UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去处理,通常是先发给应用程序的KeyWindow
- KeyWindow 会找其视图层结构找到一个最适合的View 来处理事件
事件的传递
UIApplication -> Window -> 寻找最合适的View
如何寻找最合适的 View?
- 首先判断 KeyWind 是否能接受触摸事件
- 判断触摸点是否在窗口身上
- 从后往前遍历子控件,重复上述两个步骤
- 遍历到最合适的 View
- 如果没有最合适的 View ,那么就由 KeyWind 处理
⚠️注意点:
- 透明度 < 0.01 的控件,是不能接受触摸事件
- 如果父控件不能接收事件,那么子控件也不能
- 不允许交互:userInteractionEnabled = NO
- 不管控件能否接收触摸事件,主要有触摸,就会产生事件,只是这个事件会不会被处理
如何寻找最合适的 View 的底层揭秘
hitTest:withEvent:
pointInside
hitTest:withEvent:方法
- 调用:主要有事件传递给一个控件,控件就会调用该方法
- 作用:找到最合适的 View ,并返回该 View
拦截事件的处理
- 正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
- 不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。
- 通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
如果想指定 View 作为最适合的 View,有以下两种方法
- 在父控件的
hitTest:withEvent:
返回子控件 - 在子控件的
hitTest:withEvent:
返回自
⚠️注意点:
推荐方法一,不推荐方法二的原因是有可能不能成功返回子控件,如果触摸点不在子控件,而在另一个子控件,那么没办法返回该子控件。
事件传递的正真顺序
- 产生触摸事件
- UIApplication 事件队列
- [UIWindow hitTest:withEvent:];
- 返回更合适的 View
- [子控件 hitTest:withEvent:];
- 返回更合适的 View
#import "WSWindow.h"
@implementation WSWindow
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// point:是方法调用者坐标系上的点
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1.判断下窗口能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha = 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;
//}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
@end
pointInside:withEvent:方法
pointInside:withEvent:方法判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件
事件响应
响应者链条:是由多个响应者连接起来的链条。
事件响应的流程
1.找到最合适的 View 的时候,就会调用自己 touchs 的方法处理事件。
2.touches默认做法是把事件顺着响应者链条向上抛.
2.1 首先看 initail View 能不能处理事件
2.2 不能就把事件传递给父控件,继续判断
2.3 没有一个控件能处理就抛给 Window
2.4 Window 处理不了,最后抛给 UIApplication
2.5 UIApplication 处理不了,把事件丢弃。
如果以上一个响应者重写了 touches 方法,就能处理事件
如何做到一个事件多个对象处理
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event];
}
事件传递和事件响应的过程总结
- 事件产生,添加到 UIApplication 的事件队列
- UIApplication 把事件队列最前的事件传递给UIWindow
- UIWindow 调用
hitTest:withEvent:
返回一个合适的 View - View 继续调用
hitTest:withEvent:
找到一个最适合的 View - 事件传递给最适合的 View 就开始响应事件
- 如果最适合 View 不能处理事件,把事件抛给父控件
- 一直都没有控件可以处理事件,就把事件抛给 UIWindow
- UIWindow 处理不了就抛给 UIApplication
- 最后处理不了就把事件抛弃。