响应者对象 -- UIResponder
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;
响应链
app中,所有的视图都是按照树状层次结构组织起来的,每个view都有自己的superView,包括controller的topmost view(controller的self.view)。当一个view被add到superView上的时候,他的nextResponder属性就会被指向它的superView,当controller被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller,而controller的nextResponder会被指向self.view的superView,这样,整个app就通过nextResponder串成了一条链,也就是我们所说的响应链。所以响应链就是一条虚拟的链,并没有一个对象来专门存储这样的一条链,而是通过UIResponder的属性串连起来的。
一、事件的传递
1.点击UIView产生一个触摸事件,这个触摸事件会被添加到由UIApplication管理的事件队列中(即首先接收到事件的是UIApplication)。
2.UIApplication会从事件队列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(UIWindow)。
3.UIWindow将事件向下分发给UIView。
4.UIView首先看自己是否能处理事件,触摸点是否在自己身上。如果能,那么继续寻找子视图。
5.遍历子控件,重复以上两步。
6.如果没有找到,那么自己就是事件处理者。
7.如果自己不能处理,那么不做任何处理。
其中 UIView不接受事件处理的情况主要有以下三种
1)alpha <0.01
2)userInteractionEnabled = NO
3.hidden = YES.
事件传递顺序是这样的:
产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的子控件view->[子控件 hitTest:withEvent:]->返回最合适的view。
注意:即便确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。
重难点 -- 应用如何找到最合适的控件view来处理事件?
1.主窗口(keyWindow)接收到应用程序UIApplication传递过来的事件后, 首先判断自己是否能接收触摸事件,如果能,那么再判断触摸点在不在自己身上;
2.如果触摸点也在主窗口身上,那么窗口会从后往前(首先查找数组中最后一个元素)遍历自己的子控件
3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接收事件,2.触摸点在不在子控件上)
4.循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
找到最合适的view后,就会调用该view的touches方法处理具体的事件。所以,只有找到最合适的view,把事件传递给最合适的view后,才会调用touches方法进行接下来的事件处理。找不到最合适的view,就不会调用touches方法进行事件处理。
注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。
- (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;
}
扩展:
想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!
原因在于在自己的hitTest:withEvent:方法中返回自己有时候会出现问题。因为会存在这么一种情况:当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,还要要求返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view,也就是B。这就导致了返回的不是自己而是触摸点真正所在的view。所以还是建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!
二、事件的响应
首先看initial view能否处理这个事件,如果不能则会将事件传递给其父视图(inital view的superView);如果父视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传递;一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃。
在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来处理,如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法
事件的传递和响应的区别:
事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。
应用:
扩大按钮的点击区域
#import <UIKit/UIKit.h>
@interface SYExpandHitAreaButton : UIButton
//定义扩大点击区域的数值
@property (nonatomic, assign) CGFloat expandValue;
@end
#import "SYExpandHitAreaButton.h"
@interface SYExpandHitAreaButton()
@end
@implementation SYExpandHitAreaButton
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.hidden || self.alpha <= 0.01) {
return nil;
}
CGRect touchRect = CGRectInset(self.bounds, self.expandValue, self.expandValue);
if (CGRectContainsPoint(touchRect, point)) {
for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subView convertPoint:point fromView:self];
UIView *hitTestView = [self hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}
@end
参考:
深入浅出iOS事件机制
http://zhoon.github.io/ios/2015/04/12/ios-event.html
史上最详细的iOS之事件的传递和响应机制-原理篇
网友评论