likes
comments
collection
share

详解Flutter手势事件分发原理

作者站长头像
站长
· 阅读数 10

一、整体概括

本文尝试对Flutter中的手势事件做一次详细的原理剖析。从手势事件的起源讲起,然后重点分析手势事件在Flutter层的分发和竞争机制。文章会尽可能的多谈设计思路,不罗列大量代码,让我们不会迷失在代码细节中。

通过阅读本文希望可以帮到读者了解到Flutter中手势的框架设计以及当发生手势冲突时能够知晓冲突的本质所在。(ps: 涉及到原生相关的都是在Android平台下)

关于手势事件相关,计划包含在本篇内输出三篇文章,分别是:

  • 《详解Flutter手势事件分发原理》
  • 《详解Flutter滑动原理》
  • 《Flutter滑动冲突案例分析》

二、事件起源

2.1 起源探索

首先我们先来看下事件是从哪里发生的。

在Android平台,当用户触摸屏幕或者按键操作,首次触发的是硬件驱动,驱动收到事件后,将该相应事件写入到输入设备节点/dev/input/event[x]目录,这便产生了原生态的内核事件。接着输入系统取出原生态的事件,经过层层封装后成为KeyEvent或者MotionEvent,最后交付给相应的目标窗口来消费该输入事件。 详情可参见:Input系统-启动篇等系列文章。

当事件从内核层传递到目标窗口层,与我们比较熟悉的是Activity,关于Android Framework层事件的处理在此不做过多赘述,网上有很多优秀的文章可参考。让我们把重点聚焦在Flutter上面,Flutter在Android平台下其实是存在一个FlutterActivity(io.flutter.embedding.android.FlutterActivity),这里面有一个特别重要的View,叫做FlutterView,FlutterView其实是一个继承自FrameLayout的普通View,只不过在FlutterView中存在SurfaceView或者TextureView的引用,如此便提供了可以自渲染的场地。

Flutter通过SurfaceView或者TextureView提供的Surface在Native层中创建ANativeWindow,通过C++代码将视图绘制至屏幕,如此便将Dart中的UI与Android中UI联系到了一起。

稍微有些跑题,有机会写一篇文章详细介绍下这里

ANativeWindow *window = ANativeWindow_fromSurface(env,surface);

让我们继续关注在事件上面,在FlutterView中重写了onTouchEvent()方法,我们暂且把这个入口作为Flutter事件传递的入口,如下:

// io.flutter.embedding.android.FlutterView
public boolean onTouchEvent(@NonNull MotionEvent event) {
    if (!this.isAttachedToFlutterEngine()) {
        return super.onTouchEvent(event);
    } else {
        if (VERSION.SDK_INT >= 21) {
            this.requestUnbufferedDispatch(event);
        }
        // androidTouchProcessor --> AndroidTouchProcessor
        return this.androidTouchProcessor.onTouchEvent(event);
    }
}

2.2 事件传递

那么事件是如何从Java层传递到Flutter层的呢? 让我们一起来梳理一下这个过程。

2.2.1 Java层

详解Flutter手势事件分发原理

2.2.2 C++层

Java & C++ 通信时需要建立映射关系,当首次加载共享库时会调用以下方法建立关联:

// shell/platform/android/library_loader.cc
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  // ...省略...
  // Register PlatformView
  result = flutter::PlatformViewAndroid::Register(env);
  FML_CHECK(result);
  // ...省略...
  return JNI_VERSION_1_4;
}

// shell/platform/android/platform_view_android_jni_impl.cc
bool PlatformViewAndroid::Register(JNIEnv* env) {
    // ...省略...
    g_flutter_jni_class = new fml::jni::ScopedJavaGlobalRef<jclass>(
      env, env->FindClass("io/flutter/embedding/engine/FlutterJNI"));
    if (g_flutter_jni_class->is_null()) {
        FML_LOG(ERROR) << "Failed to find FlutterJNI Class.";
        return false;
    }
    // ...省略...
    return RegisterApi(env);
}

// 函数注册,用于Java和C++层方法的相互调用
bool RegisterApi(JNIEnv* env) {
    static const JNINativeMethod flutter_jni_methods[] = {
      // ...省略...
      {
          // 将Java层FlutterJNI类的nativeDispatchPointerDataPacket方法
          // 映射到C++层的DispatchPointerDataPacket方法,用于该方法的
          // Java调用C++过程
          .name = "nativeDispatchPointerDataPacket",
          .signature = "(JLjava/nio/ByteBuffer;I)V",
          .fnPtr = reinterpret_cast<void*>(&DispatchPointerDataPacket),
      },
      // ...省略...
    };
    if (env->RegisterNatives(g_flutter_jni_class->obj(), flutter_jni_methods,
                           fml::size(flutter_jni_methods)) != 0) {
    FML_LOG(ERROR) << "Failed to RegisterNatives with FlutterJNI";
    return false;
  }  
}

static void DispatchPointerDataPacket(JNIEnv* env,
                                      jobject jcaller,
                                      jlong shell_holder,
                                      jobject buffer,
                                      jint position) {
  uint8_t* data = static_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
  auto packet = std::make_unique<flutter::PointerDataPacket>(data, position);
  // ANDROID_SHELL_HOLDER->GetPlatformView() -->  PlatformView
  ANDROID_SHELL_HOLDER->GetPlatformView()->DispatchPointerDataPacket(
      std::move(packet));
}

有了上面的注册映射关系,我们再来关注下C++层的方法调用:

C++ 与 Dart层的通信机制原理还未研究,如果有机会也希望可以为此再出一篇文章

详解Flutter手势事件分发原理

