iOS事件传递和事件响应链
2016-03-08 17:02
369 查看
参考摘录:
http://mp.weixin.qq.com/s?__biz=MzAxMzE2Mjc2Ng==&mid=401930693&idx=1&sn=80b3fd1a9ad76451952395765b5bbe41&scene=23&srcid=0307jGTi34WitUGlrKMWPbnP#rd
事件产生后,经过层层传递,直到找到最合适的视图后,再逐层返回直到有事件响应操作。
触摸事件
加速计事件
远程控制事件
这里我们只讨论iOS中的触摸事件。
UIApplication
UIViewController
UIView
UIResponder中提供了以下4个对象方法来处理触摸事件。
通过继承UIResponder后重写事件方法,可以用于处理事件,都是由系统自动调用。
其中,touches中存放的都是UITouch对象
一根手指对应一个UITouch对象
如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
它保存着跟手指相关的信息,比如触摸的位置、时间、阶段。
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。
当手指离开屏幕时,系统会销毁相应的UITouch对象。
UITouch的属性:
UITouch的方法:
通常,先发送事件给应用程序的主窗口(keyWindow)。
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
这个过程是由上到下的传递过程,
触摸事件的传递是从父控件传递到子控件
也就是UIApplication->window->寻找处理事件最合适的view
那么应用如何找到最合适的控件的?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.判断触摸点是否在自己身上
3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,因为后添加的子控件在上面,降低循环次数,然后执行1、2步骤)
4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
UIView不能接收触摸事件有三种情况:
不允许交互:userInteractionEnabled = NO
隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
透明度:如果设置一个控件的透明度
另外提一下,默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO,所以如果希望UIImageView可以交互,需要userInteractionEnabled = YES
hitTest:withEvent:方法
pointInside方法
hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法,用于寻找并返回最合适的view(能够响应事件的那个最合适的view)。
如果该方法返回nil,那么事件便不会往下遍历,也就是调用该方法的控件本身和其子控件都不是最合适的view,那么最合适的view就是该控件的父控件。
如果返回的是view,不管该事件是点在哪的,都以该view为最合适视图。
注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件(包括起父视图和其子视图),随后再调用其hitTest:withEvent:方法。
事件的传递顺序是这样的:
产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view。
事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。
通过重写视图的hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
拦截思路有两种:
想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件。
重写自己的hitTest:withEvent:方法 return self。
但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view。why?!还记得吗,事件传递遍历控件的时候,子控件视图都是从后往前遍历的,也就是后添加的视图先检查,如果有多个子视图,就有可能还没遍历到你就先返回真正合适的view。
下面举个例子:
设有视图ABC三个视图
把BC作为A的子视图
如果我们想让用户无论点击A哪个地方,或者是C,都让B来作为最合适视图。
可以在A视图中重写方法:
你会发现无论点的是谁都是打印“B-touch”(B先添加),说明B是成功拦截成为最合适视图(成为最合适视图后才能响应touches方法)
那么下面我们试试不在父视图A上重写返回,改为在B视图重写返回self
如果我们点击A视图,打印“B-touch”,但如果我们点击C视图时,打印的却是“C-touch”,拦截失败了。
首先,我们点击A视图的时候,A会遍历子视图BC,正常情况下,因为点击不在BC上,A才是最合适视图,但因为B重写返回了他自己,所以B成了最合适视图。但是,如果我们点击在了C上,但A遍历BC时,是先检查C的,而刚好点击是在C上面的,C便成了最合适视图,B就拦截失败了。
找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…
这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件,重写后才处理),将事件交给上一个响应者进行处理。
响应者链条:在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。
响应者对象:能处理事件的对象,也就是继承自UIResponder的对象
作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。
如何判断上一个响应者
1> 如果当前这个view是控制器的view,那么控制器就是上一个响应者
2> 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
响应者链的事件传递过程:
1>如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
2>在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
3>如果window对象也不处理,则其将事件或消息传递给UIApplication对象
4>如果UIApplication也不能处理该事件或消息,则将其丢弃
可以简单理解,响应链跟事件传递时的顺序相反。
传递先从上往下(UIApplication->window->view),响应是从下往上。
touches默认做法是把事件顺着响应者链条向上抛,直到找到重写了该方法的。否则,最后该事件作废。
touches的默认做法:
传递先从上往下(UIApplication->window->view),响应是从下往上。
1.当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。
2.找到最合适view后,传递结束,开始进入响应过程。响应顺序跟传递相反,当响应者没有重写touches方法来处理事件,事件就会传递给上一级view或者view controller来响应,由上一级继续检查,直到有重写touches方法的。 顺序为: initial view->superView->view controller->window->application
http://mp.weixin.qq.com/s?__biz=MzAxMzE2Mjc2Ng==&mid=401930693&idx=1&sn=80b3fd1a9ad76451952395765b5bbe41&scene=23&srcid=0307jGTi34WitUGlrKMWPbnP#rd
前言
当用户对view进行触摸时,便会产生事件,执行我们的业务操作。我们的每一个事件,在iOS系统都会经过传递和响应的过程。事件产生后,经过层层传递,直到找到最合适的视图后,再逐层返回直到有事件响应操作。
事件的定义
iOS中的事件可以分为3大类型:触摸事件
加速计事件
远程控制事件
这里我们只讨论iOS中的触摸事件。
1.响应者对象(UIResponder)
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。UIApplication
UIViewController
UIView
UIResponder中提供了以下4个对象方法来处理触摸事件。
//UIResponder内部提供了以下方法来处理事件触摸事件 // 一根或者多根手指开始触摸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; //加速计事件 - (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;
通过继承UIResponder后重写事件方法,可以用于处理事件,都是由系统自动调用。
其中,touches中存放的都是UITouch对象
2.UITouch
当用户用一根手指触摸屏幕时,会创建一个与手指相关的UITouch对象一根手指对应一个UITouch对象
如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个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; // 该方法记录了前一个触摸点的位置
事件的传递
当发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,将事件分发下去以便处理。而这个处理过程就是一个传递事件寻找最合适view的过程。通常,先发送事件给应用程序的主窗口(keyWindow)。
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
这个过程是由上到下的传递过程,
触摸事件的传递是从父控件传递到子控件
也就是UIApplication->window->寻找处理事件最合适的view
寻找事件最合适的view
其实可以说,事件传递的过程其实就是一个寻找最合适视图的过程。那么应用如何找到最合适的控件的?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.判断触摸点是否在自己身上
3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,因为后添加的子控件在上面,降低循环次数,然后执行1、2步骤)
4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
UIView不能接收触摸事件有三种情况:
不允许交互:userInteractionEnabled = NO
隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
透明度:如果设置一个控件的透明度
另外提一下,默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO,所以如果希望UIImageView可以交互,需要userInteractionEnabled = YES
拦截事件的处理
在遍历寻找最合适视图过程中,会调用视图的两个重要方法:hitTest:withEvent:方法
pointInside方法
hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法,用于寻找并返回最合适的view(能够响应事件的那个最合适的view)。
如果该方法返回nil,那么事件便不会往下遍历,也就是调用该方法的控件本身和其子控件都不是最合适的view,那么最合适的view就是该控件的父控件。
如果返回的是view,不管该事件是点在哪的,都以该view为最合适视图。
注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件(包括起父视图和其子视图),随后再调用其hitTest:withEvent:方法。
事件的传递顺序是这样的:
产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view。
事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。
通过重写视图的hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
拦截思路有两种:
想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件。
重写自己的hitTest:withEvent:方法 return self。
但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view。why?!还记得吗,事件传递遍历控件的时候,子控件视图都是从后往前遍历的,也就是后添加的视图先检查,如果有多个子视图,就有可能还没遍历到你就先返回真正合适的view。
下面举个例子:
设有视图ABC三个视图
@interface A : UIView @end @implementation A - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"A-touch"); } @end
@interface B : UIView @end @implementation B - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"B-touch"); } @end
@interface C : UIView @end @implementation C - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"C-touch"); } @end
把BC作为A的子视图
A *a=[[A alloc] initWithFrame:self.view.bounds]; B *b=[[B alloc] initWithFrame:CGRectMake(20, 20, 40, 40)]; C *c=[[C alloc] initWithFrame:CGRectMake(20, 80, 40, 40)]; [self.view addSubview:a]; [a addSubview:b]; [a addSubview:c];
如果我们想让用户无论点击A哪个地方,或者是C,都让B来作为最合适视图。
可以在A视图中重写方法:
@implementation A - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ return self.subviews[0]; } @end
你会发现无论点的是谁都是打印“B-touch”(B先添加),说明B是成功拦截成为最合适视图(成为最合适视图后才能响应touches方法)
那么下面我们试试不在父视图A上重写返回,改为在B视图重写返回self
@implementation B - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ return self; } @end
如果我们点击A视图,打印“B-touch”,但如果我们点击C视图时,打印的却是“C-touch”,拦截失败了。
首先,我们点击A视图的时候,A会遍历子视图BC,正常情况下,因为点击不在BC上,A才是最合适视图,但因为B重写返回了他自己,所以B成了最合适视图。但是,如果我们点击在了C上,但A遍历BC时,是先检查C的,而刚好点击是在C上面的,C便成了最合适视图,B就拦截失败了。
事件响应链
用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件。找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…
这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件,重写后才处理),将事件交给上一个响应者进行处理。
响应者链条:在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。
响应者对象:能处理事件的对象,也就是继承自UIResponder的对象
作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。
如何判断上一个响应者
1> 如果当前这个view是控制器的view,那么控制器就是上一个响应者
2> 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
响应者链的事件传递过程:
1>如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
2>在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
3>如果window对象也不处理,则其将事件或消息传递给UIApplication对象
4>如果UIApplication也不能处理该事件或消息,则将其丢弃
可以简单理解,响应链跟事件传递时的顺序相反。
传递先从上往下(UIApplication->window->view),响应是从下往上。
touches默认做法是把事件顺着响应者链条向上抛,直到找到重写了该方法的。否则,最后该事件作废。
touches的默认做法:
@implementation MyView //只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件 // 上一个响应者可能是父控件 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ // 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理 [super touchesBegan:touches withEvent:event]; // 注意不是调用父控件的touches方法,而是调用父类的touches方法 // super是父类 superview是父控件 } @end
一个事件多个对象响应处理
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ // 1.自己先处理事件... NSLog(@"do somthing..."); // 2.再调用系统的默认做法,再把事件交给上一个响应者处理 [super touchesBegan:touches withEvent:event]; }
总结
事件在iOS的处理基本可以分为两部分,先传递,后响应。传递先从上往下(UIApplication->window->view),响应是从下往上。
1.当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。
2.找到最合适view后,传递结束,开始进入响应过程。响应顺序跟传递相反,当响应者没有重写touches方法来处理事件,事件就会传递给上一级view或者view controller来响应,由上一级继续检查,直到有重写touches方法的。 顺序为: initial view->superView->view controller->window->application
相关文章推荐
- iOS7编程Cookbook中例15.8中一个小问题
- iOS7编程Cookbook中例15.8中一个小问题
- iOS7编程Cookbook中例15.8中一个小问题
- iOS----------SDWebimage源码解析(4)
- iOS真机测试
- iOS中的单例设计模式详解
- iOS测试证书创建,真机测试
- ios项目打包上线
- iOS 开发实践之 Auto Layout
- iOS拨打电话方式
- iOS程序支持64位以及project.pbxproj文件的介绍
- 通过URL Scheme传递参数
- IOS动画教程(一)
- IOS关于Runtime
- iOS 七巧板动画
- ios 点击抽屉页面按钮,触发跳转另一个页面
- project.pbxproj 文件的学习
- ios时间获取刚刚几分钟几秒钟几小时之前
- ios键盘高度监听
- iOS 强引用(__strong)和弱引用(__weak)浅析