iOS控件之UIResponder类
在iOS中UIResponder类是专门用来响应用户的操作处理各种事件的,我们知道UIApplication、UIView、UIViewController这几个类是直接继承自UIResponder,UIWindow是直接继承自UIView的一个特殊的View,所以这些类都可以响应事件。当然我们自定义的继承自UIView的View以及自定义的继承自UIViewController的控制器都可以响应事件。iOS里面通常将这些能响应事件的对象称之为响应者。
iOS中事件Event可以简单划分为两种,一种是系统事件(如来电、内存警告等),另外的我们就称非系统事件(触摸事件、运动事件、远程控制事件)。
无论是系统事件还是非系统事件,iOS系统都是最先知道事件的,然后将事件加入到当前正在运行的app的UIApplication的事件队列中。UIApplication会按照先进先出的顺序从事件队列中取出事件,然后分发出去寻求响应。
UIApplication对接收到的系统事件会分发给它的代理AppDelegate对象进行处理,其他的非系统事件会传给UIApplication的keyWindow去寻找合适的view进行响应。
本文主要讨论非系统事件,包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events,如插入耳机调节音量触发的事件)。关于系统事件可以参考这篇文章http://www.cnblogs.com/shaoting/p/5029092.html
下面我们根据UIResponder.h头文件来具体介绍关于响应事件的各个方面。
这个部分我们主要介绍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没有通过重写或者封装touch系列方法来处理该事件,默认touch的实现就是调用父类的touch方法,将事件传递下去。在这里由1->传递到它的父类2,2是控制器的根view,->传递到vc控制器->传递到窗口window->传递到application
再看上图右边的情况:标注为①的地方即为步骤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;时也不能响应事件!
以上内容大多转自http://www.jianshu.com/p/2dda99a0e09a