2.2.3 Dart层

详解Flutter手势事件分发原理

2.3 小结

以上便是从Linux内核层发出到Flutter层的事件传递流程:Linux Kernel -> Java -> C++ -> Dart

三、事件处理

书接上文,当事件传递到Flutter层后,我们再来分析下在Flutter层的处理流程。

3.1 事件检测

GestureBinding中调用顺序为: _handlePointerDataPacket()->_flushPointerEventQueue()->handlePointerEvent()->_handlePointerEventImmediately(), 主要做的事情就是将设备像素转换为逻辑像素,然后将PointerEvent添加至 _pendingPointerEvents队列后开始遍历队列中的事件,我们重点关注下 _handlePointerEventImmediately( )函数。

GestureBinding#_handlePointerEventImmediately(PointerEvent event)

// State for all pointers which are currently down.
// 对down事件进行保存的map
// key:event.pointer,每次down事件时+1
// value: hit test result,有一个数组path属性,将所有命中的RenderObject保存
final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};

void _handlePointerEventImmediately(PointerEvent event) {
  HitTestResult? hitTestResult;
  if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
    // down事件执行hitTest
    hitTestResult = HitTestResult();
    // 判断组件是否包含该触摸点,从父节点开始遍历
    hitTest(hitTestResult, event.position);
    // down事件将结果保存至_hitTests中,在up事件时使用,一个事件总是从down开始,up结束
    if (event is PointerDownEvent) {
      _hitTests[event.pointer] = hitTestResult;
    }
  } else if (event is PointerUpEvent || event is PointerCancelEvent) {
    // up事件从集合中移除对应的hitTestResult
    hitTestResult = _hitTests.remove(event.pointer);
  } else if (event.down) {
    // down属性表示是否与屏幕有接触,(如[PointerMoveEvent、PointerDownEvent]时会设置为true)
    // 此类事件应被分发到其初始指针down事件所在的位置
    // 这样的设计可以避免重复探测,使用初始指针down事件即可找到分发路径
    hitTestResult = _hitTests[event.pointer];
  }
  if (hitTestResult != null ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) {
    assert(event.position != null);
    // 分发事件
    dispatchEvent(event, hitTestResult);
  }
}

按照上述程序设计,代码将会执行hitTest()函数,但是在进入该函数之前我们先来看下程序运行至此this是哪个对象。观察GestureBinding类声明,发现是mixin类型,这也就意味着是不能直接new出该对象,真正的实现其实是在runApp()创建的WidgetsFlutterBinding对象,也就是此时的this对象:

flutter/src/gestures/binding.dart
mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {}

flutter/src/widgets/binding.dart
void runApp(Widget app, {bool optimizeAttach = false}) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}

class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null) {
      // 此处会隐式调用父类BindingBase的构造函数
      // 在BindingBase构造函数中会调用initInstances()方法初始化7大Binding
      WidgetsFlutterBinding();
    }  
    return WidgetsBinding.instance!;
  }  
}

根据mixin特性,写在后面的类会覆盖前面类的相同函数,观察这七大Binding,只有GestureBindingRendererBinding实现hitTest()函数,且后者会覆盖前者,前者是后者的父类,所以此处会先调用RendererBindinghitTest()函数,关于mixin特性可以阅读Flutter 语法进阶 | 深入理解混入类 mixin

RendererBinding#hitTest(HitTestResult result, Offset position)

@override
void hitTest(HitTestResult result, Offset position) {
 // renderView是渲染树的根节点
  renderView.hitTest(result, position: position);
  // 此处的super会执行到GestureBinding的hitTest函数处
  super.hitTest(result, position);
}

RenderView#hitTest(HitTestResult result, Offset position)

bool hitTest(HitTestResult result, { required Offset position }) {
  // child是RenderBox类型
  if (child != null)
    child!.hitTest(BoxHitTestResult.wrap(result), position: position);
  result.add(HitTestEntry(this));
  return true;
}

RenderBox#hitTest(HitTestResult result, Offset position)

bool hitTest(BoxHitTestResult result, { required Offset position }) {
if (size.contains(position)) {
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
  }
  return false;
}

首先判断给定的position是否在自身的size范围内,接着才执行hitTestChildren()hitTestSelf()函数。这样会存在一个问题超出parent所在范围的child是无法响应事件的,例如经过了transform操作后,存在超出parent的可能,如图1所示点击黄色超过parent区域是不会响应点击事件。

RenderBox是一个抽象类,hitTestChildren()hitTestSelf()函数在RenderBox是空实现,默认都返回false,所以具体实现逻辑在子类中。但是总的思路都是hitTestChildren()调用每一个child的hitTest()函数,如此便开启了递归,而hitTestSelf()判断自身是否符合条件。当满足这两个函数其中之一时就将自身添加至HitTestResult#_path中, 根据方法递归,层级越深的RenderObject,优先被添加。

例如:Render树关系为RenderView->A->B->C,如果以上都满足检测条件HitTestResult#_path的顺序将是:C->B->A->RenderView-> WidgetFlutterBinding 如图2所示。

详解Flutter手势事件分发原理

随着方法的出栈,程序会执行至*RendererBinding#hitTest(HitTestResult result, Offset position) 处,执行super.hitTest(result, position), super指GestureBinding

GestureBinding#hitTest(HitTestResult result, Offset position)

void hitTest(HitTestResult result, Offset position) {
  // 会将自己也添加至HitTestResult,this->WidgetFlutterBinding
  result.add(HitTestEntry(this));
}

随着将WidgetFlutterBinding添加至HitTestResult#_path,同样也伴随着hitTest的结束,即将符合条件的RenderObject收集起来,准备后续的事件分发。

