Flutter事件分发和冲突处理

3,111 阅读18分钟

看完这篇文章你能学到什么

  • flutter事件流分发流程
  • flutter事件冲突怎么处理

事件处理-常用widget

  1. Listener 监听并识别最底层的down ,up,cancel,move等事件
  2. GestureDetecotor 识别事件,包括点击,长按,双击,拖动,并解决事件之间的冲突
  3. IgnorePointer 忽略事件,包括它自己
  4. AbsorbPointe 忽略它孩子的事件

事件分类

对于移动端,可以先不考虑鼠标事件或者其他的悬浮事件,那么在flutter中,事件的分类和Native是一样的,每组事件可分为down..move..up/cancel

事件总入口

//总的初始化入口
GestureBinding在初始化的时候,设置window的onPointDartPackaget回调
ui.window 
set onPointerDataPacket(PointerDataPacketCallback callback) {
  _onPointerDataPacket = callback;
  _onPointerDataPacketZone = Zone.current;
}

//framework层的初始化入口可以认为是在GestureBinding的_handlePointerDataPacket
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
  _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
  if (!locked)
    _flushPointerEventQueue();
}

事件分发概览

这里有两个比较重要的对象,HitTestResult,HitTestEntry,在flutter中,事件从顶层分发,开始命中测试流程。

什么是命中测试?

深度优先遍历整颗RenderTree,判断当前事件的落点位置是否在RenderObject中,如果在范围内,就表示命中测试通过了,那会把自己以HitTestEntry添加到HitTestResult对象中。注意这里的事件是down事件,至于为什么不是move、up、cancel事件,后面会解释。hitTest流程如下

image.png

在histTest完成后,hitTestResult应该如下图 image (1).png

事件遍历分发的入口在GesturesBinding类里

// 遍历
void _flushPointerEventQueue() {
  assert(!locked);
  while (_pendingPointerEvents.isNotEmpty)
    _handlePointerEvent(_pendingPointerEvents.removeFirst());
}

// 对每个事件做处理,注意这里走到了 hitTest(hitTestResult, event.position);
void _handlePointerEvent(PointerEvent event) {
  HitTestResult hitTestResult;
  // 这里先只看pointDown事件,其他的事件后面会讲
  if (event is PointerDownEvent || event is PointerSignalEvent) {
    hitTestResult = HitTestResult();
    // 执行测试流程,其实就是看down的落点是否在这个RenderObject的范围内
    hitTest(hitTestResult, event.position);
    if (event is PointerDownEvent) {
        // 保存结果
      _hitTests[event.pointer] = hitTestResult;
    }
  } 
  }

但是看 GestureBinding的histTest方法,这里发现,这里什么都没做啊,只做了1个add操作

/// Determine which [HitTestTarget] objects are located at a given position.
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
  result.add(HitTestEntry(this));
}

让我们把视线移到RendererBinding里,由于mixin,RendererBinding是GesturesBinding的子类,注意RendererBinding继承并实现了hitTest方法,所以会先执行这里的hitTest. (这部分原理可以参考flutter的入口初始化方法runApp(),以及mixin,这里不做更多的赘述)

RendererBinding
@override
void hitTest(HitTestResult result, Offset position) {
  assert(renderView != null);
  // renderView先执行hitTest
  renderView.hitTest(result, position: position);
  // 然后才是GesturesBinding的hitTest
  super.hitTest(result, position);
}

RenderView是RenderObject树的根,所以从这里开始,会遍历整颗RenderTree,执行histTest

RenderView的hitTest
bool hitTest(HitTestResult result, { Offset position }) {
    // 这里的child其实是个RenderBox,当child不为空,就执行child的hitTest,接着往下看
  if (child != null)
    child.hitTest(BoxHitTestResult.wrap(result), position: position);
  result.add(HitTestEntry(this));
  return true;
}

RenderBox的hitTest


