iOS控件之UIResponder类
这个部分我们主要介绍3种事件类型,即触摸事件,运动事件,远程控制事件。当用户触发某一事件时,UIKit会创建一个UIEvent事件对象(关于iOS事件对象可以参考这篇文章),事件对象会加入到一个FIFO先进先出的队列中,UIApplication对象处理事件时,会从队列头部取出一个事件对象进行分发。
1.触摸事件
@interface UIResponder : NSObject
- (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;//系统事件干扰
这4个方法是触摸事件的最原始处理的4个方法,分别代表触摸屏幕,在屏幕上移动,离开屏幕以及受到系统优先级别高的事件的干扰(比如来电话)取消触摸事件。另外UIKit框架对于触摸事件为我们提供了UIGestureRecognizer
手势识别这个类,基本上能满足我们的大部分需求(可以参考这篇文章)。这里介绍的是最底层的处理方法,比如可以用来实现绘图类型的APP.
上面提到UIApplication对象从队列中取出事件对象进行分发,对于触摸事件来说,UIApplication会首先把事件交给keyWindow,Window会将事件交给UIGestureRecognizer
处理,如果UIGestureRecognizer
识别了传递过来的事件,则交给相对应的target去处理(关于iOS手势事件可以参考这篇文章),事件不会再传递,如果UIGestureRecognizer
并没有识别传递过来的事件(可能是没有视图添加手势,也可能手势识别不成功),事件会传递到视图树形结构,会分成寻找接受者和事件响应这两个步骤。
1.在iOS视图树形结构中找到最终的接收者,也就是触摸事件发生的那个最上层的View上,这一过程称为hit-testing(测试命中),通过一层层的遍历找到最终的命中视图称为hit-test view.
UIView中有两个方法用来确定hit-test view.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
这是一张我从apple官方文档里面的截图,这里所有的显示的View都是加载到主window上,假设我们触摸到屏幕上ViewD的区域,当我们没有重载UIView的hitTest:withEvent:
和pointInside:withEvent:
这两个方法时,系统默认的处理如下:
- keyWindow调用
pointInside:withEvent:
判断触摸点是否在其frame范围内,返回Yes,遍历keyWindow的subView->ViewA. - ViewA调用
pointInside:withEvent:
判断触摸点是否在其frame范围内,返回Yes,遍历ViewA的subView->ViewB、ViewC(关于ViewB和ViewC先执行哪个,是根据ViewA添加子控件的先后顺序,总是先执行后添加的subView.假设添加ViewB后添加ViewC) - ViewC调用
pointInside:withEvent:
判断触摸点是否在其frame范围内,返回Yes,遍历ViewC的subView->ViewD、ViewE - ViewE调用
pointInside:withEvent:
判断触摸点是否在其frame范围内,返回NO,ViewE的hitTest:withEvent:
返回nil(如果是先执行ViewB的情况,假设ViewB还有子节点subView,由于ViewB的pointInside:withEvent:
返回NO,ViewB的hitTest:withEvent:`直接返回nil是不会再去遍历ViewB的子节点的) - ViewD调用
pointInside:withEvent:
判断触摸点是否在其frame范围内,返回Yes并且没有子节点subView,ViewD的hitTest:withEvent:
返回ViewD本身,即为最终的hit-test view(不会再遍历ViewB)iewB需要注意的是:View.isHidden=YES View.alpha<=0.01 View.userInterfaceEnable=NO View.enable = NO(指继承自UIControl的View)的这4种情况下,View的pointInside返回NO,hitTest方法返回nil
默认UIImageView
的userInterfaceEnable=NO
2.找到了hit-test view,下一个步骤就是响应事件。说明一下,对于触摸事件来说,无论View是否处理事件,即使是application通过[application beginIgnoringInteractionEvents]
忽略了触摸事件,上面hit-testing的过程依然存在,它只影响第二个步骤事件响应的过程。下面我们将介绍iOS响应者链条(Responder chain)
再看上图右边的情况:标注为①的地方即为步骤1找到的hit-test view,同时它是控制器的根view并且还有父视图,事件传递到控制器->再传递到父视->传递到控制器,再传递到父视图窗口->application。其实上图左边部分也可以理解为窗口是控制器根视图的父视图。如果整个响应者链条结束,都没有对事件做处理,那么该事件会被丢弃。
总结一下响应者链条的传递过程是:由第一响应者(对于触摸事件来说是hist-test view)开始向上传递。如果该视图是控制器的根视图,先传递给控制器,再传递给父视图,如果不是控制器的根视图,直接传递给父视图。
只要在响应者的处理方法里面调用父类的方法,就可以让多个视图和控制器响应同一个事件,响应者链条的根本目的是:共享事件,让多个视图和控制器可以对同一事件做不同的处理。
2.运动事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);
这3个方法是运动事件的最原始处理的3个方法,这里处理的运动事件特指shake事件,手机摇动触发手机内部的加速度传感器,可以用来实现摇一摇计算运动的步数等等应用。类似于触摸事件,这3个方法分别代表事件开始、事件结束和受到系统干扰取消事件。
加速度计accelerometer
实际上是由三个加速度计组成,分别用于测量X,Y和Z轴直线路径速度的变化。结合所有三个加速度计可以检测设备朝任何方向的运动和获取设备的当前方向。对于shake事件来说,我们不关心3个方向上的运动,只作为一个事件对象来处理。如果只是处理设备的大方向,并不需要知道方向向量,如横屏竖屏屏幕旋转,我们可以使用UIDevice类(参考文章)。如果我们需要知道3个方向上的运动做更细致化的处理,如上了多少层楼等运动类型APP,可以使用的核心运动框架访问加速度计,陀螺仪和设备的运动类来做处理(Core Motion参考文章)
上面介绍的响应者链条对shake事件同样适用,只不过,没有hit-testing过程,如果当前显示的视图界面没有一个view声明为第一响应者(调用becomeFirstResponder
申明并且View需要重写canBecomeFirstResponder
方法返回YES,默认返回为NO),默认当前视图控制器为第一响应者,并将事件沿着响应者链条传递,直到被处理。如果有视图声明为第一响应者,就从该视图开始传递事件直到被处理,如果该事件最终没有被处理并且UIApplication的applicationSupportsShakeToEdit
属性为YES(默认就是YES),当键盘显示的时候,系统会有一个是否撤销正在输入的警告。就是微信和QQ上在输入的时候摇动手机提示撤销输入的那种效果。关于更多撤销方面的操作参考NSUndoManager
3.远程控制事件(本文暂不讨论)
4.UI控件不能响应事件的几种情况
注意:UIView.enable = NO;时也不能响应事件!