3.2 事件分发

3.2.1 GestureBinding#dispatchEvent

GestureBinding#_handlePointerEventImmediately(PointerEvent event)

void _handlePointerEventImmediately(PointerEvent event) {
  // ... 省略... 3.1小节中构造HitTestResult的过程
  if (hitTestResult != null ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) {
    // 分发事件
    dispatchEvent(event, hitTestResult);
  }
}

RendererBinding#_dispatchEvent(PointerEvent event, HitTestResult? hitTestResult)

@override // from GestureBinding
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
  // ... 省略 ...
  super.dispatchEvent(event, hitTestResult);
}

GestureBinding#_dispatchEvent(PointerEvent event, HitTestResult? hitTestResult)

void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
  // ... 省略 ...
  for (final HitTestEntry entry in hitTestResult.path) {
    try {
      // entry.target -> HitTestTarget
      // 循环调用每一个RenderObject的handleEvent方法
      entry.target.handleEvent(event.transformed(entry.transform), entry);
    } catch (exception, stack) {
     // ... 省略 ...
    }
  }
}

可以看到程序会循环调用HitTestResult#_path中每个元素的handleEvent()函数,该函数是在HitTestTarget这个抽象类中定义,RenderObject实现了该接口,仅是一个空实现,具体实现逻辑由子类提供,大多情况下我们只需要关注RenderPointerListener这个子类(其余子类大多也都是空实现),该类是Listener组件对应的RenderObject, 通常我们用到的GestureDetector内部会封装Listener组件(将会在下一节中详细介绍),事实上IconButtonInkWell等内部也都是Listener

详解Flutter手势事件分发原理

也就是说将会调用RenderPointerListenerhandleEvent()函数,具体代码如下所示:

  // lib/src/gestures/hit_test.dart
abstract class HitTestTarget {
  HitTestTarget._();
void handleEvent(PointerEvent event, HitTestEntry entry);
}

 // lib/src/rendering/object.dart
abstract class RenderObject extends AbstractNode implements HitTestTarget {
  /// Override this method to handle pointer events that hit this render object.
  void handleEvent(PointerEvent event, covariant HitTestEntry entry) { }
}

 // lib/src/rendering/proxy_box.dart
class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
 
  typedef PointerDownEventListener = void Function(PointerDownEvent event);
  PointerDownEventListener? onPointerDown;
 
  RenderPointerListener({
   this.onPointerDown,
   this.onPointerMove,
   this.onPointerUp,
   this.onPointerHover,
   this.onPointerCancel,
   this.onPointerSignal,
   HitTestBehavior behavior = HitTestBehavior.deferToChild,
   RenderBox? child,
  }) : super(behavior: behavior, child: child);

  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    assert(debugHandleEvent(event, entry));
    // 从down事件开始,执行onPointerDown回调函数
    if (event is PointerDownEvent)
      return onPointerDown?.call(event);
    if (event is PointerMoveEvent)
      return onPointerMove?.call(event);
    if (event is PointerUpEvent)
      return onPointerUp?.call(event);
    if (event is PointerHoverEvent)
      return onPointerHover?.call(event);
    if (event is PointerCancelEvent)
      return onPointerCancel?.call(event);
    if (event is PointerSignalEvent)
      return onPointerSignal?.call(event);
  }
}

abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox {
  @override
  bool hitTest(BoxHitTestResult result, { required Offset position }) {
    bool hitTarget = false;
    if (size.contains(position)) {
      hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
      if (hitTarget || behavior == HitTestBehavior.translucent)
        result.add(BoxHitTestEntry(this, position));
    }
    return hitTarget;
  }

  @override
  bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;
}

当一个down事件经过上面的层层传递来到RenderPointerListener#handleEvent中,会调用onPointerDown回调函数,那这个回调函数是从哪里设置的呢?带着这个问题让我们继续下一节。

3.2.2 GestureRecognizer

通常我们一般在业务上会将需要点击事件的组件包裹一个GestureDetector组件,该组件提供了诸如单击、双击、长按、平移、缩放等手势,如下图所示,可以看到提供了一堆回调

详解Flutter手势事件分发原理

GestureDetector本质是一个StatelessWidget,这类组件主要是组合其他组件,这就决定它不是实现核心功能的组件,我们先来看下它的build()函数

GestureDetector#_build(BuildContext context)

Widget build(BuildContext context) {
  final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
  // 当构造函数里面有以下回调的设置时,会创建TapGestureRecognizer手势识别器
  if (onTapDown != null ||
    onTapUp != null ||
    onTap != null ||
    onTapCancel != null ||
    onSecondaryTap != null ||
    onSecondaryTapDown != null ||
    onSecondaryTapUp != null ||
    onSecondaryTapCancel != null||
    onTertiaryTapDown != null ||
    onTertiaryTapUp != null ||
    onTertiaryTapCancel != null
  ) {
  gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
    () => TapGestureRecognizer(debugOwner: this),
    (TapGestureRecognizer instance) {
      instance
        ..onTapDown = onTapDown
        ..onTapUp = onTapUp
        ..onTap = onTap
        ..onTapCancel = onTapCancel
        ..onSecondaryTap = onSecondaryTap
        ..onSecondaryTapDown = onSecondaryTapDown
        ..onSecondaryTapUp = onSecondaryTapUp
        ..onSecondaryTapCancel = onSecondaryTapCancel
        ..onTertiaryTapDown = onTertiaryTapDown
        ..onTertiaryTapUp = onTertiaryTapUp
        ..onTertiaryTapCancel = onTertiaryTapCancel;
    },
  );
 }
  // ... 省略 ...
  return RawGestureDetector(
    gestures: gestures,
    behavior: behavior,
    excludeFromSemantics: excludeFromSemantics,
    child: child,
  );
}

