您的位置:首页 > 移动开发 > IOS开发

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

前言

当用户对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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: