文章目录
- UIKit继承图
- UIEvent
- UIResponder
- 注意
- 响应者
- 第一响应者
- 响应者链
- 事件处理的过程
- 第一步,寻找第一响应者
- 第二步,响应UIEvent事件
- UIControl的Target-Action设计模式
- 扩大点击范围
- 穿透
UIKit继承图
UIEvent
在iOS中,当一个事件发生时,系统会搜集相关事件信息,创建一个UIEvent对象。
iOS中的事件类型:
- 触摸事件
- 加速计事件(运动事件)
- 远程遥控事件
一个事件的周期:事件的产生—事件的传递—事件的响应
事件传递和事件响应的区别:
事件传递是自上而下,父控件到子控件
事件响应是自下而上,子控件到父控件,沿着响应者链
这里主要了解触摸事件
发生触摸事件后,系统会将该事件添加到UIApplication管理的事件队列中。UIApplication会取出事件队列中的第一个事件分发下去,首先将该事件分发给UIApplication的主窗口对象(keyWindow),然后由主窗口会在视图层次结构中找到一个最合适的视图来处理事件,这也就是事件产生与传递。
UIResponder
UIResponder类提供的响应和处理触摸事件的4个主要方法
//开始接触屏幕,就会调用一次
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
//手指开始移动就会调用(这个方法会频繁的调用,其实一接触屏幕就会多次调用)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
//手指离开屏幕时,调用一次
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
//触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,或者view上面添加手势时,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
通过这四个方法就可以知道触摸事件的整个过程。
其中有两个相关参数UITouch、UIEvent
- UITouch对象
当用户用一根手指触摸屏幕时,就会创建一个与手指相关联的UITouch对象
一根手指对应一个UITouch对象 - UITouch作用
保存着跟手指相关的信息,比如触摸的位置、时间、阶段
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
当手指离开屏幕时,系统会销毁相应的UITouch对象。 - UIEvent对象
- 每产生一个事件,就会产生一个UIEvent事件
UIEvent,称为事件对象,记录事件产生的时刻和类型
这里再来说一下touches,上面四个方法的参数
- touches里包含了一个或多个UITouch对象,即一个或多个手指同时触摸view,因此touches.count就是触摸的点数,1就是单点触摸,大于1就是多点触摸
- 一次触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数。
如果两根手指同时触摸一个view,那就是一个事件,view只会调用一次touchesBegan:withEvent:方法,但touches参数中装着2个UITouch对象 - 如果这两根手指一前一后分开触摸同一个view,那就是两个事件,view会分别调用两次touchesBegan:withEvent:方法,并且touches参数中只包含一个UITouch对象
注意
- 在view上使用上述四个方法,必须是自定义view并重写父类方法
- 在viewController里重写这四个方法,就是viewController响应这个事件,viewController上的子视图需要相应并处理事件时,这个view需要自定义并重写方法。
可能有些人会以为在viewController里重写,容易混淆
响应者
在iOS中,只有继承UIResponder的类或子类才可以响应和处理事件。
上面有一张继承图,可以看到具体可以处理触摸事件的类如下:
- UIApplication
- UIViewController(包括它的子类)
- UiView(包括它的子类)
第一响应者
在UIWindow下,第一个接收到UIEvent的视图对象(除UIWindow外),往往也是最适合处理当前触摸事件的视图。比如点击事件在一个按钮上,那么这个按钮就是第一响应者。
响应者链
响应者链是支撑app界面交互的重要基础
响应者链就是由多个响应者对象(即继承自UIResponder的对象)连接起来的链条。
在UIResponder类中有个属性
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
响应者链就是由nextResponder指针连接起来的
事件处理的过程
第一步,寻找第一响应者
- 发生触摸事件后,压力转为电信号,系统将产生UIEvent事件,记录事件产生的时间和类型。
- 系统会将该事件添加到UIApplication管理的事件队列中。
- UIApplication将事件队列中的第一个事件分发给UIWindow,这时就会调用UIWindow的hitTest:withEvent:方法,看看当前触摸的位置是否在window内。
- 如果在,则依此调用其subView的hitTest:withEvent:方法,这个方法最后会返回一个view,即在图层结构中会找到最合适的视图,来处理触摸事件
图层结构如图所示
这里涉及到的方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01 || ![self pointInside:point withEvent:event] || ![self _isAnimatedUserInteractionEnabled]) {
return nil;
} else {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event];
if (hitView) {
return hitView;
}
}
return self;
}
}
// - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
//该方法判断触摸点是否在当前视图内
这里遍历子视图时是逆序的,即遍历subview时,是从view.subviews的lastObject到firstObject的顺序,找到合适的响应者view就停止遍历
还有一点需要注意
在打印视图层级结构中部分视图执行hitTest和pointInSide方法中可以看到,viewController并没有执行这两个方法。
所以传递链中没有controller,因为controller本身不具有大小的概念。
而响应链中有controller,因为controller继承UIResponder。
通过代码可以看到,有四种情况view不会接收触摸事件
- hidden属性为NO
- userInteractionEnabled属性为YES,该属性表示允许控件同用户交互
- alpha属性值小于0.01
- 触摸点不在这个触摸范围内
过程如下
到这里我们已经找到了第一响应者
第二步,响应UIEvent事件
经过上面的步骤,UIApplication已经找到了第一响应者,接下来UIWIndow就会讲UIEvent对象直接交给第一响应者
视图之所以能够处理UIEvent,是因为它们都是Responder对象,UIResponder中定义的那四个方法,用来处理UIEvent事件。
如果视图要响应处理UIEvent事件,那就必须要重写那四个方法。
默认情况下视图并没有重写那四个方法,此时就会走UIResponder的默认实现。
而默认实现就是:传递响应链
也就是如果该视图没有实现重写touch方法,那么事件就会沿着响应者链传递给它的下一个响应者(nextResponder),直到能遇到能够处理该事件的响应者或者响应者链结束。
我们也可以在重写touch方法中加入[super touch],使多个响应者同时响应同一事件。
沿着响应者链的响应过程
- 如果view是一个viewController的root view,nextResponder就是viewController
- 如果view不是viewControllerView的rootView,nextResponder就是view的super view
- 如果viewController的view是window的root view,viewController的nextResponder就是这个window
- 如果viewController是被其他viewController present来的,那么viewController的nextResponder就是调用present的那个viewController
- window的nextResponder是UIApplication对象
- 如果UIApplication也不能处理该事件或消息,则将其丢弃。
UIControl的Target-Action设计模式
UIResponder可以完全检测触摸事件,但却不能区分不同类型的触摸事件
而UIControl就可以区分不同类型的触摸事件,并且会为特定的触摸分配事件
在UIControl及其子类(UIButton…)的设计上,iOSAPI采用了Target-Action设计模式。
[button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
上面的代码我们经常会写,也是典型的Target-Action设计模式
- 第一个参数,Target,UIEvent的新的作用对象(响应者)
- 第二个参数,Action,是对该UIEvent做出响应的具体动作。
- 第三个参数,是对UIEvent的抽象映射
UIControl通过这种Target-Action的方法对UIEvent进行了转发,从而可以把UIEvent事件转发给任意对象处理。(原本只能UIResponder对象和手势识别对象才能处理)
UIControl实现的具体做法是,重写了-touchesBegan相关方法,改变了响应者链来实现
- 首先在touchesBegan方法中调用-sendAction:to:forEvent:把消息先转发给UIApplication,让其统一处理。
- 然后UIApplication调用sendAction:to:from:forEvent:把消息交给具体的Target处理。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self sendAction:@selector(buttonClicked:) to:target forEvent:event];
}
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[[UIApplication sharedApplication] sendAction:action to:target from:self forEvent:event];
}
在button设置的target-action中,执行action方法时打上断点可以看到这个大概的调用流程
所以对于button的事件传递以及相应的过程应该是
- 点击button,即发生触摸事件,系统会产生一个UIEvent事件,将这个事件交给由UIappliction管理的事件队列中,并从中取出第一个事件分发下去。
- 首先会交给window,执行window的hitTest方法、pointInSide方法,当触摸位置在window内则继续遍历其子视图。
- 在视图层次结构中最终会找到第一响应者,这时就应该是button,hitTest方法会向上返回button一直到UIApplication。
- UIApplication就会将事件UIEvent交给这个button
- 而button所属的UIButton类并没有重写touchBegan等方法,所以执行的是父类UIControl的touchBegan方法,因为UIControl类重写了UIResponder的touchBegan方法
它是实现是调用sendAction:to:forEvent方法,把消息让转发给单例UIApplication进行分派
UIApplication再调用sendAction:to:from:forEvent:交给具体的target来执行action方法。如果目标对象为nil,就会在响应链中搜索定义该方法的对象。
扩大点击范围
//返回矩形是否包含指定的点。
//rect 要检查的矩形。
//point 检查的点。
CG_EXTERN bool CGRectContainsPoint(CGRect rect, CGPoint point)
CG_AVAILABLE_STARTING(10.0, 2.0);
//返回一个比源矩形小或大且具有!!相同中心点!!的矩形。
//rect 源CGRect结构。
//dx 用于调整源矩形的x坐标值。若要创建嵌入矩形,请指定一个正值。若要创建更大的包含矩形,请指定负值。
//dy 用于调整源矩形的y坐标值。若要创建嵌入矩形,请指定一个正值。若要创建更大的包含矩形,请指定负值。
CGRect CGRectInset(CGRect rect, CGFloat dx, CGFloat dy);
创建一个UIButton的子类,并重写pointInSide方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect bounds = CGRectInset(self.bounds, -10, -10);
return CGRectContainsPoint(bounds, point);
}
或者重写hitTest方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGRect bounds = CGRectInset(self.bounds, -10, -10);
if (CGRectContainsPoint(bounds, point)) {
return self;
} else {
return nil;
}
}
穿透
如图,视图1与视图2有重合部分3,当点击3时,我们希望视图1来响应这个点击事件
这时应该重写绿色部分的hitTest的方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint point1 = [self convertPoint:point toView:_purpleView];
if ([_purpleView pointInside:point1 withEvent:event]) {
return _purpleView;
} else {
return [super hitTest:point withEvent:event];
}
}
参考
参考