Flutter的触摸事件

作者: NightRainBreeze | 来源:发表于2021-07-13 20:58 被阅读0次

    一次触摸事件从Down开始Up结束, 而Flutter是一个跨平台的UI框架, 那么Flutter是如何收集不同平台传递的触摸事件?

    我们以Android为例深入探索Flutter对触摸事件的处理.

    点击事件响应流程

    首先一个点击事件出发点肯定是从Native发出的, 然后经过多次转换传到Flutter层.

    图片来自网络

    总结来说

    Native -> C++

    Android onTouchEvent 接收到触摸事件, 通过 Flutter.JNI 传递到 C++ 层.

    C++ -> Dart

    C++ 通过 RuntimeControll 控制器 调用 Window.DispatchPointerDataPacker 将窗体坐标点位传递到Dart层.

    Dart -> 逻辑像素

    Dart层通过FlutterWindow与Native交互, 通过GestureBinding#window.onPointDataPacket初始化, 开始进行手势处理.

    Flutter处理点击事件流程

    • 收集所有的触摸事件转换为逻辑像素.
    • 对收集的点击事件进行命中测试, 得到一个新的集合HitTestResult.
    • HitTestResult收集的Widget进行触摸事件分发, 最后处理事件的是GestureBinding, 宣告一次点击事件结束, 并将都能够响应触摸事件的Widget进行手势竞争, 胜出者接受手势, 其他接受拒绝手势.

    触摸事件收集与分发

    原始数据转换为逻辑像素

    由Native传来的原始触摸事件会把由GestureBinding#_handlePointerDataPacket方法转换为对应的逻辑像素.

      // 需要处理的队列
      final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
    
      void _handlePointerDataPacket(ui.PointerDataPacket packet) {
        // We convert pointer data to logical pixels so that e.g. the touch slop can be
        // defined in a device-independent manner.
        // 将指针数据转化为逻辑像素
        _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
        if (!locked)
          _flushPointerEventQueue();
      }
    

    触摸事件处理

    GestureBinding#_handlePointerEvent方法进行触摸事件的命中测试和分发.

        // 记录所有的Down点击事件, 不会跟踪悬停状态的事件, 因为需要对每一帧进行命中测试
        // key是event.pointer, 它不会重复, 每个Down事件的时候pointer会+1
        final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};
    
        void _handlePointerEvent(PointerEvent event) {
          HitTestResult? hitTestResult;
          if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
            // Dwon事件触发
            assert(!_hitTests.containsKey(event.pointer));
            hitTestResult = HitTestResult();
            //进行命中测试
            hitTest(hitTestResult, event.position);
            if (event is PointerDownEvent) {
              // 添加到 _hitTests 集合
              _hitTests[event.pointer] = hitTestResult;
            }
          } else if (event is PointerUpEvent || event is PointerCancelEvent) {
            // up 事件之后, 手势结束, 所以移除
            hitTestResult = _hitTests.remove(event.pointer);
          } else if (event.down) {
            // 移动的过程, 根据event.pointer, 进行重新赋值
            hitTestResult = _hitTests[event.pointer];
          }
          if (hitTestResult != null ||
              event is PointerAddedEvent ||
              event is PointerRemovedEvent) {
            assert(event.position != null);
            // 事件分发
            dispatchEvent(event, hitTestResult);
          }
        }
    

    一次点击事件由Down+Up组成, 经过Dart代码将屏幕实际位置转换为物理位置, 然后加入队列, 对Dwon事件进行hitTest(命中测试)

    hitTest 对响应触摸事件的Widget进行汇总

    hitTest 会从根Widget进行深度优先遍历, 把所有能够响应触摸事件的Widget添加到队列.

    也就是说最后添加到Widget树的Widget会优先添加到队列进行命中测试.

    命中测试就是判断触摸点位是否在Widget的Layout范围内(包顶部和左侧边缘, 不包底部和右侧边缘)

    最终HitTestResult汇总了所有能够响应点击事件的Widget集合, 而需要注意的是, GestureBinding方法最终会把自己添加到队列末尾.

    • GestureBinding#hitTest
      // GestureBinding 会把自己添加到队列末尾
      @override // from HitTestable
        void hitTest(HitTestResult result, Offset position) {
        result.add(HitTestEntry(this));
      }
    
    • RendererBinding#hitTest
      /// 负责绘制渲染的 Root 节点
      RenderView get renderView => _pipelineOwner.rootNode! as RenderView;
      ///绘制树的owner,负责绘制,布局,合成
      PipelineOwner get pipelineOwner => _pipelineOwner;
    
      // RendererBinding 对 hitTest进行了重写
      @override
      void hitTest(HitTestResult result, Offset position) {
        assert(renderView != null);
        assert(result != null);
        assert(position != null);
        // 从根节点进行遍历
        // 调用 -> View#hitTest
        renderView.hitTest(result, position: position);
        super.hitTest(result, position);
      }
    
    • View#hitTest
      bool hitTest(HitTestResult result, { required Offset position }) {
        // 深度优先遍历
        // 调用 -> RenderBox#hitTest
        if (child != null)
          child!.hitTest(BoxHitTestResult.wrap(result), position: position);
        result.add(HitTestEntry(this));
        return true;
      }
    
    • RenderBox#hitTest
      // 如果接受的触摸事件在Widget树中, 则返回 true
      // position 是被转换之后的相对坐标
      bool hitTest(BoxHitTestResult result, { required Offset position }) {
        // 判断触摸点位是否在Widget的上下左右范围内(包顶部和左侧边缘, 不包底部和右侧边缘)
        if (_size!.contains(position)) {
          // true: 当这个Widget的孩子或者自己包含触摸点, 则把添加这个绘制对象到hitResult中, 这样就说明当前Widget已经响应了触摸事件.
          if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
            result.add(BoxHitTestEntry(this, position));
            return true;
          }
        }
        // false: 交给下一个Widget处理
        return false;
      }
    

    手势分发

    GestureBinding#_handlePointerEvent方法最后进行事件分发.

        void _handlePointerEvent(PointerEvent event) {
          // 深度遍历收集所有命中测试的Widget.
          // ......
          if (hitTestResult != null || event is PointerAddedEvent || event is PointerRemovedEvent) {
            assert(event.position != null);
            // 对收集的Widget进行事件分发, Widget树最深的, 即UI最上层的Widget最先响应.
            dispatchEvent(event, hitTestResult);
          }
        }
    
        // 事件将发送到[HitTestResult]集合中的每个[HitTestTarget]handleEvent方法, 其中处理程序的所有异常都会被捕获
        @override // from HitTestDispatcher
        void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
          for (final HitTestEntry entry in hitTestResult.path) {
            entry.target.handleEvent(event.transformed(entry.transform), entry);
          }
        }
    

    总结

    触摸事件主要的处理类是GestureBinding, 它会对Widget树进行深度遍历, 并对每一个Widget进行测试命中, 测试命中的判断依据是该触摸事件的坐标是否在WidgetLayout范围内, HitTestResult负责收集所有测试命中的Widget, 并对其进行事件分发dispatchEvent.

    手势冲突

    上面介绍了, GestureBinding#_handlePointerEvent方法会对测试命中的Widget进行事件分发, 而很显然并不是测试命中的Widget就可以响应并处理触摸事件, 那么什么样的Widget会收到Down事件并且能够进行后续处理呢?

    Down 事件进一步收集

    以InkWell为例, 我们查看Down事件是如何被收集的.

    // InkWell继承了InkResponse, 那么查看InkResponse的实现
    class InkWell extends InkResponse {}
    
    // InkResponse是一个StatelessWidget, 那么查看build方法
    class InkResponse extends StatelessWidget {
        Widget build() {
          return
            // 其他 ...
            // 最后嵌套了GestureDetector
            GestureDetector();
        }
    }
    // 继续查看build方法
    class GestureDetector extends StatelessWidget {
        Widget build() {
          return RawGestureDetector();
        }
    }
    // 继续查看build方法
    class RawGestureDetector extends StatefulWidget {
        Widget build() {
          return Listener();
        }
    }
    // Listener最后嵌套的了RenderPointerListener
    class Listener extends SingleChildRenderObjectWidget {
      RenderPointerListener createRenderObject(BuildContext context) {
        return RenderPointerListener();
      }
    }
    // 最终发现 RenderPointerListener 间接实现了RenderObject, 而RenderObject实现了 HitTestTarget
    // 还记得 GestureBinding#_handlePointerEvent 会遍历调用 HitTestTarget#handleEvent 方法吗
    class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
    
      // event 是 经过RawGestureDetector实现了Down事件传递过来的
      @override
      void handleEvent(PointerEvent event, HitTestEntry 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);
      }
    }
    
    // RawGestureDetector最终嵌套了RenderPointerListener, 而RenderPointerListener被HitTestResult#_handlePointerDown方法回调.
    // 而在回调RawGestureDetector#_handlePointerDown方法中会把该触摸事件添加到手势竞争场, 并进行手势竞争.
    void _handlePointerDown(PointerDownEvent event) {
      // 添加event到手势竞技场
      for (final GestureRecognizer recognizer in _recognizers!.values)
        recognizer.addPointer(event);
    }
    
    // GestureRecognizer#addPointer
    void addPointer(PointerDownEvent event) {
      // GestureRecognizer 负责将自身添加到全局指针路由器, 以接收此指针的后续事件,
      // 并将Pointer添加到GestureArenaManager, 以跟踪该指针。
      addAllowedPointer(event);
      //...
    }
    
    // 最终, 重写addAllowedPointer的控件会被添加到手势竞技场中.
    GestureBinding.instance!.gestureArena.add(event.pointer, this),
    

    手势冲突

    我们通过Down事件进一步收集发现, 并不是所有的Widget都会响应触摸事件, 而是通过RenderPointerListener进行不同的触摸事件识别并回调到RawGestureDetector.

    如果在页面中存在多个RawGestureDetector就会有触摸事件冲突问题, 具体由哪个Widget接受触摸事件? 每一个RawGestureDetector都响应了触摸事件怎么处理? 这就需要了解手势的竞争留存问题.

    TODO 手势竞技场

    相关文章

      网友评论

        本文标题:Flutter的触摸事件

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