目录
先说一下事件处理里的被处理者:事件
一、iOS里的事件
二、Flutter里的事件
然后说一下事件处理里的处理者:响应者
三、iOS里的响应者和响应者链
四、Flutter里的响应者和响应者数组
然后再说一下响应者具体是怎么处理事件的
五、iOS里的寻找第一响应者、事件传递和事件响应
六、Flutter里的寻找第一响应者、事件分发和事件响应
最后补充一下原始指针事件和手势同时存在时会怎样
七、iOS里的手势
八、Flutter里的手势
iOS和Flutter里的事件处理
iOS和Flutter里的事件处理实例
一、iOS里的事件
iOS里的事件分三类:
- 触摸事件(本篇我们主要研究一下触摸事件)
- 加速计事件
- 远程控制事件
UITouch
我们的一根手指触摸屏幕,系统就会为其创建一个对应的UITouch
对象,多根手指触摸屏幕,系统就会为其创建多个对应的UITouch
对象。每个UITouch
对象内部都存储着对应手指触摸屏幕的时间、位置、力度、所在的window
、所在的view
等信息。当手指离开屏幕一小段时间后,系统判定对应的UITouch
对象不会再更新,就会销毁它。
@interface UITouch : NSObject
// 时间
@property (nonatomic,readonly) NSTimeInterval timestamp;
// 当前触摸的点在[view]坐标系统下的位置
- (CGPoint)locationInView:(nullable UIView *)view;
// 上一个触摸的点在[view]坐标系统下的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;
// 力度
@property (nonatomic,readonly) CGFloat force API_AVAILABLE(ios(9.0));
// 所在的window
@property (nullable,nonatomic,readonly,strong) UIWindow *window;
// 所在的view
@property (nullable,nonatomic,readonly,strong) UIView *view;
// 寻找第一响应者的过程中,该数组会搜集第一响应者hitTested view上添加的手势、hitTested view父视图上添加的手势、...、直到window上添加的手势,这一串view的手势都会被依次添加到这个数组里
@property (nullable,nonatomic,readonly,copy) NSArray *gestureRecognizers API_AVAILABLE(ios(3.2));
@end
UIEvent
创建UITouch
对象的时候,系统也会创建一个UIEvent
对象。UIEvent
对象内部存储着当前触摸事件的类型以及触发当前触摸事件的UITouch
对象集合(因为一个触摸事件可能是由多根手指共同触发的)。销毁UITouch
对象的时候,也会销毁UIEvent
对象。
@interface UIEvent : NSObject
// 当前触摸事件的类型
@property (nonatomic,readonly) UIEventType type API_AVAILABLE(ios(3.0));
// 触发当前触摸事件的UITouch对象集合
@property (nonatomic, readonly, nullable) NSSet *allTouches;
@end
二、Flutter里的事件
Flutter里也有相应的触摸事件,叫PointerEvent
及其子类PointerDownEvent
、PointerMoveEvent
、PointerCancelEvent
、PointerUpEvent
,它们内部存储的东西和iOS差不多,如触摸屏幕的时间、位置(相对于全局坐标系统,如有需要我们得自己转换成局部坐标)、力度、当前触摸事件的类型等。
abstract class PointerEvent with Diagnosticable {
const PointerEvent({
// 当前原始指针事件的唯一标识
this.pointer = 0,
// 触摸屏幕的时间
this.timeStamp = Duration.zero,
// 触摸屏幕的位置
this.position = Offset.zero,
// 触摸屏幕的力度
this.pressure = 1.0,
this.pressureMin = 1.0,
this.pressureMax = 1.0,
// 当前触摸事件的类型
this.kind = PointerDeviceKind.touch,
// 两次原始指针移动事件(PointerMoveEvent)的距离
this.delta = Offset.zero,
// 是否正摸着屏幕
final bool down;
...
});
}
class PointerDownEvent extends PointerEvent {
...
}
class PointerMoveEvent extends PointerEvent {
...
}
class PointerUpEvent extends PointerEvent {
...
}
class PointerCancelEvent extends PointerEvent {
...
}
三、iOS里的响应者和响应者链
响应者
iOS里并非所有的对象都能传递和响应事件,只有继承自UIResponder
的才行,这类对象被称之为响应者。我们常见的UIApplication
、UIViewController
、UIView
都继承自UIResponder
,所以它们都能传递和响应事件。注意这里的传递是指寻找第一响应者阶段父视图通过hitTest
方法把事件传递给子视图以及事件传递阶段UIApplication
、window
通过sendEvent
方法把事件精准地传递给第一响应者,响应是指事件响应阶段UIResponder
通过touchesBegan、touchesMoved、touchesEnded、touchesCancelled
四个方法来响应事件。
响应者链
实际开发中,我们的屏幕上肯定不止一个view
,也就是说不止一个响应者,而这众多的响应者之间会通过nextResponder
属性串起来形成一个叫响应者链的东西,这个链的形成时机是我们把view
层级的代码写好后就形成了(可以在addSubview:
之后打印验证),不需要等到hitTest
的时候。也就是说当我们把一个view
添加到它的父视图上后,该view
的nextResponder
就已经指向了它的父视图;父视图的nextResponder
又会指向rootViewController
的view
;rootViewController
的view
的nextResponder
又会指向rootViewController
;rootViewController
的nextResponder
又会指向window
;window
的nextResponder
又会指向UIApplication
。
四、Flutter里的响应者和响应者数组
响应者
Flutter里也并非所有的对象都能传递和响应事件,只有真正渲染在屏幕上的东西——即RenderObject
(相当于iOS里的UIView
)才行,我们也把它们称之为响应者,同时也只有一个特殊的RenderObject
——RenderPointerListener
(对应的渲染对象Widget为Listener
)才能响应事件(iOS里是所有的UIView
都能响应事件)。注意这里的传递是指寻找第一响应者阶段父视图通过hitTest
方法把事件(准确地说是点击的位置)传递给子视图,响应是指事件响应阶段RenderPointerListener/Listener
通过onPointerDown、onPointerMove、onPointerUp、onPointerCancel
四个方法来响应事件。
这里我们回顾一个知识点:
——Widget
------------ComponentWidget(组件Widget)
————————StatelessWidget
————————StatefulWidget
————RenderObjectWidget(渲染对象Widget)
————————SingleChildRenderObjectWidget
————————MultiChildRenderObjectWidgetWidget可以分为两类:组件Widget和渲染对象Widget。
- 组件Widget是指那些仅仅起到包装其它Widget的作用、Flutter Framework并不会为它们创建对应的
RenderObject
的Widget,例如我们常用的Container、Text、Image、ListView、GridView、PageView、自定义的Widget等,总之但凡是继承自StatelessWidget或StatefulWidget的Widget都是组件Widget。- 渲染对象Widget是指那些Flutter Framework会为它们创建对应的
RenderObject
的Widget,例如我们常用的SizedBox、Row、Column等,总之但凡是继承自RenderObjectWidget的Widget都是渲染对象Widget。也就是说,组件Widget肯定都不是响应者,因为它们压根儿都没真正渲染在屏幕上,只有渲染对象Widget才是响应者(准确地说是它们对应的
RenderObject
才是响应者),因为它们才会被转换成RenderObject
真正渲染在屏幕上。因此,如果我们想重写某些Widget的
hitTest
方法,就不能继承自组件Widget,因为组件Widget根本就没有hitTest
方法,而必须继承自渲染对象Widget,它对应的RenderObject
才有hitTest
方法。同时RenderObject
是个抽象类,真正渲染在屏幕上的东西其实是它的子类RenderBox
,而RenderBox
又是个抽象类,真正渲染在屏幕上的东西其实又是它的子类:单个子对象的时候,就用RenderProxyBox
或RenderShiftedBox
,它俩的主要区别是前者没有跟布局相关的属性,后者有跟布局相关的属性;多个子对象的时候,就用ContainerRenderObjectMixin
,我们可以根据实际情况给渲染对象Widgetcreate
不同的RenderObject
。
响应者数组
这里和iOS稍有不同,iOS里是响应者链,Flutter里是响应者数组,不过两者的用途差不多。
iOS里的响应者链是指众多的响应者之间会通过nextResponder
属性串起来形成一个链,当前响应者的nextResponder
就是下一个响应者,这个链的形成时机是我们把view
层级的代码写好后就形成了,不需要等到hitTest
的时候。Flutter里的响应者数组是指众多的响应者会按顺序放在一个数组里,当前响应者在数组里的下一个元素就是下一个响应者,这个数组的形成时机是hitTest
的时候(这里的形成时机是指数组把响应者全都add
进去,不是指数组本身的创建)。
五、iOS里的寻找第一响应者、事件传递和事件响应
有了前两节的理论知识,我们就来回答一个问题“手指触摸屏幕后,发生了什么”,一共分三步:
- 第一步:寻找第一响应者
- 第二步:事件传递
- 第三步:事件响应
寻找第一响应者
手指触摸屏幕后,就发生了一个触摸事件,但是这个时候屏幕上可能会有很多个响应者,也就是说可能会有很多个view
,那到底该由谁来响应这个触摸事件呢?因此第一步就是要寻找一个最适合响应该触摸事件的响应者——第一响应者firstResponder
。
寻找第一响应者的过程涉及到一个关键方法hitTest
,寻找第一响应者的过程可以说就是一个递归调用hitTest
的过程:
/// @return 当前view所在层级的第一响应者
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 如果当前view不能响应事件:当前view不允许用户交互 || 当前view隐藏了 || 当前view的透明度小于等于0.01
// 那么当前view不能作为第一响应者
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
return nil;
}
// 如果触摸的点不在当前view身上
// 那么当前view不能作为第一响应者
if ([self pointInside:point withEvent:event] == NO) {
return nil;
}
// 如果过了前两关,则代表当前view有可能作为第一响应者,但不一定就是它
// 还得倒序遍历它的子视图,优先它的子视图做第一响应者
for (int i = self.subviews.count - 1; i >= 0; i--) {
// 获取子视图
UIView *childView = self.subviews[I];
// 把触摸点的坐标转换到子视图的坐标系统下
CGPoint convertedPoint = [self convertPoint:point toView:childView];
// 调用子视图的hitTest方法,把触摸事件传递给子视图,寻找子视图这一层级的第一响应者
UIView *fitView = [childView hitTest:convertedPoint withEvent:event];
if (fitView) { // 如果最终找到了就返回
return fitView;
}
}
// 只有当前view没有子视图 || 它的子视图都不能响应事件 || 触摸的点都不在它的子视图身上时
// 当前view才直接作为第一响应者
return self;
}
- 手指触摸屏幕后,就发生了一个触摸事件,系统会先把这个触摸事件放进
UIApplication
管理的一个事件队列里,等轮到处理该触摸事件时,UIApplication
就会把该触摸事件出列,并把该触摸事件倒序传递给应用程序的window
;(注意:因为这个时候才刚开始寻找第一响应者,所以触摸事件还不知道它所在的window
是哪个,因此UIApplication
还不能精准地给某个特定的window
传递事件,而是按显示顺序倒序地给很多个window
都传递) -
window
接收到触摸事件后,就会调用自己的hitTest
方法,如果发现自己不能响应事件或者触摸的点不在自己身上,UIApplication
就会把触摸事件传递给其它的window
,通常情况下触摸事件最终会被传递给keyWindow
,keyWindow
也会调用自己的hitTest
方法,通常情况下keyWindow
能响应事件并且触摸的点也在keyWindow
身上,于是keyWindow
又会把该触摸事件倒序传递给它的子视图;(注意:这里执行完,就找到了触摸事件所在的window
,UIEvent.UITouch.window
属性就有值了) - 子视图接收到触摸事件后,就会调用自己的
hitTest
方法,如果发现自己不能响应事件或者触摸的点不在自己身上,keyWindow
就会把触摸事件传递给其它的子视图,如果某个子视图能响应事件并且触摸的点也在它身上,那么它就会继续把触摸事件倒序传递给它的子视图......如此循环,直到找到第一响应者——即触摸事件所在的view
。(注意:这里执行完,就找到了触摸事件所在的view
,UIEvent.UITouch.view
属性就有值了)
举个例子,view
层级如下:
WhiteView(rootViewController的view)
————RedView
————————YellowView
————OrangeView
————————GreenView
————————CyanView
- 假设我们触摸了
GreenView
; -
keyWindow
调用hitTest
方法,发现自己能响应事件并且触摸的点也在自己身上,于是就把触摸事件传递给它的子视图WhiteView
; -
WhiteView
调用hitTest
方法,发现自己能响应事件并且触摸的点也在自己身上,于是就把触摸事件传递给它的子视图OrangeView
; -
OrangeView
调用hitTest
方法,发现自己能响应事件并且触摸的点也在自己身上,于是就把触摸事件传递给它的子视图CyanView
; -
CyanView
调用hitTest
方法,发现自己能响应事件但是触摸的点不在自己身上,于是OrangeView
又把触摸事件传递给它的子视图GreenView
; -
GreenView
调用hitTest
方法,发现自己能响应事件并且触摸的点在自己身上,但是自己已经没有子视图了,于是不再做事件传递,所以GreenView
就成为第一响应者。
一些经验:
hitTest
第一关:如果一个父视图不能响应事件,那么它的子视图肯定就不能响应事件,因为父视图压根就没机会把触摸事件传递给子视图,子视图都不知道这个触摸事件的存在,当然就不能响应事件;hitTest
第二关:如果一个父视图能响应事件,它的子视图不能响应事件,那么点击子视图时父视图就会响应事件,但是如果子视图有超出父视图的部分,那么点击子视图超出父视图的部分时,父视图就不会响应事件,因为父视图在判断到触摸的点不在自己身上时就会直接return nil
,而不会作为第一响应者;- 寻找第一响应者的过程中存在事件传递,父视图是通过
hitTest
方法把事件传递给子视图的,因此在实际开发中我们可以重写视图的hitTest
方法来自定义到底由谁来做第一响应者——即到底由谁来响应触摸事件。至于该重写谁的hitTest
方法,我们只需要分析一遍hitTest
的过程,看看到底是谁的hitTest
方法导致不满足实际需求就可以了。
事件传递
经过第一步寻找第一响应者,UIApplication
就知道触摸事件该由谁来响应了,因为UIEvent.UITouch.window
属性和UIEvent.UITouch.view
属性都有值了,接下来要做的就是第二步:将触摸事件传递给第一响应者。UIApplication
会通过sendEvent
方法把触摸事件精准地传递给触摸事件所在的window
,window
又会通过sendEvent
方法把触摸事件精准地传递给触摸事件所在的view
——即第一响应者。
还是上面的例子,我们在GreenView
的touchesBegan
方法里打个断点,触摸一下GreenView
,查看方法调用栈就能看到事件传递的过程:
事件响应
我们知道UIResponder
内部提供了四个方法来响应触摸事件,实际上系统为所有的响应者都默认实现了这四个方法,只不过大家默认的实现都是什么都不做,只是调用父类的touches...
方法把触摸事件沿着响应者链传递给nextResponder
。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 什么都不做
// 只是调用父类的touches...方法把触摸事件沿着响应者链传递给nextResponder
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// 什么都不做
// 只是调用父类的touches...方法把触摸事件沿着响应者链传递给nextResponder
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
// 什么都不做
// 只是调用父类的touches...方法把触摸事件沿着响应者链传递给nextResponder
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
// 什么都不做
// 只是调用父类的touches...方法把触摸事件沿着响应者链传递给nextResponder
[super touchesCancelled:touches withEvent:event];
}
经过第二步事件传递,第一响应者就接收到了需要响应的触摸事件,接下来要做的就是第三步:事件响应。如果第一响应者重写了触摸事件的四个方法,那么它就会响应该触摸事件;如果第一响应者没有重写触摸事件的四个方法,那么它就不会响应该触摸事件,该触摸事件就会默认地沿着响应者链传递给第一响应者的nextResponder
;这样一直传一直传,如果触摸事件传递到
了window
乃至UIApplication
,UIApplication
都没有重写触摸事件的四个方法,那么该触摸事件就会被丢弃。
一些经验:
- 实际开发中我们可以重写这四个方法来完成一些自定义的操作,并且可以主动决定要不要调用父类的
touches...
方法来把触摸事件继续沿着响应者链传递。
六、Flutter里的寻找第一响应者、事件分发和事件响应
那在Flutter里“手指触摸屏幕后,发生了什么”,答案和iOS基本一样,还是分三步:
- 第一步:寻找第一响应者
- 第二步:事件分发(iOS里是事件传递)
- 第三步:事件响应
寻找第一响应者
Flutter里寻找第一响应者的过程和iOS里几乎一模一样,都是一个递归调用hitTest
的过程——PointerDown
事件发生后,就从根视图RenderView
的hitTest
方法开始,倒序递归调用子视图的hitTest
方法,如果判断到触摸的点在某个视图内部,就把它放进响应者数组里,位于视图层级上方的视图会被优先放进响应者数组,最终响应者数组的第一个元素就会成为第一响应者。hitTest
的默认实现:
bool hitTest(HitTestResult result, { @required Offset position }) {
...
if (_size.contains(position)) { // 如果触摸的点在Widget范围内
// 就去检测在不在子视图的范围内 || 执行hitTestSelf
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
// 如果在子视图的范围内,就把子视图和自己都添加进响应者数组
result.add(BoxHitTestEntry(this, position));
// 同时return true,告诉父视图已经命中了,不用去hitTest自己的兄弟视图了
return true;
}
}
// 如果触摸的点不在Widget范围内,直接return false,告诉父视图去hitTest自己的兄弟视图
return false;
}
还是iOS里举过的例子,view
层级如下:
WhiteView
————RedView
————————YellowView
————OrangeView
————————GreenView
————————CyanView
- 假设我们触摸了
GreenView
; -
RenderView
调用hitTest
方法,发现触摸的点也在自己身上,于是就把触摸事件传递给它的子视图WhiteView
; -
WhiteView
调用hitTest
方法,发现触摸的点也在自己身上,于是就把触摸事件传递给它的子视图OrangeView
; -
OrangeView
调用hitTest
方法,发现触摸的点也在自己身上,于是就把触摸事件传递给它的子视图CyanView
; -
CyanView
调用hitTest
方法,发现触摸的点不在自己身上,于是OrangeView
又把触摸事件传递给它的子视图GreenView
; -
GreenView
调用hitTest
方法,发现触摸的点在自己身上,但是自己已经没有子视图了,于是不再做事件传递,所以GreenView
就成为第一响应者——即响应者数组里的第一个元素; - 经过这么一轮查找,响应者数组里依次存放的就是
[GreenView、OrangeView、WhiteView、RenderView]
。
事件分发
这一步和iOS有区别,iOS里是事件传递——即window
会精准地把触摸事件传递给第一响应者,而Flutter里是事件分发——即GestureBinding
会遍历响应者数组里所有的响应者,按顺序把触摸事件分发给所有的响应者。
------GestureBinding------
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
// hitTestResult.path就是响应者数组
// entry就是对响应者包装后的一个对象,entry.target就是响应者
for (final HitTestEntry entry in hitTestResult.path) {
// entry.target.handleEvent就是调用响应者的handleEvent方法,而handleEvent方法里会真正调用Listener的四个方法
entry.target.handleEvent(event.transformed(entry.transform), entry);
}
}
事件响应
这一步和iOS也有区别,iOS里是如果第一响应者重写了触摸事件的四个方法,那么它就会响应触摸事件,其它的响应者默认是不响应事件的(当然我们也可以自己搞得其它响应者也响应事件),只有第一响应者没有重写触摸事件的四个方法时,它才不会响应该触摸事件,该触摸事件就会默认地沿着响应者链传递给第一响应者的nextResponder
来响应。而Flutter里因为是一次性给所有的响应者都分发了事件,所以只要是实现了四个方法的Listener
都会响应事件,没实现的就不响应,不存在往下一个响应者传递这么一说,只不过是第一响应者会第一个响应、第二个响应者会第二个响应等等。
七、iOS里的手势
这里我们不专门说手势,主要说一下原始指针事件和手势同时存在时会怎样。
UIGestureRecognizer的优先级
实际上,iOS里并非只有UIResponder
才能传递和响应事件,UIGestureRecognizer
也行,而且UIGestureRecognizer
本质上就是对UIResponder
四个方法的封装。常见的手势有点按手势UITapGestureRecognizer
、轻扫手势UISwipeGestureRecognizer
,平移手势UIPanGestureRecognizer
、旋转手势UIRotationGestureRecognizer
、缩放手势UIPinchGestureRecognizer
、长按手势UILongPressGestureRecognizer
。
现在看个例子:
ViewController.view
上添加了一个redView
,redView
上添加了一个平移手势,并且redView
实现了UIResponder
的四个方法。
------ViewController.h------
#import
@interface ViewController : UIViewController
@end
------ViewController.m------
#import "ViewController.h"
#import "RedView.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet RedView *redView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// redView添加平移手势
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
[self.redView addGestureRecognizer:panGestureRecognizer];
}
- (void)pan:(UIPanGestureRecognizer *)panGestureRecognizer {
NSLog(@"redView panned");
}
@end
------RedView.h------
#import
@interface RedView : UIView
@end
------RedView.m------
#import "RedView.h"
@implementation RedView
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"redView touchesBegan");
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"redView touchesMoved");
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"redView touchesEnded");
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"redView touchesCancelled");
}
@end
在redView
上执行一次滑动,控制台的打印如下:
redView touchesBegan // 此时,手势识别器正在识别触摸事件,还没有识别成功...
redView touchesMoved // 此时,手势识别器正在识别触摸事件,还没有识别成功...
redView touchesMoved // 此时,手势识别器正在识别触摸事件,还没有识别成功...
redView touchesMoved // 此时,手势识别器正在识别触摸事件,还没有识别成功...
redView panned // 此时,手势识别器成功识别触摸事件
redView touchesCancelled // 此时,系统取消了redView对触摸事件的响应
redView panned
redView panned
...
从打印可以看出这次滑动触发了redView
的touchesBegan
、touchesMoved
方法,然后触发了一次平移手势的方法,紧接着触发了一次redView
的touchesCancelled
方法,接下来就一直触发平移手势的方法,直至滑动结束我们也没见到触发redView
的touchesEnded
方法。为什么redView
的touches
被cancel
掉了,而不能正常end
?官方文档对此有如下解释:
A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties.
意思就是说:
window
在将触摸事件传递给第一响应者hitTested view
之前,会优先把触摸事件传递给手势识别器。如果手势识别器成功识别了该触摸事件,那么手势识别器就拥有了该触摸事件的响应权,系统就会取消第一响应者hitTested view
对触摸事件的响应;如果手势识别器没能识别该触摸事件,那么第一响应者hitTested view
才拥有该触摸事件的响应权。一言以蔽之,手势识别器拥有比
UIResponder
更高的事件响应优先级。(注意如果自己身上没有添加手势,那父视图、爷视图......身上的手势也会比自己的原始指针事件响应优先级高,因为UIEvent.UITouch.gestureRecognizers
里存储的是响应者链上所有响应者的手势,不仅仅是自己身上的手势)
UIGestureRecognizer的两个属性和一个代理方法
@property (nonatomic) BOOL cancelsTouchesInView;
@property (nonatomic) BOOL delaysTouchesBegan;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
- cancelsTouchesInView
默认值为YES
,代表如果手势识别器成功识别了触摸事件,那么手势识别器就拥有该触摸事件的响应权,系统就取消第一响应者hitTested view
对触摸事件的响应。
如果设置为NO
,代表就算手势识别器成功识别了触摸事件,系统也不取消第一响应者hitTested view
对触摸事件的响应,即手势识别器和第一响应者hitTested view
同时响应触摸事件。
上面例子中如果设置panGestureRecognizer.cancelsTouchesInView = NO;
,那么控制台将会打印:
redView touchesBegan
redView touchesMoved
redView touchesMoved
redView touchesMoved
redView panned
redView touchesMoved
redView panned
redView touchesMoved
redView panned
redView touchesMoved
...
redView touchesEnded
- delaysTouchesBegan
默认值为NO
,代表window
不仅会把触摸事件传递给手势识别器,而且在手势识别器识别事件期间还会把触摸事件传递给第一响应者hitTested view
。
如果设置为YES
,代表window
只会把触摸事件传递给手势识别器,不会传递给第一响应者hitTested view
,即只有手势识别器响应触摸事件。
上面例子中如果设置panGestureRecognizer.delaysTouchesBegan = YES;
,那么控制台将会打印:
redView panned
redView panned
redView panned
...
-
gestureRecognizer: shouldReceiveTouch:
(enabled
属性也行)
默认返回YES
,代表手势识别器响应触摸事件。
如果返回NO
,代表手势识别器不响应触摸事件,即只有第一响应者hitTested view
响应触摸事件。
上面例子中如果设置
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return NO;
}
那么控制台将会打印:
redView touchesBegan
redView touchesMoved
redView touchesMoved
redView touchesMoved
...
redView touchesEnded
八、Flutter里的手势
这里我们也不专门说手势、手势竞技,也主要说一下原始指针事件和手势同时存在时会怎样。
GestureDetector的优先级
实际上,Flutter里也不止Listener
才能传递传递和响应事件,GestureDetector
也行,不过话说回来GestureDetector
本质上就是个Listener
。常见的手势也有点按手势、轻扫手势,平移手势、旋转手势、缩放手势、长按手势。
还是iOS里举过的例子:
界面上添加了一个redView
,redView
上添加了一个平移手势,并且redView
实现了Listener
的四个方法。
------main.dart------
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({
Key? key,
}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
padding: EdgeInsets.only(top: 20, left: 20),
alignment: Alignment.topLeft,
child: Listener(
onPointerDown: (_) {
print("redView onPointerDown");
},
onPointerMove: (_) {
print("redView onPointerMove");
},
onPointerUp: (_) {
print("redView onPointerUp");
},
onPointerCancel: (_) {
print("redView onPointerCancel");
},
child: GestureDetector(
onPanUpdate: (_) {
print("redView onPanUpdate");
},
child: Container(
color: Colors.red,
width: 300,
height: 300,
),
),
),
);
}
}
在redView
上执行一次滑动,控制台的打印如下:
flutter: redView onPointerDown
flutter: redView onPointerMove
flutter: redView onPanUpdate
flutter: redView onPointerMove
flutter: redView onPanUpdate
flutter: redView onPointerMove
flutter: redView onPanUpdate
flutter: redView onPointerUp
...
从打印可以看出原始指针事件和平移手势会同时触发,并不像iOS里那样手势会具备更高的优先级。但是手势的响应总是在原始指针事件的后面,这是为什么?
当我们PointDown
到redView
上时,会先执行第一步寻找第一响应者,最先触发的是RenderBinding
的hitTest
,里面就是先做UI的hitTest
——即从renderView
开始递归调用hitTest
,把命中的子视图都添加到响应者数组里,这一步GestureDetector对应的Listener
会被先放进响应者数组里,然后Listener
也会被先放进响应者数组里,此时响应者数组里存放的就是[GestureDetector对应的Listener、Listener]
;然后才会做手势的hitTest
——手势的hitTest
比较简单,就是把GestureBinding
这个类本身添加到响应者数组里,手势相关的回调其实都放在GestureBinding
类里由这个类处理,此时响应者数组里存放的就是[GestureDetector对应的Listener、Listener、GestureBinding]
;到此响应者数组就确定了,第一响应者也就确定了——它就是GestureDetector对应的Listener
,第二响应者就是Listener
,第三响应者才是GestureBinding
所以手势的响应总是在原始指针事件的后面。
------RenderBinding------
@override
void hitTest(HitTestResult result, Offset position) {
// UI的hitTest:从根节点开始进行命中测试
renderView.hitTest(result, position: position);
// 手势的hitTest:会调用GestureBinding中的hitTest方法
super.hitTest(result, position);
}
两个拦截事件的Widget
如果我们只想让原始指针事件和手势中的一个响应事件,那就换换它们的父子关系,给子视图外面套一个IgnorePointer
或AbsorbPointer
就行了,它俩分别有一个bool
值属性叫ignoring
、absorbing
用来决定是否拦截事件,我们可以根据实际情况来改变这俩属性的值,其实这俩Widget拦截事件的本质就是拦截响应者不被添加进响应者数组里。
参考
1、史上最详细的iOS之事件的传递和响应机制-原理篇
2、史上最详细的iOS之事件的传递和响应机制-实践篇
3、iOS 事件(UITouch、UIControl、UIGestureRecognizer)传递机制
4、iOS触摸事件全家桶
5、Flutter实战电子书第八章:事件处理与通知
6、Flutter完整开发实战详解(十三、全面深入触摸和滑动原理)
7、Flutter中的事件流和手势简析
8、flutter的RenderBox使用&原理浅析