美文网首页
Flutter事件分发和冲突处理

Flutter事件分发和冲突处理

作者: 余瑜雨鱼 | 来源:发表于2020-11-15 23:06 被阅读0次

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

    • 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还是能收到后续事件的,只不过都不处理了

    相关文章

      网友评论

          本文标题:Flutter事件分发和冲突处理

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