事件也有自己的生命周期,按照时间的顺序来看,事件的声明是下面这样子的:
事件的产生和传递(事件如何从父控件传递到子控件并寻找最合适的view
,寻找最合适的view
的底层实现,拦截事件的处理)---->找到最合适的view
后事件的处理(touch
方法的重写,也就是事件的响应)。
重点:1.如何找到最合适的view
。2.寻找最合适的view
的底层实现。
iOS中的触摸事件
想了解iOS中的触摸事件就需要学习一个重要的知识点:响应者对象(UIResponder)。
iOS中不是所有的控件都能处理事件,只有继承UIResponder的对象才可以处理事件。比如UIApplication,UIViewController,UIView
。
那么为什么继承自UIResponder的对象才能处理事件呢?因为UIResponder提供了以下几个方法来接收处理事件。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
iOS中的触摸事件处理
// UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件
// 一根或者多根手指开始触摸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
// 提示:touches中存放的都是UITouch对象
以上四个方法是系统自动调用的,可以通过重写这几个方法来处理一些事件。
1.如果两根手指同时触摸一个view
,那么系统只会调用一次touchesBegan
方法,touches
参数中装着两个UITouch
对象。
2.如果一前一后分别触摸一个view
,则会调用两次touchesBegan
方法,touches
参数中分别装有一个UITouch
对象。
3.重写以上四个方法,如果是重写UIView
的触摸事件,必须要自定义UIView
并继承自UIView
。
4.如果重写UIViewControlle
r的触摸事件,直接在控制器内重写这四个方法即可。
代码示例:
RespondsView.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface RespondsView : UIView
@end
NS_ASSUME_NONNULL_END
RespondsView.m
#import "RespondsView.h"
@implementation RespondsView
// 开始触摸时就会调用一次这个方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"摸我干啥!");
}
// 手指移动就会调用这个方法
// 这个方法调用非常频繁
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"哎呀,不要拽人家!");
}
// 手指离开屏幕时就会调用一次这个方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"手放开还能继续玩耍!");
}
@end
ViewController.m
#import "ViewController.h"
#import "RespondsView.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
RespondsView *touchView = [[RespondsView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
touchView.backgroundColor = [UIColor redColor];
[self.view addSubview:touchView];
}
UIView的拖拽
实现UIView
的拖拽就需要重写touchesMoved
方法,此时则需要用到参数touches
,我们看一下UITouch
的各种属性和方法。
NS_CLASS_AVAILABLE_IOS(2_0) @interface UITouch : NSObject
触摸产生时所处的窗口
@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;
- (CGPoint)locationInView:(UIView *)view;
//返回值表示触摸在view上的位置
//这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
//调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)previousLocationInView:(UIView *)view;
//记录了前一个触摸点的位置
控件拖拽的实现代码
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"哎呀,不要拽人家!");
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
// 获取上一个点
CGPoint prePoint = [touch previousLocationInView:self];
CGFloat offsetX = currentPoint.x - prePoint.x;
CGFloat offsetY = currentPoint.y - prePoint.y;
NSLog(@"FlyElephant----当前位置:%@---之前的位置:%@",NSStringFromCGPoint(currentPoint),NSStringFromCGPoint(prePoint));
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}
UITouch的作用
1.保存着跟手指相关的信息,比如触摸的时间,位置,阶段。
2.当手指移动时,系统会更新同一个UITouch
对象,使之能一直保存该手指所在的位置。
3.手指离开屏幕时,系统会销毁相应的UITouch
对象。
iOS中事件的产生和传递
1.事件的产生
(1)发生触摸事件时,系统会将事件加入到一个由UIApplication
管理的队列中,为什么是队列而不是栈,因为队列是FIFO,先产生的事件先处理才对。
(2)UIApplication
会从队列中取出最前面的事件,然后分发下去,通常会先分发给事件的主窗口(keyWindow)
。
(3)主窗口会在视图层次中找出一个最适合的视图来处理触摸事件,这也是整个事件处理过程的第一步。找到合适的视图后,就会调用视图的touches
方法来做具体的事件处理。
2.事件的处理
(1)事件的传递是从父控件传递到子控件
(2)UIApplication
->keyWindow
->寻找最合适的view
ps.如果父控件无法处理事件,子控件是不可能处理的。
pps.view
的透明度小于等于0.01
时,view
就将无法处理事件。
应用如何找到最合适的视图来处理事件
1.首先判断主窗口是否可以处理事件
2.判断触摸点是否在自己身上
3.子控件在数组中从后往前遍历重复前两个步骤
4.遍历到的view
,比如叫做fitview
,然后再遍历fitView
的子控件,直到没有更合适的view
。
5.如果没有更合适的子控件,那么就认为自己是最符合的。
如何打破事件的传递链呢?可以设置
userInteractionEnabled
为no
,比如设置蓝色的userInteractionEnabled
为no
,那么蓝色以及黄色的点击事件只能由橙色来处理了,所以不管视图能不能处理事件,只要点击了视图,就会产生事件,关键是最终由谁来处理!如果蓝色不能处理事件,即使点击了蓝色,产生的事件也不会由蓝色来处理!
如何寻找最合适的View
1.主窗口收到点击事件,首先判断自己能否成为点击事件的处理者,如果能再判断触摸点是否在自己身上。
2.如果触摸点在自己身上,则会从后往前遍历子控件。
3.遍历到每一个子控件后,则会重复前两个步骤(判断自己是否能处理事件,触摸点是否在自己上面)
4.如此循环,知道找不到最合适的子控件,则自己就是最合适的View
。
找到最合适的view
之后就会调用touches
方法来进行相应处理,找不到合适的view
就不会调用touches
方法。
找到合适的View底层方法。
两个重要的方法:
hitTest:withEvent:方法
pointInside方法
什么时候调用?
hitTest:withEvent:
方法:只要事件传递到一个控件,这个控件就会调用自己的hitTest:withEvent:
方法。
作用
找到最合适的view
。
ps.不管这个控件是否能处理事件,也不管触摸点是否在这个控件上,都会调用它的hitTest:withEvent:
方法。
事件的拦截
1.因为hitTest:withEvent:
方法可以返回最合适的view
,所以可以通过重写hitTest:withEvent:
方法来返回指定的view
作为最合适的view
。
2.不管点击那里,处理事件的view
都是hitTest:withEvent:
方法返回的view
。
3.通过重写hitTest:withEvent:
方法可以指定处理事件的view
,想让谁处理就可以让谁处理。
如果hitTest:withEvent:
方法返回nil
,那么调用该方法的控件和子控件都不是最合适的view
,那么最合适的控件就是它的父控件。
所以事件传递的顺序是这样的:产生触摸事件->UIApplication
事件队列->[UIWindow hitTest:withEvent:]
->返回更合适的view
->[子控件 hitTest:withEvent:]
->返回最合适的view
技巧:想让谁成为最合适的view
就重写谁自己的父控件的hitTest:withEvent:
方法返回指定的子控件,或者重写自己的hitTest:withEvent:
方法 return self
。但是,建议在父控件的hitTest:withEvent:
中返回子控件作为最合适的view
!
hitTest:withEvent:底层实现
#import "WSWindow.h"
@implementation WSWindow
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的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;
}
// 作用:判断下传入过来的点在不在方法调用者的坐标系上
// point:是方法调用者坐标系上的点
//- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
//{
// return NO;
//}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
@end
hit:withEvent:
方法底层会调用pointInside:withEvent:
方法判断点在不在方法调用者的坐标系上。如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。
事件的响应
(1)事件处理的过程
1.用户点击屏幕产生一个触摸事件,经过一系列的传递找到最合适的view
来处理这个事件
2.找到最合适的视图控件后就会调用控件的touches
来具体处理事件,touchesBegan…touchesMoved…touchedEnded…
3.这些touches
默认是不处理事件,而是顺着响应者链条向上传递事件,将事件交给上一个响应者进行处理。
(2)响应者链条示意图
在我们开发的程序中视图是有层级的,我们经常会在一个视图控件上添加另一个视图控件,那么我们点击上面这个视图控件时,是上面的来处理事件还是下面的来处理事件呢?这种先后关系就形成一个链条,叫做"响应者链条"。
响应者对象:能处理事件的对象,也就是继承自UIResponder
的对象。
作用:清晰的看到每个响应者之间的关系。
响应者链条事件传递过程
1>如果当前的view
是控制器的view
,那么控制器就是上一个响应者。如果不是控制器的view
,那么其父控件就是上一个响应者。
2>在视图层次的最顶层视图也不能处理事件的话,那么它的事件将会交给window
来处理。
3>如果window
也不处理的话,将交给UIApplication
来处理事件。
4>如果UIApplication
也不处理事件,事件就会被废弃。
事件处理的流程总结
1.触摸屏产生触摸事件之后,会将事件放进一个由UIApplication
管理的队列当中。
2.UIApplication
会将最先进来的事件交给window
去处理。
3.主窗口会在视图层次中找到一个最合适的view
去处理事件。
4.最合适的view会调用自己的touches
方法去处理事件。
5.默认情况会将事件向上传递,交给父控件。
#import "WSView.h"
@implementation WSView
//只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
// 上一个响应者可能是父控件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event];
// 注意不是调用父控件的touches方法,而是调用父类的touches方法
// super是父类 superview是父控件
}
@end
注意:最合适的view
将会先自己处理事件,如果不能处理才会交给上一个响应者处理。上级视图仍然无法处理则一直向上传递。在传递的过程中如果某个控件实现了touches
方法,则事件由这个控件处理。
如何实现一个事件多个对象处理?
重写自己的和父控件的touches方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event];
}
事件的传递和响应的区别
事件的传递是由上到下(父控件->子控件),事件的响应是由下到上(子控件->父控件)。