美文网首页消零派086号
iOS和Flutter里的事件处理

iOS和Flutter里的事件处理

作者: 意一ineyee | 来源:发表于2021-12-28 20:44 被阅读0次

目录

先说一下事件处理里的被处理者:事件
一、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<UIGestureRecognizer *> *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<UITouch *> *allTouches;

@end

二、Flutter里的事件


Flutter里也有相应的触摸事件,叫PointerEvent及其子类PointerDownEventPointerMoveEventPointerCancelEventPointerUpEvent,它们内部存储的东西和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的才行,这类对象被称之为响应者。我们常见的UIApplicationUIViewControllerUIView都继承自UIResponder,所以它们都能传递和响应事件。注意这里的传递是指寻找第一响应者阶段父视图通过hitTest方法把事件传递给子视图以及事件传递阶段UIApplicationwindow通过sendEvent方法把事件精准地传递给第一响应者,响应是指事件响应阶段UIResponder通过touchesBegan、touchesMoved、touchesEnded、touchesCancelled四个方法来响应事件。

响应者链

实际开发中,我们的屏幕上肯定不止一个view,也就是说不止一个响应者,而这众多的响应者之间会通过nextResponder属性串起来形成一个叫响应者链的东西,这个链的形成时机是我们把view层级的代码写好后就形成了(可以在addSubview:之后打印验证),不需要等到hitTest的时候。也就是说当我们把一个view添加到它的父视图上后,该viewnextResponder就已经指向了它的父视图;父视图的nextResponder又会指向rootViewControllerviewrootViewControllerviewnextResponder又会指向rootViewControllerrootViewControllernextResponder又会指向windowwindownextResponder又会指向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
————————MultiChildRenderObjectWidget

Widget可以分为两类:组件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又是个抽象类,真正渲染在屏幕上的东西其实又是它的子类:单个子对象的时候,就用RenderProxyBoxRenderShiftedBox,它俩的主要区别是前者没有跟布局相关的属性,后者有跟布局相关的属性;多个子对象的时候,就用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,通常情况下触摸事件最终会被传递给keyWindowkeyWindow也会调用自己的hitTest方法,通常情况下keyWindow能响应事件并且触摸的点也在keyWindow身上,于是keyWindow又会把该触摸事件倒序传递给它的子视图;(注意:这里执行完,就找到了触摸事件所在的windowUIEvent.UITouch.window属性就有值了)
  • 子视图接收到触摸事件后,就会调用自己的hitTest方法,如果发现自己不能响应事件或者触摸的点不在自己身上,keyWindow就会把触摸事件传递给其它的子视图,如果某个子视图能响应事件并且触摸的点也在它身上,那么它就会继续把触摸事件倒序传递给它的子视图......如此循环,直到找到第一响应者——即触摸事件所在的view(注意:这里执行完,就找到了触摸事件所在的viewUIEvent.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方法把触摸事件精准地传递给触摸事件所在的windowwindow又会通过sendEvent方法把触摸事件精准地传递给触摸事件所在的view——即第一响应者。

还是上面的例子,我们在GreenViewtouchesBegan方法里打个断点,触摸一下GreenView,查看方法调用栈就能看到事件传递的过程:

事件响应

我们知道UIResponder内部提供了四个方法来响应触摸事件,实际上系统为所有的响应者都默认实现了这四个方法,只不过大家默认的实现都是什么都不做,只是调用父类的touches...方法把触摸事件沿着响应者链传递给nextResponder

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 什么都不做

    // 只是调用父类的touches...方法把触摸事件沿着响应者链传递给nextResponder
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 什么都不做

    // 只是调用父类的touches...方法把触摸事件沿着响应者链传递给nextResponder
    [super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 什么都不做

    // 只是调用父类的touches...方法把触摸事件沿着响应者链传递给nextResponder
    [super touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 什么都不做

    // 只是调用父类的touches...方法把触摸事件沿着响应者链传递给nextResponder
    [super touchesCancelled:touches withEvent:event];
}

经过第二步事件传递,第一响应者就接收到了需要响应的触摸事件,接下来要做的就是第三步:事件响应。如果第一响应者重写了触摸事件的四个方法,那么它就会响应该触摸事件;如果第一响应者没有重写触摸事件的四个方法,那么它就不会响应该触摸事件,该触摸事件就会默认地沿着响应者链传递给第一响应者的nextResponder;这样一直传一直传,如果触摸事件传递到
window乃至UIApplicationUIApplication都没有重写触摸事件的四个方法,那么该触摸事件就会被丢弃。

一些经验:

  • 实际开发中我们可以重写这四个方法来完成一些自定义的操作,并且可以主动决定要不要调用父类的touches...方法来把触摸事件继续沿着响应者链传递。

六、Flutter里的寻找第一响应者、事件分发和事件响应


那在Flutter里“手指触摸屏幕后,发生了什么”,答案和iOS基本一样,还是分三步:

  • 第一步:寻找第一响应者
  • 第二步:事件分发(iOS里是事件传递)
  • 第三步:事件响应

寻找第一响应者

Flutter里寻找第一响应者的过程和iOS里几乎一模一样,都是一个递归调用hitTest的过程——PointerDown事件发生后,就从根视图RenderViewhitTest方法开始,倒序递归调用子视图的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的优先级

UIGestureRecognizer不在响应者链里,更不是UIResponder的子类

实际上,iOS里并非只有UIResponder才能传递和响应事件,UIGestureRecognizer也行,而且UIGestureRecognizer本质上就是对UIResponder四个方法的封装。常见的手势有点按手势UITapGestureRecognizer、轻扫手势UISwipeGestureRecognizer,平移手势UIPanGestureRecognizer、旋转手势UIRotationGestureRecognizer、缩放手势UIPinchGestureRecognizer、长按手势UILongPressGestureRecognizer

现在看个例子:

ViewController.view上添加了一个redViewredView上添加了一个平移手势,并且redView实现了UIResponder的四个方法。

------ViewController.h------

#import <UIKit/UIKit.h>

@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 <UIKit/UIKit.h>

@interface RedView : UIView

@end


------RedView.m------

#import "RedView.h"

@implementation RedView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"redView touchesBegan");
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"redView touchesMoved");
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"redView touchesEnded");
}