bool hitTest(BoxHitTestResult result, { @required Offset position }) {
    // 事件的position必须在当前组件内
  if (_size.contains(position)) {
      // 优先判断children,再判断自己,只要有一个为true,就把自己加入到result中
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
  }
  return false;
}

注意RenderBox的hitTest里的判断顺序,是优先判断孩子再判断自己,也就是意味着如果孩子和自己都符合条件,那孩子是先加入到队列里的,这个顺序很重要,涉及到手势冲突的解决。 后面会细说

再看hitTestChildren,在RenderBox里是空实现,由子类实现,子类分两种,一种是单孩子的,一种是多孩子的,分别挑一个看下源码

@protected
bool hitTestChildren(BoxHitTestResult result, { Offset position }) => false;

这里单孩子以Padding为例,Padding对应的Render是RenderPadding,RenderPadding的父类是RenderShiftedBox,看下RenderShiftedBox的hitTestChildren方法

@override
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
  if (child != null) {
    final BoxParentData childParentData = child.parentData as BoxParentData;
    // 根据偏移量计算,实际上就是根据offset做个偏移,然后调用RenderBox里的hitTest
    return result.addWithPaintOffset(
      offset: childParentData.offset,
      position: position,
      hitTest: (BoxHitTestResult result, Offset transformed) {
        assert(transformed == position - childParentData.offset);
        // 返回是否命中
        return child.hitTest(result, position: transformed);
      },
    );
  }
  return false;
}

Padding是单child,那常用的多孩子容器Row,Column呢? Row和Column的父类都是Flex,直接看Flex对应的RenderFlex。

RenderFlex的hitTestChildren方法,接着往下看
@override
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
  return defaultHitTestChildren(result, position: position);
}

RenderFlex with了 RenderBoxContainerDefaultsMixin, RenderBoxContainerDefaultsMixin是个mixin
它的defaultHitTestChildren实现了hitTestChildren的逻辑,由于Flex里可以有多个孩子,所以
会循环hitTest,直到有一个孩子命中了
bool defaultHitTestChildren(BoxHitTestResult result, { Offset position }) {
  // The x, y parameters have the top left of the node's box as the origin.
  // lastChild是最后一个孩子
  ChildType child = lastChild;
  while (child != null) {
    final ParentDataType childParentData = child.parentData as ParentDataType;
    final bool isHit = result.addWithPaintOffset(
      offset: childParentData.offset,
      position: position,
      hitTest: (BoxHitTestResult result, Offset transformed) {
        assert(transformed == position - childParentData.offset);
        return child.hitTest(result, position: transformed);
      },
    );
    // 一旦有命中,就返回了
    if (isHit)
      return true;
      // 如果没有命中,就往前取
    child = childParentData.previousSibling;
  }
  return false;
}

当hitTest返回ture以后,就会把自己加入到HitTestResult中,后续的事件分发就是根据HitTestResult进行的

down事件的分发讲完了,那move、up、cancel事件呢? 接着看GestureBinding

void _handlePointerEvent(PointerEvent event) {
  HitTestResult hitTestResult;
  // 只有down事件才会走到hitTest
  if (event is PointerDownEvent || event is PointerSignalEvent) {
    assert(!_hitTests.containsKey(event.pointer));
    hitTestResult = HitTestResult();
    hitTest(hitTestResult, event.position);
    if (event is PointerDownEvent) {
      _hitTests[event.pointer] = hitTestResult;
    }
  } else if (event is PointerUpEvent || event is PointerCancelEvent) {
      // 如果是up or cancel,从_hitTests里移除,同时会返回当前的result,继续分发这个事件,
      // 但同时也代表这次事件流结束了
    hitTestResult = _hitTests.remove(event.pointer);
  } else if (event.down) {
      // 比如move事件的时候,手指一直是down的
    hitTestResult = _hitTests[event.pointer];
  }
  if (hitTestResult != null ||
      event is PointerHoverEvent ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) {
          //分发事件
    dispatchEvent(event, hitTestResult);
  }
}

