本次笔记主要是整理一下关于 iOS 中关于事件传递和响应机制,参考了一些其他资料加上自己的理解。
事件 Events
定义是 objects sent to an app informing user actions.
iOS 中的事件
iOS 有三种事件类型:
- 触摸事件:单点、多点触控以及各种手势操作;
- 加速计事件:重力、加速度传感器等;
- 远程控制事件:远程遥控iOS设备多媒体播放等;
响应者对象 UIResponder
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。
- UIApplication
- UIViewController
- UIView
以上三个类都继承自 UIResponder ,所以都可以接收并处理事件。
UIResponder 中提供了以下对象方法来处理接收到的事件。
//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;
当用手指触摸 UIView 的一个实例的时候,就会产生触摸事件 UIEventTypeTouches,而接收对象 UIView 就是一个Responder object。 一个事件可以被多个 Responder 接收,第一个接收事件的对象就是 firstResponder。
触摸事件
UIView 的触摸事件处理方法有以下几种:
// UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件
// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指离开view,系统会自动调用view的下面方法- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch对象
以上四个方法是由系统自动调用的,所以可以通过重写该方法来处理一些事件。需要注意的是重写以上四个方法,如果是处理UIView的触摸事件,必须要自定义UIView子类继承自UIView。没有UIView的 .m 文件,只能通过子类继承父类,重写子类方法的方式处理UIView的触摸事件。如果是处理UIViewController的触摸事件,那么在控制器的.m文件中直接重写即可。
UIView 的拖拽事件
实现拖拽是让UIView随着手指的移动而移动,重写touchsMoved:withEvent:
,需要用到UITouch
对象。
- 手指触摸屏幕时,会创建一个与手指相关的UITouch对象
- 一根手指对应一个UITouch对象
- 如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
- 手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
UITouch对象的作用:
- 保存着跟手指相关的信息,比如触摸的位置、时间、阶段
- 手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
- 手指离开屏幕时,系统会销毁相应的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
该方法记录了前一个触摸点的位置
iOS 中事件的产生和传递
事件传递
- 发生触摸事件后,系统会将该事件 UIEvent 加入到一个由UIApplication管理的事件队列中
- UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通
常,先发送事件给应用程序的主窗口(keyWindow) - 主窗又会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理
过程的第一步 - 找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理
- touchesBegan...
- touchesMoved...
- touchedEnded...
举例:
- 触摸事件的传递是从父控件传递到子控件
- 点击了绿色的view:
UIApplication -> UIWindow -> 白色 -> 绿色 - 点击了蓝色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色 - 点击了黄色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色 -> 黄色 - 如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件
- 如何找到最合适的控件来处理事件?
- 自己是否能接收触摸事件?
- 触摸点是否在自己身上?
- 从后往前遍历子控件,重复前面的两个步骤
- 如果没有符合条件的子控件,那么就自己最适合处理
找到最合适的 view 后,就会调用该 View 的 touches 方法处理具体的事件,只有找到最合适的 View,把事件传递给 View 后才会调用 touches 等方法来进行事件处理。
具体如何找到最合适的 View 来处理事件
整个过程类似递归调用。
举例说明,假如点在黄色 View 上,触摸事件是从父控件传递到子控件,UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色 -> 黄色。点在黄色View上,同时也点在白色、橙色和蓝色上,系统如何判断是点在黄色上呢?首先点到黄色后会产生一个触摸事件,系统会将该事件 UIEvent 加入到一个由 UIApplication 管理的事件队列中,UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow),主窗又会在视图层次结构中找到一个最合适的视图来处理触摸事件。关键来了,UIWindows首先判断自己是否接收触摸事件,及触摸点是否在自己身上,这两点满足后,便开始从后往前遍历子控件,注意此时 UIWindow 的子控件只有白色 View,然后判断白色 View 是否满足 1.自己是否接收触摸事件 2.触摸点是否在自己身上,两点满足后开始执行 3.从后往前遍历子控件。此时白色 View 有两个子控件,绿色 View 和橙色 View,按照从后往前,即后添加的橙色 View 。先判断橙色 View 的是否满足三个条件,橙色判断完后在判断绿色,绿色判断的时候不满足触摸点在自己身上,终止。橙色判断满足前两条,然后开始第三条,遍历橙色的子控件,然后红色不满足,蓝色继续判断,前两条满足,然后遍历蓝色的子控件,只有黄色 View,此时黄色满足2条,但是没有子控件,循环停止,最终返回的就是黄色 View。可以看出,如果父控件不能接收触摸事件,触摸事件是不能传递到子控件的。
底层实现
两个方法
- hitTest:withEvent:方法
- pointInside方法
hitTest:withEvent:
方法用来寻找并返回最合适的 View,可以通过重写该方法返回指定的 View 作为最合适的 View。这样可以拦截事件的传递过程,指定 View 来处理事件。
事件传递给谁就会调用谁的hitTest:withEvent:
方法,如果hitTest:withEvent:
方法中返回nil
,那么调用该方法的控件本身和其子控件都不是最合适的 view,也就是在自己身上没有找到更合适的 view。那么最合适的 view 就是该控件的父控件。
事件传递给窗口或控件后,首先要调用hitTest:withEvent:
方法来寻找最合适的 View。其过程是先传递事件,view 接收到该事件后开始调用hitTest:withEvent:
方法。假如层级结构A->B->C,A是最顶层的 View,A收到事件后,会根据hitTest:withEvent:
来查找最合适的 View,此时会返回自己为合适的 view,要查找最合适的 View 就需要继续传递事件,即使 A 是最合适的 View,也需要进一步传递事件,因为此时还不知道自己是最合适的 View,将事件传递给 B 之后,B 就会调用自己的 hitTest:withEvent:
方法来查找最合适的 View,仍然返回自己为合适的 View,继续寻找最合适的 View,然后事件传递给 C,C 调用后有子控件,则 C 就是最合适的 View。
想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件。
hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。
UIView 不接收触摸实践的三种情况
- 不支持交互
userInteractionEnabled = NO
- 隐藏
hidden = YES
- 透明度
alpha = 0.0 - 0.01
事件的响应
上述的事件传递,只是根据 View 是否接收触摸事件及触摸点的位置来找到最合适的 View 来处理事件,找到最合适的 View 后会调用 touches 等方法来作具体的事件处理,但是最合适的 View 可能不具备 touches 方法来处理事件,此时机会存在一个事件响应的问题,最合适的 View 不一定能响应该事件,引出了响应链的传递过程,一般是讲事件交给上一个响应者来进行处理。
响应者链条示意图
- 响应者链条: 是由多个响应者对象连接起来的链条
- 作用:可以很清楚的看见每个响应者之间的联系,并且可以让一个事件处理给多个对象处理
- 响应者对象:能处理事件的对象
事件传递的完整过程
- 先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件 来处理这个事件。
- 调用最合适控件的touches....方法
- 如果调用了
[super touches....]
;就会将事件顺着响应者链条往上传递,传递
给上一个响应者 - 接着就会调用上一个响应者的touches....方法
如何判断上一个响应者
- 如果当前这个view是控制器的view,那么控制器就是上一个响应者
- 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
响应者链的事件传递过程
- 如果view的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它 的父视图
- 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件 或消息传递给window对象进行处理
- 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
- 如果UIApplication也不能处理该事件或消息,则将其丢弃
事件的传递和响应
- 当一个事件发生后首先发生的是事件的传递,事件会从父控件依次传给子控件,Application -> UIWindow -> UIView -> initial view,此过程是寻找最合适的 view 的过程
- 找到最合适的 view 后,就是事件的响应过程,先看 initial view 是否能够处理事件,如果不能将传给上级视图,传到视图控制器,一直传到 window 和 application,都不能处理则丢弃
- 事件的响应中,如果某个控件实现了
touches
方法,则这个事件将由该控件来处理,如果调用了[supertouches….]
;就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches
方法。 - 通过重写自己的
touches
方法和父控件的touches
方法可以达到一个事件多个对象处理的目的
** 事件的传递和响应的区别:**
- 事件的传递是从上到下(父控件到子控件)
- 事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。
Demo
关于事件传递和响应链条的小Demo:事件传递及响应Demo