- (void)touchesCancelled:(NSSet<UITouch *> *)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
...

从打印可以看出这次滑动触发了redViewtouchesBegantouchesMoved方法,然后触发了一次平移手势的方法,紧接着触发了一次redViewtouchesCancelled方法,接下来就一直触发平移手势的方法,直至滑动结束我们也没见到触发redViewtouchesEnded方法。为什么redViewtouchescancel掉了,而不能正常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的优先级

GestureDetector在响应者数组里,因为它就是个`Listener`

实际上,Flutter里也不止Listener才能传递传递和响应事件,GestureDetector也行,不过话说回来GestureDetector本质上就是个Listener常见的手势也有点按手势、轻扫手势,平移手势、旋转手势、缩放手势、长按手势。

还是iOS里举过的例子:

界面上添加了一个redViewredView上添加了一个平移手势,并且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<MyHomePage> {
  @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里那样手势会具备更高的优先级。但是手势的响应总是在原始指针事件的后面,这是为什么?

当我们PointDownredView上时,会先执行第一步寻找第一响应者,最先触发的是RenderBindinghitTest,里面就是先做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

如果我们只想让原始指针事件和手势中的一个响应事件,那就换换它们的父子关系,给子视图外面套一个IgnorePointerAbsorbPointer就行了,它俩分别有一个bool值属性叫ignoringabsorbing用来决定是否拦截事件,我们可以根据实际情况来改变这俩属性的值,其实这俩Widget拦截事件的本质就是拦截响应者不被添加进响应者数组里。

参考
1、史上最详细的iOS之事件的传递和响应机制-原理篇
2、史上最详细的iOS之事件的传递和响应机制-实践篇
3、iOS 事件(UITouch、UIControl、UIGestureRecognizer)传递机制
4、iOS触摸事件全家桶
5、Flutter实战电子书第八章:事件处理与通知
6、Flutter完整开发实战详解(十三、全面深入触摸和滑动原理)
7、Flutter中的事件流和手势简析
8、flutter的RenderBox使用&原理浅析

相关文章

网友评论

    本文标题:iOS和Flutter里的事件处理

    本文链接:https://www.haomeiwen.com/subject/ywxaxrtx.html