可以看到除了down事件,其他事件只要有RenderObject命中,也就是hitTestResult不为空,就会执行事件分发

@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
  assert(!locked);
  // 没有Render命中,先不看这个
  if (hitTestResult == null) {
    try {
      pointerRouter.route(event);
    } catch (exception, stack) {    }
    return;
  }
  
  // hitTestResult不为空,循环调用处理事件,entry 里的target是HitTestTarget
  // RenderObject 默认有实现HitTestTarget,但是个空实现,只有需要处理事件的render才会实现
  for (final HitTestEntry entry in hitTestResult.path) {
    try {
      entry.target.handleEvent(event.transformed(entry.transform), entry);
    } catch (exception, stack) {
    }
  }
}

到这里我们基本上讲完了point事件的核心分发逻辑,并举例了单孩子容器Padding和多孩子容器Flex的事件分发,简单总结下:

  • 当设备触发point事件时,参与分发的binding有RenderBinding和GestureBinding,RenderView是RenderTree分发的总入口
    • 当事件为point down时,开始执行hitTest()测试,把符合条件的RenderObject加入到hitTestResult中
    • 如果是其他事件,就根据point id 取出之前的hitTestResult并分发
    • 最后就是遍历hitTestResult,执行 entry.target.handleEvent();

那疑问来了,没看到move或者cancel事件的处理,而且flutter里可以做到像Android 在父组件拦截事件吗?

答案是可以的,但只能在hitTest这一步中做,IgnorePointer和AbsorbPointer就是这个作用。但如果RenderObject已经在hitTestResult中了,就不能再拦截了,因为在分发代码中,只有遍历分发,没有任何其他处理


事件冲突处理概览

事件分发讲完了,那事件冲突如何处理呢?假设有如下代码

Scaffold(
    appBar: AppBar(
      title: Text("事件竞争"),
    ),
    body: GestureDetector(
      onTap: () {
        print('tab parent');
      },
      child: Container(
        width: 300,
        height: 300,
        color: Colors.red, //父亲是红色
        alignment: Alignment.center,
        child: GestureDetector(
          onTap: () {
            print('tab child');
          },
          child: Container(
            width: 100,
            height: 100,
            color: Colors.green, // 孩子是绿色,居中
          ),
        ),
      ),
    ));

代码运行后截图如下 image (2).png

当点击红色区域时 输出‘tab parent’,当点击绿色区域时输出‘tab child’,这个大家应该都知道,但有没有想过这是怎么做到的?内部的原理是什么?flutter又是如何处理事件冲突的?一个个来。

先说结论:

  • 点击绿色区域时‘tab child’的内部原理,上面我们有提到,在down事件触发hitTest的时候,优先对children做hitTest,然后才是对自己做hitTest,也就意味着child先被加入到hitTestResult中,那在事件竞争时,默认的是第一个加入竞技场的事件胜利,这也是flutter能做到 优先响应child事件的做法
  • 点击红色区域时,输出‘tab parent’,绿色区域的child不在点击返回,在_hitTest阶段就没有加入到hitResult中,具体分析可以看第一部分

想要了解事件竞争的底层原理,我们先要认识几个成员,这几个成员都在arena.dart

  • 手势竞技场 _GestureArena,记录当前所有的参与者
  • 手势竞技场成员 GestureArenaMember ,这是个抽象类,实际开发我们更多看到的是GestureRecognizer
  • 手势竞技场管理者 GestureArenaManager,这个是竞技场的核心管理类,可以决定哪个事件胜利或失败
  • GestureArenaEntry 这个不需要怎么关心,粘合了member和manager