如上代码中我们有看到onTap() != null等判断,进而创建TapGestureRecognizer

这里其实还有根据onDoubleTap != null创建DoubleTapGestureRecognizer等等。最后将创建好的Map<Type, GestureRecognizerFactory> gestures传递至 RawGestureDetector 类中。

Flutter还有一堆xxxGestureRecognizer,他们的作用是用来处理各种不同手势,如:平移、缩放、长按等。可以看到它们的顶层父类是GestureArenaMember也就是竞技场的成员(竞技场在下一章节详细介绍),手势之间的竞争依赖这些手势识别器。

详解Flutter手势事件分发原理

3.2.3 handlePointerDown

观察RawGestureDetector是一个StatefulWidget组件,表明会通过State类维护组件状态,即RawGestureDetectorState,我们直接去观察它的build()函数

RawGestureDetectorState#build()

@override
Widget build(BuildContext context) {
  Widget result = Listener(
    // 重要位置
    onPointerDown: _handlePointerDown, 
    behavior: widget.behavior ?? _defaultBehavior,
    child: widget.child,
  );
  // ... 省略 ...
  return result;
}

在3.2节结尾位置处,我们提了一个问题:

当一个down事件经过上面的层层传递来到RenderPointerListener#handleEvent中,会调用onPointerDown回调函数,那这个回调函数是从哪里设置的呢?

答案就是在RawGestureDetectorState创建Listener时设置的,Listener组件创建对应的RenderObject时,再将onPointerDown等传递至RenderPointerListener中。

Listener#createRenderObject(BuildContext context)

@override
RenderPointerListener createRenderObject(BuildContext context) {
  return RenderPointerListener(
    onPointerDown: onPointerDown,
    onPointerMove: onPointerMove,
    onPointerUp: onPointerUp,
    onPointerHover: onPointerHover,
    onPointerCancel: onPointerCancel,
    onPointerSignal: onPointerSignal,
    behavior: behavior,
  );
}

RenderPointerListener#handleEvent(PointerEvent event, HitTestEntry entry)

@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
  assert(debugHandleEvent(event, entry));
  if (event is PointerDownEvent)
    return onPointerDown?.call(event);
  if (event is PointerMoveEvent)
    return onPointerMove?.call(event);
  if (event is PointerUpEvent)
    return onPointerUp?.call(event);
  if (event is PointerHoverEvent)
    return onPointerHover?.call(event);
  if (event is PointerCancelEvent)
    return onPointerCancel?.call(event);
  if (event is PointerSignalEvent)
    return onPointerSignal?.call(event);
}

稍微总结下:当我们点击屏幕后,从Linux->Java->C++->Dart一路传递过来,根据触摸位置,从RenderView递归遍历Render-Tree,将符合条件的RenderObject添加至HitTestResult#_path中,紧接着遍历HitTestResult#_path,分发事件执行每一个HitTestTarget的handleEvent()函数,大部分情况下HitTestTarget是RenderPointerListener类型。一个事件从down开始,所以会首先执行RenderPointerListener#handleEvent()函数中pointerDown这个条件分支,也就是执行onPointerDown回调。onPointerDown最初在RawGestureDetector#build中创建Listener组件设置。

RawGestureDetector#_handlePointerDown(PointerDownEvent event)

void _handlePointerDown(PointerDownEvent event) {
  // _recognizers!.values 原始数据是我们在GestureDetector#build函数创建的
  // 假定我们只在GestureDetector设置了onTap回调函数,即该map中只有一个TapGestureRecognizer
  for (final GestureRecognizer recognizer in _recognizers!.values)
    // addPointer经过调用会执行到OneSequenceGestureRecognizer#startTrackingPointer
    recognizer.addPointer(event);
}

3.2.4 startTrackingPointer

*OneSequenceGestureRecognizer# *startTrackingPointer(int pointer, [Matrix4? transform])

// key: event.pointer
// value: 竞技场的一个封装类,下一章节会详细讲解
final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
final Set<int> _trackedPointers = HashSet<int>();

void startTrackingPointer(int pointer, [Matrix4? transform]) {
  // 在GestureBinding中存在一个单例PointerRouter
  // pointer: event.pointer事件指针
  // handleEvent: 回调函数,由具体的手势识别器实现,即XXXGestureRecognizer
  GestureBinding.instance!.pointerRouter.addRoute(pointer, handleEvent, transform);
  _trackedPointers.add(pointer);
  // 以pointer为key创建竞技场
  _entries[pointer] = _addPointerToArena(pointer);
}

PointerRouter#addRoute(int pointer, PointerRoute route, [Matrix4? transform])

final Map<int, Map<PointerRoute, Matrix4?>> _routeMap = <int, Map<PointerRoute, Matrix4?>>{};

// route: XXXGestureRecognizer的一个函数对象
void addRoute(int pointer, PointerRoute route, [Matrix4? transform]) {
  // 判断是否当前pointer指针是否存在,不存在则创建
  final Map<PointerRoute, Matrix4?> routes = _routeMap.putIfAbsent(
    pointer,
    () => <PointerRoute, Matrix4?>{},
  );
  routes[route] = transform;
}

这里的_routeMap我们在GestureBinding#dispatch()方法处还会遇到,等遇到时再来看具体的作用是什么。可以看下Map<PointerRoute, Matrix4?>里面存的是以入参的函数对象route为key,value对应当前事件的transform(用于不同的坐标空间转换,暂不讨论)矩阵。

