iOS之事件的传递和响应机制
渐渐发现有些东西还是得形成文字才能记得更准确吧。
iOS中的事件可以分为3大类型:
触摸事件
加速计事件
远程控制事件
这里我们只讨论iOS中的触摸事件。
在iOS中不是任何对象都能处理事件,只有继承了
UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自
UIResponder的
UIApplication
UIViewController
UIView
那么为什么继承自UIResponder
的类就能够接收并处理事件呢?
因为UIResponder中提供了以下4个对象方法来处理触摸事件。
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
的.m
文件提 供给我们。我们只能通过子类继承父类,重写子类方法的方式处理UIView
的触摸事件(注意:我说的是UIView
触摸事件而不是说的 UIViewController
的触摸事件)。
有人认为,我要是处理控制器的自带的view
的事件就不需要自定义UIView
子类继承于UIView
,因为可以在viewController.m
文件中重写touchBegan:withEvent:
方法,但是,我们此处讨论的是处理UIView
的触摸事件,而不是处理 UIViewController
的触摸事件。你如果是在viewController.m
文件中重写touchBegan:withEvent:
方法,相当于处理的是viewController
的触摸事件,因为viewController
也是继承自UIResponder
,所以会给人一种错觉。
所以,还是那句话,想处理UIView的触摸事件,必须自定义UIView子类继承自UIView
。
事件的传递与响应:
1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view
,以上就是事件的传递,也就是寻找最合适的view的过程。
2、接下来是事件的响应。首先看initial view
能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view
的superView
);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller
,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到window
,如果window
还是不能处理此事件则继续交给application
处理,如果最后application
还是不能处理此事件则将其丢弃
3、在事件的响应中,如果某个控件实现了touches...
方法,则这个事件将由该控件来接受,如果调用了[super touches….]
;就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….
方法
事件的传递和响应的区别:
事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。
找到最合适的响应View
首先 UIView
不能接收触摸事件的三种情况:
不允许交互:userInteractionEnabled = NO
隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
1.0事件产生
- 发生触摸事件后,系统会将该事件加入到一个由
UIApplication
管理的事件队列中为什么是队列而不是栈?因为队列的特定是先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。 -
UIApplication
会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow
)。 - 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
找到合适的视图控件后,就会调用视图控件的touches
方法来作具体的事件处理。
2.0事件传递
- 触摸事件的传递是从父控件传递到子控件
- 也就是
UIApplication
->window
->寻找处理事件最合适的view
应用如何找到最合适的控件来处理事件?
- 1.首先判断主窗口(
keyWindow
)自己是否能接受触摸事件 - 2.判断触摸点是否在自己身上
- 3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
- 4.
view
,比如叫做fitView
,那么会把这个事件交给这个fitView
,再遍历这个fitView
的子控件,直至没有更合适的view
为止。 - 5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的
view
。
找到最底层的View到底是怎么实现的
先看两个api:
第一个:-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法,不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法。
拦截事件的处理
- 正因为
hitTest:withEvent:
方法可以返回最合适的view
,所以可以通过重写hitTest:withEvent:
方法,返回指定的view
作为最合适的view
。 - 不管点击哪里,最合适的
view
都是hitTest:withEvent:
方法中返回的那个view。 - 通过重写
hitTest:withEvent:
,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。
所以事件的传递顺序是这样的:
产生触摸事件->UIApplication
事件队列->[UIWindow hitTest:withEvent:]
->返回更合适的view
->[子控件 hitTest:withEvent:
]->返回最合适的view
事件传递给窗口或控件的后,就调用hitTest:withEvent:
方法寻找更合适的view
。所以是,先传递事件,再根据事件在自己身上找更合适的view
。
不管子控件是不是最合适的view
,系统默认都要先把事件传递给子控件,经过子控件调用自己的hitTest:withEvent:
方法验证后才知道有没有更合适的view
。即便父控件是最合适的view
了,子控件的hitTest:withEvent:
方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view
,那么该父控件的子控件的hitTest:withEvent:
方法也是会被调用的。
技巧:想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:
方法返回指定的子控件,或者重写自己的hitTest:withEvent:
方法 return self
。但是,建议在父控件的hitTest:withEvent:
中返回子控件作为最合适的view
!
原因在于在自己的hitTest:withEvent:
方法中返回自己有时候会出现问题,因为会存在这么一种情况,当遍历子控件时,如果触摸点不在子控件A
自己身上而是在子控件B
身上,还要要求返回子控件A
作为最合适的view
,采用返回自己的方法可能会导致还没有来得及遍历A
自己,就有可能已经遍历了点真正所在的view
,也就是B
。这就导致了返回的不是自己而是点真正所在的view
。所以还是建议在父控件的hitTest:withEvent:
中返回子控件作为最合适的view
!
hitTest:withEvent:方法底层实现
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// point:是方法调用者坐标系上的点
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1.判断下窗口能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判断下点在不在窗口上
// 不在窗口上
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历子控件数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
// 获取子控件
UIView *childView = self.subviews[i];
// 坐标系的转换,把窗口上的点转换为子控件上的点
// 把自己控件上的点转换成子控件上的点
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) {
// 如果能找到最合适的view
return fitView;
}
}
// 4.没有找到更合适的view,也就是没有比自己更合适的view
return self;
}
第二个:-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
作用:判断下传入过来的点在不在方法调用者的坐标系上
point:是方法调用者坐标系上的点。
第一个api用来做拦截处理,第二api可以扩大按钮的点击范围,示例扩大按钮的点击范围。