再看下竞技场的实际参与者,每种手势,都有一个对应的识别者,在竞技场中,他们就是一个个成员

  • GestureArenaMember 所有竞技场参与者的父类
  • GestureRecognizer 同上,GestureArenaMemberd 子类,是个虚拟类,手势识别的基类
  • OneSequenceGestureRecognizer 主要是跟踪单个手势,点击,拖动
  • PrimaryPointerGestureRecognizer 单个手势跟踪的实现类,例如tab,相比于它的父亲,加了距离的限制(超过一定滑动距离就不能认定为点击了,这个和native一样),所以这个类是长按,单击手势识别器的父类
  • BaseTapGestureRecognizer 单击手势的基类
  • TapGestureRecognizer 单击手势识别
  • LongPressGestureRecognizer 长按
  • DragGestureRecognizer 手势拖动识别的基类
  • MultiTapGestureRecognizer 多个点的触摸,好像没怎么用到过
  • DoubleTapGestureRecognizer 双击

还记得上面有分析过hitTest的过程,最终GestureBingding都会在最后加入到HitTestResult中,在dispatchEvent的时候,都会调用

entry.target.handleEvent(event.transformed(entry.transform), entry);

而GestureBingdin本身也是HitTestTarget,所以我们看GestureBinding的handleEvent方法

@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
    // 这里是在分发event,但暂时先不看这里,后面会详细说到
  pointerRouter.route(event);
  // down事件的时候,竞技场关闭了事件注册
  if (event is PointerDownEvent) {
    gestureArena.close(event.pointer);
  } else if (event is PointerUpEvent) {
  // up事件的时候,竞技场开始清扫
    gestureArena.sweep(event.pointer);
  } else if (event is PointerSignalEvent) {
      //离散事件,这个先不管
    pointerSignalResolver.resolve(event);
  }
}

接着看GestureArena.close方法

为什么是down事件关闭了注册呢?
注意看方法的注释,阻止新的成员进入竞技场,调用时机在分发完down事件之后,还记得事件分发的流程吗?
RenderView作为根节点最先开始分发,在整个RenderTree分发完之后,才会走到GestureBinding的分发,所以
这里的分发是最后执行的,在这里调用gestureArena.close刚刚好
/// Prevents new members from entering the arena.
///
/// Called after the framework has finished dispatching the pointer down event.
void close(int pointer) {
    // 根据事件id获取当前竞技场
  final _GestureArena state = _arenas[pointer];
  if (state == null)
    return; // This arena either never existed or has been resolved.
    // 标记为false,不再接受新成员了
  state.isOpen = false;
  assert(_debugLogDiagnostic(pointer, 'Closing', state));
  // 查看有没有胜利者
  _tryToResolveArena(pointer, state);
}


void _tryToResolveArena(int pointer, _GestureArena state) {
  assert(_arenas[pointer] == state);
  assert(!state.isOpen);
  if (state.members.length == 1) {
      // 如果只有一个竞争者,那直接宣布为胜利者,后续的move,up事件都不用走了
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  } else if (state.members.isEmpty) {
      // 如果没有竞争者,那直接remove
    _arenas.remove(pointer);
    assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
  } else if (state.eagerWinner != null) {
      // 是否有激进的竞争者? 这个怎么来的,后续看
    assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
    _resolveInFavorOf(pointer, state, state.eagerWinner);
  }
}

// 这个方法的作用就是宣布第一个竞争者胜利
void _resolveByDefault(int pointer, _GestureArena state) {
    //还是要检查下竞技场是不是空了
  if (!_arenas.containsKey(pointer))
    return; // Already resolved earlier.
  final List<GestureArenaMember> members = state.members;
  // 移除,都胜利了,就不要存在了
  _arenas.remove(pointer);
  state.members.first.acceptGesture(pointer);
}

// 宣布指定的竞争者胜利
void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
  _arenas.remove(pointer);
  // 遍历,除了指定的竞技者外,其他都是失败者,就跟追妹子一样 哈哈哈
  for (final GestureArenaMember rejectedMember in state.members) {
    if (rejectedMember != member)
      rejectedMember.rejectGesture(pointer);
  }
  // 宣告这个竞技者胜利
  member.acceptGesture(pointer);
}

再看GestureArena sweep 清扫方法