OneSequenceGestureRecognizer#_addPointerToArena(int pointer)

GestureArenaEntry _addPointerToArena(int pointer) {
  if (_team != null)
    return _team!.add(pointer, this);
  // gestureArena -> GestureArenaManager
  return GestureBinding.instance!.gestureArena.add(pointer, this);
}

GestureArenaManager#add(int pointer, GestureArenaMember member)

 /// Adds a new member (e.g., gesture recognizer) to the arena.
GestureArenaEntry add(int pointer, GestureArenaMember member) {
  final _GestureArena state = _arenas.putIfAbsent(pointer, () {
    return _GestureArena();
  });
  state.add(member);
  return GestureArenaEntry._(this, pointer, member);
}

主要的作用就是将手势识别器也就是手势竞技场成员添加至对应的竞技场,以上出现了几个对象GestureArenaManager、GestureArenaEntry、GestureArenaMember、_GestureArena暂时按下不表。

3.2.5 GestureBinding#handleEvent

让我们回到3.2.1小节的代码逻辑中,回顾下程序运行至此是缘从何起:

void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
  // ... 省略 ...
  for (final HitTestEntry entry in hitTestResult.path) {
    try {
      // entry.target -> HitTestTarget
      // 循环调用每一个RenderObject的handleEvent方法
      entry.target.handleEvent(event.transformed(entry.transform), entry);
    } catch (exception, stack) {
     // ... 省略 ...
    }
  }
}

HitTestResult构建完成后会循环遍历每一个HitTestTarget的handleEvent()函数,还记得HitTestResult#_path数组中最后一个元素是什么吗?没错是WidgetFlutterBinding, 亦即会执行到*GestureBinding#handleEvent()*

GestureBinding#handleEvent(PointerEvent event, HitTestEntry entry)


@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
  // 我们在3.2.4小节有遇到PointerRouter,当时是把event.pointer和手势识别器类中的handleEvent函数保存起来
  pointerRouter.route(event);
  if (event is PointerDownEvent) {
    gestureArena.close(event.pointer);
  } else if (event is PointerUpEvent) {
    gestureArena.sweep(event.pointer);
  } else if (event is PointerSignalEvent) {
    pointerSignalResolver.resolve(event);
  }
}

PointerRouter#route(PointerEvent event)

void route(PointerEvent event) {
  final Map<PointerRoute, Matrix4?>? routes = _routeMap[event.pointer];
  if (routes != null) {
    // 跟进
    _dispatchEventToRoutes(
      event,
      routes,
      Map<PointerRoute, Matrix4?>.of(routes),
    );
  }
  // ... 省略 ...
}

void _dispatchEventToRoutes(
  PointerEvent event,
  Map<PointerRoute, Matrix4?> referenceRoutes,
  Map<PointerRoute, Matrix4?> copiedRoutes,
) {
  copiedRoutes.forEach((PointerRoute route, Matrix4? transform) {
    if (referenceRoutes.containsKey(route)) {
     // 跟进
      _dispatch(event, route, transform);
    }
  });
}

void _dispatch(PointerEvent event, PointerRoute route, Matrix4? transform) {
  try {
    event = event.transformed(transform);
    // route: 表示的是XXXGestureRecognizer中的handleEvent函数
    route(event);
  } catch (exception, stack) {
    // ... 省略 ...
  }
}

经过以上程序会执行到XXXGestureRecognizer#handleEvent()函数,让我们以开始的TapGestureRecognizer为例,其父类是PrimaryPointerGestureRecognizer

PrimaryPointerGestureRecognizer#handleEvent(PointerEvent event)

void handleEvent(PointerEvent event) {
  assert(state != GestureRecognizerState.ready);
  if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
    final bool isPreAcceptSlopPastTolerance =
        !_gestureAccepted &&
        preAcceptSlopTolerance != null &&
        _getGlobalDistance(event) > preAcceptSlopTolerance!;
    final bool isPostAcceptSlopPastTolerance =
        _gestureAccepted &&
        postAcceptSlopTolerance != null &&
        _getGlobalDistance(event) > postAcceptSlopTolerance!;
    // 判断是否滑动场景,当下场景我们暂且不关注滑动的判断
    if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
      resolve(GestureDisposition.rejected);
      stopTrackingPointer(primaryPointer!);
    } else {
      // 抽象方法,由具体子类实现BaseTapGestureRecognizer
      handlePrimaryPointer(event);
    }
  }
  stopTrackingIfPointerNoLongerDown(event);
}

BaseTapGestureRecognizer#handlePrimaryPointer(PointerEvent event)

void handlePrimaryPointer(PointerEvent event) {
  // 如果当前事件是PointerUpEvent,会执行_checkUp(),通常业务上设置的onTap回调在此触发
  if (event is PointerUpEvent) {
    _up = event;
    _checkUp();
  } else if (event is PointerCancelEvent) {
    resolve(GestureDisposition.rejected);
    if (_sentTapDown) {
      _checkCancel(event, '');
    }
    _reset();
  } else if (event.buttons != _down!.buttons) {
    resolve(GestureDisposition.rejected);
    stopTrackingPointer(primaryPointer!);
  }
}

BaseTapGestureRecognizer#_checkUp()

void _checkUp() {
  if (!_wonArenaForPrimaryPointer || _up == null) {
    return;
  }
  assert(_up!.pointer == _down!.pointer);
  // 抽象函数,由具体的子类实现TapGestureRecognizer
  handleTapUp(down: _down!, up: _up!);
  _reset();
}

TapGestureRecognizer#handleTapUp()

