前言
平日我们在玩手机,当我们的手指点击的当人手在点击屏幕时,系统会根据我们的手指动作产生一个触屏事件,这个事件可以是点击、拖动、缩放等手势,我们统称为触屏事件。那么系统是如何根据触屏事件去响应我们想要的结果呢?
一、基本元素了解
想要知道系统是怎么响应用户的触屏事件,需要提前知道3个与用户界面事件相关的类,它们分别是UITouch、UIEvent和UIResponder
UITouch
一个手指触摸会生成一个UITouch对象,多个手指触摸会生成多个UITouch对象。查看官方文档,其定义如下:
图中的属性和含义分别表示
timestamp:触摸事件发生的时间戳,单位是秒(s)phase:触摸事件的阶段,即触摸事件的状态(例如began、end、cancel)tapCount:短时间内触摸的点击次数,用来判断单击,双击等type:触摸类型,例如手指触摸或笔触摸(ios9.0可用)majorRadius:触摸的主轴半径,一般用于识别触摸点的大小和形状(ios8.0可用)majorRadiusTolerance:触摸的主轴半径容差,通常用于确定触摸点的大小和形状的变化范围(ios8.0可用)window:触摸所在的窗口view:触摸所在的视图gestureRecognizers:是一个数组,包含了与当前触摸点关联的所有手势识别器对象,但是并不包含其他视图或窗口中的手势识别器对象(ios3.2可用)
UIEvent
UIEvent 是所有用户界面事件的基类,包括触屏事件、手势事件、按键事件等,每个UIEvent 提供事件类型、事件发生时间、事件涉及到的触摸对象等信息。UITouch 对象则是 UIEvent 对象中的一部分,表示了用户当前的触屏信息。它主要包含以下属性:
type:表示事件的类型,例如触摸、按压、滚动subtype:表示事件的子类型,用于进一步描述事件的类型。例如上面的type的值是触摸事件,那么subtype可以表示触摸事件的具体类型,例如单击、双击、长按等timestamp:表示事件发生的时间戳,,单位是秒(s)modifierFlags:表示事件发生时键盘上的修饰键状态,如 Shift、Control、Option 等buttonMask:表示触摸事件中的按下的按钮掩码。在触摸事件中,用户可能会按下屏幕上的多个按钮,例如 Home 按钮、电源按钮等,buttonMask 属性可以表示这些按钮的状态allTouches:表示事件涉及到的所有触摸对象,里面是一个由UITouch组成的列表
UIResponder
UIResponder 是 iOS 中响应者对象的基类,它定义了一些方法和属性,用于响应输入事件和触摸事件,并将事件传递给后续的响应者对象。 它包含以下属性:
nextResponder:下一个响应者对象,也是一个UIResponder对象。在响应事件的过程中,如果当前对象无法处理事件,系统会将该事件传递给nextResponder对象处理。如果nextResponder为nil,则表示当前对象已经是响应链的末尾canBecomeFirstResponder:当前对象是否能成为第一响应者,只读canResignFirstResponder:当前对象是否可以放弃第一响应者的身份,只读isFirstResponder:,当前对象是否是第一响应者,只读
二、触屏事件的基本元素关联
介绍完触屏事件的基本元素的三个类,我们来说一下它们的关联
首先,在iOS应用程序中,事件通常是由UIApplication对象分发给UIResponder对象去处理的。当用户触摸屏幕,产生触屏事件,UIApplication对象会创建一个UIEvent对象(UITouch 对象则是 UIEvent 对象中的一部分),然后将这个UIEvent对象发送给当前响应事件的UIResponder对象。
然后,如果当前响应事件的UIResponder对象无法处理该事件,则会将事件传递给父视图或父控制器,直到有一个对象能成功处理该事件,否则,事件最后到达返回到UIApplication对象时,则会被丢弃。
最后,我们可以通过重写UIResponder的touches方法来响应用户事件。例如-touchesBegan:withEvent: 方法,这个方法会在用户开始触摸屏幕时被调用,我们可以重写它,通过判断触摸事件的位置,执行相应操作,比如弹出定制显示框。
三、触屏事件深入到到应用
在 iOS 应用程序中,当我们触摸屏幕时,系统会将触摸事件加入UIApplication对象的管理队列事件中,然后UIApplication对象会取出队列里最先进去的事件(FIFO),发出去处理,一般是传递给最上层的窗口对象(UIWindow 类),UIWindow 类通过 hitTest:withEvent: 方法来确定哪个视图对象(同时也是响应者UIResponder),来负责处理该事件。因此,hitTest:withEvent: 方法是触屏事件和视图对象的关键联系点。
hitTest:withEvent: 是 UIView 类的方法,该方法会递归地遍历视图层次结构,找到最合适的视图对象(UIResponder)来处理触摸事件,其寻找规律如下图:
上图是一幅按照数字从小到大添加进去的视图对象
举个例子:
1. 点击红色view,寻找顺序是:
白色view(找到,去子视图)-> 黄色view(找不到,去同级视图)-> 红色view(找到,没有子视图,返回自己)
2. 点击蓝色view,寻找顺序是:
白色view(找到,去子视图)-> 黄色view(找到,去子视图)-> 紫红色view(找不到,去同级视图)-> 绿色view(找不到,去同级视图)-> 蓝色view(找到,没有子视图,返回自己)
3. 蓝色view隐藏,点击蓝色view的位置,寻找顺序是:
白色view(找到,去子视图)-> 黄色view(找到,去子视图)-> 紫红色view(找不到,去同级视图)-> 绿色view(找不到,去同级视图)-> 蓝色view(蓝色view隐藏了,返回nil,找不到,返回)->顺着响应者链回到黄色view,然后返回黄色view
从现象可以分析出:
- 调用顺序是先从父视图逐级向其子视图遍历调用,如果视图在同级,就按照同级视图添加到这个父视图上的顺序,从后向前遍历,即后添加的先遍历(FILO)
- 每一个被遍历到的视图,会调用
-pointInside:withEvent:方法判断点击事件是不是在本视图,如果不是,则返回NO,就不会遍历其子视图。如果是,则返回YES,就会遍历其子视图,先调用其子视图的-hitTest:withEvent:方法,然后该子视图又会调用-pointInside:withEvent:方法判断点击事件是不是在本视图,如此递归下去。 - 如果某个视图的
-hitTest:withEvent:方法返回了响应视图(响应者),则会停止遍历还没遍历的视图。 - 如果视图不接受交互 :
userInteractionEnabled = NO;,或者设置了隐藏:hidden = YES;,或者透明度: alpha<=0.01 , 则不会响应触摸事件,会直接在-hitTest:withEvent:方法返回nil,并不会调用-pointInside:withEvent:方法
四、关于响应流程
前面介绍了触摸事件的传递过程,那么找到响应者之后,响应者是如何响应事件的呢?
在 iOS 应用程序中,当-hitTest:withEvent: 方法返回了响应视图,系统会将触摸事件传递给该视图对象的touches方法来处理事件,它们分别是:
//触摸事件的开始
- (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;
在上面四个方法中,touchesBegan和touchesEnded表示触摸事件的开始和结束,是一定会调用到的,至于移动和取消需要根据手势和其他情况才会触发。touches方法来处理完事件后,系统会把触摸事件UIEvent打包发送给响应者的响应方法,其流程如下图:
注意:如果响应者无法响应事件,那么系统会顺着响应者链从下往上找,直至找到最终的响应者
五、总结
熟悉iOS触摸事件传递的机制和流程,可以创造很多匪夷所思的功能,例如点击A控件,让B控件响应事件,或者点击A控件,A控件以及A控件的父控件一起响应事件。还需要注意的是,手势的识别优先级是高于点击事件的,这方面的分析等我找天有时间再做个分享。