void sweep(int pointer) {
  final _GestureArena state = _arenas[pointer];
  if (state == null)
    return; // This arena either never existed or has been resolved.
    // 竞技场是可以被延迟的,如果isHeld设置为true,那就延迟清扫,直到管理者调用release
  if (state.isHeld) {
    state.hasPendingSweep = true;
    return; // This arena is being held for a long-lived member.
  }
  // 默认情况下都是走到这里,清扫掉这个事件
  _arenas.remove(pointer);
  
  // 竞技场成员不为空,默认宣布第一个为胜利者
  if (state.members.isNotEmpty) {
    // First member wins. 宣布第一个为胜利者
    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);
  }
}

看到这里,GestureBinding关于事件处理的逻辑已经差不多讲完了,代码并不多,可以看到GestureBinding只处理了down和up事件,donw-> 报名,up->清扫,那move事件呢? 怎么任意让一个事件胜利,让一个事件失败? 失败了还能强制胜利吗? 接着往下看

先说结论

  • Listener只是事件的接收者,接受并转发基本的down,up,move等事件,不负责处理事件冲突,也不识别点击,滑动,缩放手势,处理以上这些事件还得看GestureDetector,这是flutter内部封装好的类
  • GestureDetector内部封装了各种手势的识别器,当调用方注册了某个类型的回调时,比如onTap,那GestureDetector就会让这个类型的识别器参与竞技场的竞争
  • 在后面的分析中会看到,move事件是竞技者自己处理的,down和up也会处理,这里回答了第一部分的问题
  • 可以让一个事件马上胜利,也可以让事件失败
  • 失败了还能再宣告胜利吗? 可以的!

接着看GestureDetector源码,在文件gesture_detector.dart里,这文件比较长,有1253行,只挑重点看, GestureDetector是个StatelessWidget,所以直接看build方法

  • GestureRecognizerFactoryWithHandlers,注册了各种Recognizer(竞争者),比如TapGestureRecognizer,VerticalDragGestureRecognizer等等,具体待会儿一起看下源码
  • RawGestureDetector ,GestureDetector的实现类,底层还是Listener,这个很重要

以TapGestureRecognizer为例,详细看下事件分发,响应的流程

  • 当手指触发down事件
    • 走到GestureBinding的_handlePointerEvent,获得当前renderTree的HitTestResult,具体原理可以看第一部分
    • 接着走到GestureBinding的dispatchEvent
for (final HitTestEntry entry in hitTestResult.path) {
    entry.target.handleEvent(event.transformed(entry.transform), entry);
}

这里要注意,RenderTree里的target的handleEvent先被执行,然后才执行GestureBinding里的handleEvent,看看RawGestureDetector里接收了哪些事件

@override
Widget build(BuildContext context) {
  Widget result = Listener(
    onPointerDown: _handlePointerDown,
    behavior: widget.behavior ?? _defaultBehavior,
    child: widget.child,
  );
  // 省略部分代码
  return result;
}

可以看到RawGestureDetector只接受了down事件,move和up事件稍后解释,继续看_handlePointerDown方法

// RawGestureDetectorState
void _handlePointerDown(PointerDownEvent event) {
  for (final GestureRecognizer recognizer in _recognizers.values)
    recognizer.addPointer(event);
}

//GestureRecognizer  接着看addPointer
void addPointer(PointerDownEvent event) {
  _pointerToKind[event.pointer] = event.kind;
  //isPointerAllowed比较简单,跳过了
  // 正常情况都会走到addAllowedPointer 以TapGestureRecognizer为例看下实现
  if (isPointerAllowed(event)) {
    addAllowedPointer(event);
  } else {
    handleNonAllowedPointer(event);
  }
}


//GestureRecognizer addAllowedPointer 
// 是个空实现 ,看子类
void addAllowedPointer(PointerDownEvent event) { }