void handleTapUp({ required PointerDownEvent down, required PointerUpEvent up}) {
  final TapUpDetails details = TapUpDetails(
    kind: up.kind,
    globalPosition: up.position,
    localPosition: up.localPosition,
  );
  switch (down.buttons) {
    case kPrimaryButton:
      if (onTapUp != null)
        invokeCallback<void>('onTapUp', () => onTapUp!(details));
      // BD ADD: START
      if (startTapUpMonitor)
        performTapUpTimeStamp = DateTime.now().millisecondsSinceEpoch;
      // END
      if (onTap != null)
        // 触发业务上设置的onTap回调
        invokeCallback<void>('onTap', onTap!);
      break;
    // ... 省略 ...
    default:
  }
}

3.3 小结

事件分发的最开始是通过触摸的position收集符合条件的RenderObject,构造HitTestResult#_path数组,构造完成后,开始循环遍历集合中每个元素的handleEvent()函数,而让我们关注的元素大都是RenderPointerListener#handleEvent(),通过此将事件传递手势识别器XXXGestureRecognizer,将事件保存至GestureBinding的PointerRouter对象中,以event.pointer为key,XXXGestureRecognizer的handleEvent函数为value。并且将手势识别器作为竞技场成员添加至GestureBinding的GestureArenaManager对象,此对象即竞技场管理类

当遍历到HitTestResult#_path数组中最后一个元素GestureBinding时,事件也到了能真正执行的时刻,当然在此需要经过竞技场的一阵裁决,我们在本章节先不关注具体如何裁决的。通过GestureBinding的PointerRouter对象中注册的事件最终分发到对应的手势识别器handleEvent函数,最终onTap回调函数被调用。

在以上我们讨论了事件从最初到最终执行的过程,其中少了一个特别重要的概念手势竞争,例如,当一个触摸位置的position在符合很多组件的情况下,到底应该让哪个组件执行?带着这个问题,我们进入下一章节。

四、手势竞技

4.1 核心类

4.1.1 竞技成员#GestureArenaMember

// 通知竞技者在本竞技场成功或者失败
abstract class GestureArenaMember {
  /// Called when this member wins the arena for the given pointer id.
void acceptGesture(int pointer);

  /// Called when this member loses the arena for the given pointer id.
void rejectGesture(int pointer);
}

4.1.2 竞技场#_GestureArena

class _GestureArena {
  // 竞技场维护竞技成员列表
  final List<GestureArenaMember> members = <GestureArenaMember>[];
  // 表示该竞技场是否开放
  bool isOpen = true;
  // 表示该竞技场是否挂起
  bool isHeld = false;
  // 表示该竞技场是否等待清扫
  bool hasPendingSweep = false;
  // 渴望胜利的参赛者
  GestureArenaMember? eagerWinner;
}

4.1.3 竞技发送器#GestureArenaEntry

class GestureArenaEntry {
  GestureArenaEntry._(this._arena, this._pointer, this._member);
  // 手势竞技场的管理者
  final GestureArenaManager _arena;
  // 触点 id
  final int _pointer;
  // 参赛者
  final GestureArenaMember _member;
  // 通过 _arena 的 _resolve 方法进行处理当前竞技者面对当前的触点被接受或被拒绝
  void resolve(GestureDisposition disposition) {
    _arena._resolve(_pointer, _member, disposition);
  }
}

enum GestureDisposition {
  accepted, // 手势被接受
  rejected, // 手势被拒绝
}

4.1.4 竞技管理#GestureArenaManager

竞技场管理者是这体系中最复杂的一个,也是所有裁决逻辑处理的地方。他定义了很多方法,如下:

详解Flutter手势事件分发原理

其中add、close、sweep、hold是对竞技场的维护, _resove、_tryToResolve、_resolveInfavorOf是处理竞技场裁决的逻辑。

4.1.5 小结

在第三章节的3.2.4小节中我们有遇到和竞技场相关的,这里我们再详细说明一下;

GestureArenaManager#add(int pointer, GestureArenaMember member)

// key: 事件指针即 event.pointer
// value: 竞技场
final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

/// Adds a new member (e.g., gesture recognizer) to the arena.
GestureArenaEntry add(int pointer, GestureArenaMember member) {
  final _GestureArena state = _arenas.putIfAbsent(pointer, () {
    return _GestureArena();
  });
  state.add(member);
  return GestureArenaEntry._(this, pointer, member);
}

如上,一个指针事件对应一个竞技场,对应关系在GestureArenaManager_arenas保存,竞技场里面有竞技成员GestureArenaMember即手势识别器XXXGestureRecognizerGestureArenaEntry持有GestureArenaManagerGestureArenaMember

4.2 不同组件同一手势

我们以下图模型来表述一下,不同的组件当面对TabGestureRecognizer时是如何做决策的,如下假设每个组件都被GestureDetector包裹。

详解Flutter手势事件分发原理

当手指按下时即down事件,会进行事件检测构建HitTestResult,然后会遍历HitTestResult#_path中每个元素的handleEvent方法,这个时候做了两件非常重要的事:

  • 将当前的手势识别器的handleEvent方法保存至PointerRouter中
  • 将当前的触摸指针事件保存到竞技场中(注意:此时只有一个竞技场,竞技成员有三个,分别是包裹了C、B、A的TabGestureRecognizer)
void startTrackingPointer(int pointer, [Matrix4? transform]) {
  GestureBinding.instance!.pointerRouter.addRoute(pointer, handleEvent, transform);
  _trackedPointers.add(pointer);
  _entries[pointer] = _addPointerToArena(pointer);
}

