(UIKit Framework) UITouch事件的处理
iOS系统在运行过程中,会收到各种不同的事件,包括touch事件
,motion事件
,remote control事件
以及press 事件
。
-
touch事件
指的是与设备屏幕间的交互 -
motion事件
指的是设备运动相关的事件,例如摇一摇等; -
remote control
事件指的是收到外设(例如耳机)发出的命令,例如耳机控制音视频的播放; -
press 事件
指的是游戏手柄,apple TV遥控器等有物理按钮的设备间的交互。
我们这里只讨论touch事件
与iOS设备屏幕的交互。
当点击屏幕时,iOS系统首先会收到touch事件
并分派到相应的app,然后从UIWindow
开始自下而上遍历图层,找到最上层touch事件点击的View,即first responder
。如果first responder
能够处理Touch事件,则触发事件响应的action;否则根据响应链寻找到能够处理 touch事件
的Responder再处理事件。
如图所示,有以上的图层关系。
确定first responder
当点击屏幕上CustomView2区域时,首先系统会收到touch事件
,然后调用UIWindow的方法 [UIWindow _targetWindowForPathIndex:atPoint:forEvent:windowServerHitTestWindow:]
,该方法会遍历UIWindow
的子视图,并调用子视图的 hitTest:withEvent:
确定first responder。
hitTest:withEvent:
方法作用是返回touch事件点击的view。如果touch事件没有点击在该view的范围中,则返回nil;如果点击在该view的范围中,则遍历子视图,调用子视图的hitTest:withEvent:
方法。具体步骤如下:
-
如果view
hidden
设置为YES,alpha
设置为0或者userInteractionEnabled
设置为NO,则表明view被隐藏或者不处理交互,直接返回nil;否则跳至步骤2 -
如果view没有被隐藏,则调用
pointInside:withEvent:
方法,确定touch事件是否点击在view的范围中。如果pointInside:withEvent:
返回YES,跳至步骤3;否则返回nil -
遍历子视图,调用子视图的
hitTest:withEvent:
,如果子视图返回为nil,则返回自己;如果子视图返回非nil,则返回子视图的结果
根据响应链查找可以处理事件的Responder
当通过hitTest:withEvent:
确定了first responder,系统就会调用UIApplication
的sendEvent:
方法,将事件发送到first responder。
由上图1可以看到CustomView2直接收到了事件,并调用
touchesBegan:withEvent:
。
/**
* 通常情况下,任何UIResponder想要自定义的处理touch事件,都要实现以下4个方法。
**/
- (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
的子类重写了touchesBegan:withEvent:
等方法且没有在派生方法中调用父类的touchesBegan:withEvent:
方法,则说明该view可以处理该事件,事件就会被拦截不会通过响应链向下传递。
UIControl
作为UIResponder
的子类,重写了touchesBegan:withEvent:
等方法,所以UIButton
,UISwitch
等收到事件,会直接阻止事件向下传递;而UIView
,UIImaeView
等则不会阻止事件的传递。
在这里,我们设置了符号断点,追踪了事件的传递。图1所示的
CustomView
,CustomView1
,CustomView2
都直接继承自UIView
,且没有重写touchesBegan:withEvent:
等方法,所以都不会拦截事件,事件会顺着响应链一直传递到UIApplicationDelegate
(这里UIApplicationDelegate
是UIResponder
的子类)。可以看到事件的传递如下:
如果将
CustomView1
改为继承自UIControl
或者重写touchesBegan:withEvent:
等方法,则会拦截事件不再传递。可以看到事件的传递如下:
UIControl
UIControl
是UIView
的子类,是可以响应特定的用户交互视觉元素。Target-Action
机制则是
UIControl
去处理交互的机制。
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
- (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(UIControlEvents)controlEvents;
这里我将
CustomView1
修改为UIButton
,并设置target-action;在点击后,堆栈如下图所示
可以看到 UIButton
在 touchesEnded:withEvent:
方法中,
- 根据
UITouch
确定了UIControlEvents
, 然后确定相应的target
和action
。 UIControl
调用sendAction:to:forEvent
方法UIApplication
调用sendAction:to:from:forEvent
方法- 最终
target
调用action
方法,即-[ViewController onButtonClick:]
UIGestureRecognizer
touch事件
在响应链中传递时,UIGestureRecognizer
会比它们的 view
先收到 touch事件
。如果 UIGestureRecognizer
无法响应 touch事件
,view
才会收到并处理。