// PrimaryPointerGestureRecognizer  addAllowedPointer
@override
void addAllowedPointer(PointerDownEvent event) {
    // 这个方法里注册了事件回调,
  startTrackingPointer(event.pointer, event.transform);
  if (state == GestureRecognizerState.ready) {
    state = GestureRecognizerState.possible;
    primaryPointer = event.pointer;
    initialPosition = OffsetPair(local: event.localPosition, global: event.position);
    if (deadline != null)
      _timer = Timer(deadline, () => didExceedDeadlineWithEvent(event));
  }
}

在startTrackingPointer里注册了事件的回调

// OneSequenceGestureRecognizer
@protected
void startTrackingPointer(int pointer, [Matrix4 transform]) {
    //往 GestureBinding的单例注册了回调,每次有事件分发时,都会走到 第二个入参handleEvent
   //这里是不是很熟悉,还记得GestureBinding的handleEvent方法吗。第一句代码就是对pointerRouter的分发
  GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
  _trackedPointers.add(pointer);
  assert(!_entries.containsValue(pointer));
  _entries[pointer] = _addPointerToArena(pointer);
}

// 子类里自己实现
/// Called when a pointer event is routed to this recognizer.
@protected
void handleEvent(PointerEvent event);

handleEvent在子类PrimaryPointerGestureRecognizer的实现

@override
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;
        
        // 因为识别的是单击事件,所以如果是move,且move的距离超过最小距离,直接判定失败
    if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
      resolve(GestureDisposition.rejected);
      stopTrackingPointer(primaryPointer);
    } else {
        // 通过判定
      handlePrimaryPointer(event);
    }
  }
  stopTrackingIfPointerNoLongerDown(event);
}

// 这里是个空实现,再看子类 BaseTapGestureRecognizer
/// Override to provide behavior for the primary pointer when the gesture is still possible.
@protected
void handlePrimaryPointer(PointerEvent event);

BaseTapGestureRecognizer里handlePrimaryPointer的实现

@override
void handlePrimaryPointer(PointerEvent event) {
    // 如果是up事件
  if (event is PointerUpEvent) {
    _up = event;
    // 走到这里
    _checkUp();
  } else if (event is PointerCancelEvent) {
      // cancel事件,判定竞争失败
    resolve(GestureDisposition.rejected);
    if (_sentTapDown) {
      _checkCancel(event, '');
    }
    _reset();
  } else if (event.buttons != _down.buttons) {
      // 如果和down记录的button不相等,直接判定失败
    resolve(GestureDisposition.rejected);
    stopTrackingPointer(primaryPointer);
  }
}

void _checkUp() {
    // 如果没有宣布过胜利 或者 _up为空 直接返回
  if (!_wonArenaForPrimaryPointer || _up == null) {
    return;
  }
  handleTapUp(down: _down, up: _up);
  _reset();
}

down事件看完了,总结下流程

  • hitTest先命中HitResult,然后分发
  • RawGestureDetector里的Listener第一个分发到事件,执行_handlePointerDown 回调GestureBinding单例里的pointerRouter注册回调,并调用_addPointerToArena把自己加入的手势竞技场

因为是看单击事件识别器TapGestureRecognizer,所以略过move事件,直接看up事件 up事件还是先走到GestureBinding里的分发,handleEvent

@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
    // 在down事件里注册了回调,最终会回调到OneSequenceGestureRecognizer的handleEvent
    // 所以会先执行BaseTapGestureRecognizer里的handlePrimaryPointer-> _checkUp(),但此时
    // _wonArenaForPrimaryPointer还是false,所以执行了个寂寞,并没有竞争胜利,返回了
    // 接着up事件走到竞技场的清扫方法
  pointerRouter.route(event);
  if (event is PointerDownEvent) {
    gestureArena.close(event.pointer);
  } else if (event is PointerUpEvent) {
      // 紧接着走到这里,竞技场开始清扫了,还记得前面说的吗? 没有hold事件,那默认会宣布
      // 第一个成员胜利,那就走到GestureArenaMember的acceptGesture
    gestureArena.sweep(event.pointer);
  } else if (event is PointerSignalEvent) {
    pointerSignalResolver.resolve(event);
  }
}