GestureArenaEntry _addPointerToArena(int pointer) {
  if (_team != null)
    return _team!.add(pointer, this);
  return GestureBinding.instance!.gestureArena.add(pointer, this);
}

GestureArenaEntry add(int pointer, GestureArenaMember member) {
  final _GestureArena state = _arenas.putIfAbsent(pointer, () {
    return _GestureArena();
  });
  state.add(member);
  return GestureArenaEntry._(this, pointer, member);
}

4.2.1 GestureBinding#handleEvent

void handleEvent(PointerEvent event, HitTestEntry entry) {
  pointerRouter.route(event);
  if (event is PointerDownEvent) {
    gestureArena.close(event.pointer);
  } else if (event is PointerUpEvent) {
    gestureArena.sweep(event.pointer);
  } else if (event is PointerSignalEvent) {
    pointerSignalResolver.resolve(event);
  }
}

让我再来看看当程序执行至HitTestResult#_path最后一个元素的handleEvent函数时,pointerRouter.route会分别调用TapGestureRecognizer父类PrimaryPointerGestureRecognizer的handleEvent()函数。

PrimaryPointerGestureRecognizer#handleEvent(PointerEvent event)

void handleEvent(PointerEvent event) {
  // 判断是否移动
  if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
    resolve(GestureDisposition.rejected);
    stopTrackingPointer(primaryPointer!);
  } else {
   // 非移动事件
    handlePrimaryPointer(event);
  }
  stopTrackingIfPointerNoLongerDown(event);
}

// BaseTapGestureRecognizer#handlePrimaryPointer
// down事件在该函数不会有作为
void handlePrimaryPointer(PointerEvent event) {
  if (event is PointerUpEvent) {
    _up = event;
    _checkUp();
  } else if (event is PointerCancelEvent) {
    resolve(GestureDisposition.rejected);
    if (_sentTapDown) {
      _checkCancel(event, '');
    }
    _reset();
  } else if (event.buttons != _down!.buttons) {
    resolve(GestureDisposition.rejected);
    stopTrackingPointer(primaryPointer!);
  }
}

down事件经过PointerRoute分发后,并没有做什么实质性的工作,继续跟进

4.2.2 GestureArenaManager#close

void close(int pointer) {
  // 从竞技场map中根据pointer获取当前的竞技场
  final _GestureArena? state = _arenas[pointer];
  if (state == null)
    return; // This arena either never existed or has been resolved.
  // 关闭竞技场  
  state.isOpen = false;
  // 尝试裁决
  _tryToResolveArena(pointer, state);
}

void _tryToResolveArena(int pointer, _GestureArena state) {
  // 当竞技场只有一个成员,直接宣布胜出,此时我们是有多个以下条件也不满足此时未能裁决出谁胜利
  if (state.members.length == 1) {
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  } else if (state.members.isEmpty) {
    _arenas.remove(pointer);
  } else if (state.eagerWinner != null) {
    _resolveInFavorOf(pointer, state, state.eagerWinner!);
  }
}

当事件由down->up时,还是会经过和down事件一样的流程处理,重复的流程不再赘述,关注不一样的地方:BaseTapGestureRecognizer#handlePrimaryPointer会执行_checkUp函数

BaseTapGestureRecognizer#_checkUp()

void _checkUp() {
  // _wonArenaForPrimaryPointer只有在竞争胜利时会赋值,此时还是flase
  // 事件此刻还是没有真正的到处理
  if (!_wonArenaForPrimaryPointer || _up == null) {
    return;
  }
  handleTapUp(down: _down!, up: _up!);
  _reset();
}

4.2.3 GestureArenaManager#sweep

void sweep(int pointer) {
  final _GestureArena? state = _arenas[pointer];
  if (state == null)
    return; // This arena either never existed or has been resolved.
  if (state.isHeld) {
    state.hasPendingSweep = true;
    return; // This arena is being held for a long-lived member.
  }
  _arenas.remove(pointer);
  if (state.members.isNotEmpty) {
    // 取出竞技场中的第一个元素,获取胜利
    state.members.first.acceptGesture(pointer);
    // Give all the other members the bad news.
    for (int i = 1; i < state.members.length; i++)
      state.members[i].rejectGesture(pointer);
  }
}

BaseTapGestureRecognizer#acceptGesture(int pointer)

@override
void acceptGesture(int pointer) {
  super.acceptGesture(pointer);
  if (pointer == primaryPointer) {
    _checkDown();
    _wonArenaForPrimaryPointer = true;
    _checkUp();
  }
}

void _checkDown() {
  if (_sentTapDown) {
    return;
  }
  // 执行onTapDown回调函数
  handleTapDown(down: _down!);
  _sentTapDown = true;
}

void _checkUp() {
  if (!_wonArenaForPrimaryPointer || _up == null) {
    return;
  }
  // 执行onTap回调函数
  handleTapUp(down: _down!, up: _up!);
  _reset();
}

4.3 同一组件不同手势

这里我们分析下同一组件当设置onTap(单击)和onDoubleTap(双击)时,手势之间是如何竞争的。当使用GestureDetector设置了这两个回调函数时,会创建两个手势识别器,如下代码中:

GestureDetector#build(BuildContext context)

