GeekBand - iOS 事件传递及响应笔记第一周

本次笔记主要是整理一下关于 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...

举例:


GeekBand - iOS 事件传递及响应笔记第一周_第1张图片
UIEvent
  • 触摸事件的传递是从父控件传递到子控件
  • 点击了绿色的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 不一定能响应该事件,引出了响应链的传递过程,一般是讲事件交给上一个响应者来进行处理。

响应者链条示意图

  • 响应者链条: 是由多个响应者对象连接起来的链条
  • 作用:可以很清楚的看见每个响应者之间的联系,并且可以让一个事件处理给多个对象处理
  • 响应者对象:能处理事件的对象
GeekBand - iOS 事件传递及响应笔记第一周_第2张图片
响应链条示意图

事件传递的完整过程

  1. 先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件 来处理这个事件。
  2. 调用最合适控件的touches....方法
  3. 如果调用了[super touches....];就会将事件顺着响应者链条往上传递,传递
    给上一个响应者
  4. 接着就会调用上一个响应者的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

你可能感兴趣的:(GeekBand - iOS 事件传递及响应笔记第一周)