BaseTapGestureRecognizer是GestureArenaMember的实现类

// 竞争胜利,妹子终于追到手了
@override
void acceptGesture(int pointer) {
  super.acceptGesture(pointer);
  if (pointer == primaryPointer) {
      // 回调了GesturetureDetictord onTapDown()
    _checkDown();
     // 标记胜利啦
    _wonArenaForPrimaryPointer = true;
    // 回调了GesturetureDetictord onTapUp()
    // 回调了GesturetureDetictord onTap()
    // 重置状态,回调结束
    _checkUp();
  }
}

总结下up事件的流程

  • 执行分发,先执行route的分发,如果这个竞争者是个激进者,比如在down事件就宣告了胜利,那 此时就会直接回调,onTapUp(),onTap()了,事件结束
  • 竞技场执行清扫,宣布第一个竞争者胜利,执行onTapDown(),onTapUp(),onTap(),GestureRecognizer状态重置,事件结束

down和up讲完了,以单击事件为例,事件最终是竞技场管理GestureArenaManager宣告胜利的,那有没有可能竞争者GestureRecognizer自己宣告胜利呢?答案是可以的。

假设有这个场景,一个竖向的ListView, 它的item设置了单机事件,那假设用TapGestureRecognizer和VerticalDragGestureRecognizer,一个是单击手势识别,一个是竖向移动手势识别,理论上应该是down-> move-> move,move超过一定距离VerticalDragGestureRecognizer就宣布胜利了,TapGestureRecognizer竞争失败,item此时不响应点击事件,来看看VerticalDragGestureRecognizer是怎么宣布胜利的

先看下继承关系

  • GestureRecognizer

    • OneSequenceGestureRecognizer
      • DragGestureRecognizer
        • VerticalDragGestureRecognizer 竖向拖动事件识别
        • HorizontalDragGestureRecognizer 横向拖动事件识别
        • PanGestureRecognizer 竖向和横向拖动事件识别

VerticalDragGestureRecognizer里没什么代码,直接看DragGestureRecognizer

//DragGestureRecognizer handleEvent
@override
void handleEvent(PointerEvent event) {
  assert(_state != _DragState.ready);
    // 省略部分代码
    // 处理move事件
  if (event is PointerMoveEvent) {
    if (_state == _DragState.accepted) {
       // 省略部分代码,这里是通知监听方更新事件位置,说明之前就已经胜利了
    } else {
      _pendingDragOffset += OffsetPair(local: event.localDelta, global: event.delta);
      _lastPendingEventTimestamp = event.timeStamp;
      _lastTransform = event.transform;
      final Offset movedLocally = _getDeltaForDetails(event.localDelta);
      final Matrix4 localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform);
      _globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
        transform: localToGlobalTransform,
        untransformedDelta: movedLocally,
        untransformedEndPosition: event.localPosition,
      ).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
      if (_hasSufficientGlobalDistanceToAccept)
          // 宣告胜利 入参状态是GestureDisposition.accepted
          // 接着看OneSequenceGestureRecognizer的resolve
        resolve(GestureDisposition.accepted);
    }
  }
}

// VerticalDragGestureRecognizer 判断移动距离大于最小距离
@override
bool get _hasSufficientGlobalDistanceToAccept => _globalDistanceMoved.abs() > kTouchSlop;

OneSequenceGestureRecognizer的resolve

/// Resolves this recognizer's participation in each gesture arena with the
/// given disposition.
@protected
@mustCallSuper
void resolve(GestureDisposition disposition) {
  final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values);
  _entries.clear();
  // 遍历所有竞技场,宣告我胜利了
  for (final GestureArenaEntry entry in localEntries)
      //  调用GestureArenaEntry的resolve
    entry.resolve(disposition);
}