Widget build(BuildContext context) {
  final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

  // 单击
  if (
      onTap != null || 省略其他条件判断
  ) {
    gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
      () => TapGestureRecognizer(debugOwner: this),
      (TapGestureRecognizer instance) {
        instance
          ..onTap = onTap
          // 省略其他回调
      },
    );
  }

  // 双击
  if (onDoubleTap != null) {
    gestures[c] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
      () => DoubleTapGestureRecognizer(debugOwner: this),
      (DoubleTapGestureRecognizer instance) {
        instance
          ..onDoubleTapDown = onDoubleTapDown
          ..onDoubleTap = onDoubleTap
          ..onDoubleTapCancel = onDoubleTapCancel;
      },
    );
  }

  // ... 省略 ...
  return RawGestureDetector(
    gestures: gestures,
    behavior: behavior,
    excludeFromSemantics: excludeFromSemantics,
    child: child,
  );
}

创建了两个手势识别器:TapGestureRecognizer 和 DoubleTapGestureRecognizer。

  1. 第一触点按下时单击和双击两个竞技者分别加入到竞技场
GestureBinding.instance!.pointerRouter.addRoute(pointer, handleEvent, transform);
GestureBinding.instance!.gestureArena.add(pointer, this);
  1. 竞技场关闭gestureArena.close时,由于竞技场里面有两个竞技者,在_tryToResolveArena 暂时还无法分出胜负,所以竞技继续
  1. 第一触点抬起时,pointerRouter 会通过注入的回调来通知竞技场中的竞技者,让他们对新的 event对象进行校验

    1. 单击事件我们在上一小节中有过介绍,不再赘述
    2. 双击事件
    3. void _handleEvent(PointerEvent event) {
        final _TapTracker tracker = _trackers[event.pointer]!;
        if (event is PointerUpEvent) {
          if (_firstTap == null)
            // 保存一次点击
            _registerFirstTap(tracker);
          else
            _registerSecondTap(tracker);
        } else if (event is PointerMoveEvent) {
          if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop))
            _reject(tracker);
        } else if (event is PointerCancelEvent) {
          _reject(tracker);
        }
      }
      
      void _registerFirstTap(_TapTracker tracker) {
        _startDoubleTapTimer();
        // 将该竞技场挂起
        GestureBinding.instance!.gestureArena.hold(tracker.pointer);
        // Note, order is important below in order for the clear -> reject logic to
        // work properly.
        _freezeTracker(tracker);
        _trackers.remove(tracker.pointer);
        _clearTrackers();
        // 保存第一次的点击
        _firstTap = tracker;
      }
      
      // GestureArenaManager#hold(int pointer)
      void hold(int pointer) {
        final _GestureArena? state = _arenas[pointer];
        if (state == null)
          return; // This arena either never existed or has been resolved.
        // 挂起该竞技场  
        state.isHeld = true;
      }
      
  1. 竞技场清扫,当pointerRouter 分发完毕后,如果该触点事件是抬起类型时,竞技场管理者会执行清扫 sweep 。竞技场被挂起时,会将 hasPendingSweep 置为 true,直接返回。

    1. void sweep(int pointer) {
        final _GestureArena? state = _arenas[pointer];
        if (state == null)
          return; // This arena either never existed or has been resolved.
        // 在双击事件抬起时会将该竞技场挂起  
        if (state.isHeld) {
          state.hasPendingSweep = true;
          return; // This arena is being held for a long-lived member.
        }
        // ... 省略 ...
      }
      
  1. 胜负未分,进行第二次点击的判断,此时会再创建一个竞技场(一个event.pointer对应一个竞技场),同样的逻辑是两个竞技者分别加入到新竞技场,当第二触点抬起时,pointerRouter 通知参赛者触点的变化,单击和之前一样,没什么动作,双击会在此时执行_registerSecondTap(tracker)

    1. // DoubleTapGestureRecognizer#_registerSecondTap
      void _registerSecondTap(_TapTracker tracker) {
        // 技场管理者,将通过 _resolve 裁决,宣布双击获取胜利。其他竞技者者全部失败
        _firstTap!.entry.resolve(GestureDisposition.accepted);
        tracker.entry.resolve(GestureDisposition.accepted);
        
        _freezeTracker(tracker);
        _trackers.remove(tracker.pointer);
        _checkUp(tracker.initialButtons);
        _reset();
      }
      
  1. 那么当单击和双击手势发生竞争时,单击是何时获取成功的呢?事实上在创建双击识别器时,会有一个超时机制(300ms),当超过规定的时间时会触发_reset()方法

    1.   DoubleTapGestureRecognizer#_reset()
    2. // 
      void _reset() {
        _stopDoubleTapTimer();
        if (_firstTap != null) {
          if (_trackers.isNotEmpty)
            _checkCancel();
          final _TapTracker tracker = _firstTap!;
          _firstTap = null;
          // 将自己从竞技场移除,并通知竞技场管理者进行裁决
          _reject(tracker);
          // release时会将挂起的竞技场清扫
          GestureBinding.instance!.gestureArena.release(tracker.pointer);
        }
        _clearTrackers();
      }
      
      void release(int pointer) {
        final _GestureArena? state = _arenas[pointer];
        if (state == null)
          return; // This arena either never existed or has been resolved.
        state.isHeld = false;
        // 在第一个触点抬起时执行sweep时会将该字段设置为true,宣布首位竞技者获取胜利
        // 此时单击事件会获取胜利
        if (state.hasPendingSweep)
          sweep(pointer);
      }
      
  1. 存在问题:如果单击和双击事件同时存在时,相较于只有单击事件的情况,单击事件的反应速度会慢300ms

五、总结

以上就是Flutter手势事件分发原理,洋洋洒洒说了很多,希望对你有所帮助,有任何疑问或者问题欢迎评论。

参考:

深入进阶-从一次点击探寻Flutter事件分发原理:juejin.cn/post/689599…

Flutter手势探索:juejin.cn/book/689637…

Flutter手势探索——原理与实现的背后:www.yuque.com/xytech/flut…