// GestureArenaEntry 
class GestureArenaEntry {
  // 看这里 ,竞技场
  void resolve(GestureDisposition disposition) {
      // 走到竞技场管理者的_resolve
    _arena._resolve(_pointer, _member, disposition);
  }
}

GestureArenaManager的_resolve


// GestureArenaManager的_resolve
void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
  final _GestureArena state = _arenas[pointer];
  // 如果没有竞技场了,返回
  if (state == null)
    return; // This arena has already resolved.
    // 如果是宣告失败
  if (disposition == GestureDisposition.rejected) {
    state.members.remove(member);
    // 通知竞争者 你失败了
    member.rejectGesture(pointer);
    
    // 这里重新走了一遍_tryToResolveArena,应该是当前竞争者失败了,有可能只剩下
    // 一个竞争者了,那就直接宣告那个竞争者胜利
    if (!state.isOpen)
      _tryToResolveArena(pointer, state);
  } else {
   // 如果是宣告胜利
    if (state.isOpen) {
        // 赋值激进者,这里有可能在down的时候就被调用了,move的时候状态肯定是关闭的
      state.eagerWinner ??= member;
    } else {
        // 宣告自己胜利,其他都是渣渣,你们失败了
      _resolveInFavorOf(pointer, state, member);
    }
  }
}

至此,VerticalDragGestureRecognizer的竞争机制已经讲完了,可以看到对于VerticalDragGestureRecognizer来说,在move阶段就宣告胜利了,同时也通知TapGestureRecognizer失败。

后续的move和up事件,交给胜利者处理,对于TapGestureRecognizer来说,注意还是能收到handlevent的分发的,但是内部的state已经变成GestureRecognizerState.defunct(已废止,失败者,没办法),不会再执行handleEvent后续的代码了

那TapGestureRecognizer失败了还能再次宣告胜利吗?(追妹子失败了还能再追吗) 答案是可以的。看代码

//很简单,集成TapGestureRecognizer,重写rejectGesture,大意是
//拒绝了我?不行,我自己同意一遍...
class TapMultipleGestureRecognizer extends TapGestureRecognizer {
  @override
  void rejectGesture(int pointer) {
      // 失败不可怕,我自己宣告胜利
    acceptGesture(pointer);
  }
}

// 然后把TapAgainGestureRecognizer赋值给RawGestureDetector
RawGestureDetector(
  child: your child,
  gestures: {
    TapAgainGestureRecognizer: GestureRecognizerFactoryWithHandlers<
        TapAgainGestureRecognizer>(
            () => TapAgainGestureRecognizer(),
            (TapAgainGestureRecognizer instance) {
          instance.onTap = () {
              // 失败后宣告胜利的回调
          };
          instance.onTapDown = (_) {
          };
          instance.onTapUp = (_) {
          };
        })
  },
);

至此,事件分发、处理流程都讲完了,整体上讲,比Android要简单的多,而且flutter源码能直接调试!

简单总结下

  1. 通过hitTest确定哪些Render需要消费这次的事件流,histTest只有down事件的时候才会执行
  2. 事件分发,dispatchEvent,先到RenderTree里的HitTestTarget->handleEvent 也就是Listener。
  3. 对于down事件的分发,GestureRecognizer竞争者们都为自己设置了事件的回调,并报名竞技场,方便后续竞争(做好了准备追妹子),在报名完毕后,GestureBinding关闭了竞技场的报名
  4. 对于move事件,个别性子急的比如drag事件竞争者就可以宣告胜利了,当然急切者的在down事件的时候就可以
  5. 对于up 和 cancel事件,只有胜利者才能享用up,失败者是没机会的,只能享用cancel事件
  6. 竞争失败还是能再次胜利的,可以自我宣布胜利
  7. 竞争失败并不会一定直接从竞技场移除,例如drage在move事件就宣布了胜利,但tap还是能收到后续事件的,只不过都不处理了 原文链接 HitTestBehavior分析