Flutter的滑动原理

本文深入探讨了Flutter的滑动原理,从PageView的核心结构、事件分发流程到滑动冲突的解决。分析了手势事件从Native层传递到Dart层,通过HitTest收集响应控件,再由GestureArenaManager进行手势竞争。在滑动事件中,Down事件用于收集竞争者,Up事件决定胜利者。在ListView的滑动事件中,Move事件触发滑动行为,通过ScrollNotification进行滑动冲突的处理。文章还介绍了自定义ScrollPosition和监听ScrollNotification解决滑动冲突的方法,帮助读者理解Flutter手势处理的底层机制。
摘要由CSDN通过智能技术生成

1 Flutter 的滑动原理

1.1 PageView的核心结构

(一) PageView
PageView就是基于Scrollable进行了定制,通过封装Notification获取到ScrollNotification类的通知,根据通知信息里的偏移判断当前页面是否发生了切换,然后回调onPageChanged
(二)RawGestureDetector
手势收集类,在ScrollablesetCanDrag方法中,绑定了VerticalDragGestureRecognizer或者HorizontalDragGestureRecognizer用来收集垂直或水平方向的滑动信息。
(三) ScrollController与ScrollPosition
ScrollPositionScrollable中实际控制滑动的对象,在ScrollControllerattach方法中。ScrollPosition会将ScrollController作为它的观察者添加到Listeners中,我们往往使用ScrollController.addListener方法添加滚动监听,实际上的通知顺序是:ScrollPosition->ScrollController->添加的回调
(四) Viewport
接受来自ScrollPosition的偏移量,绘制不同的区域完成"滑动"。

1.2基本流程分析

当手指在屏幕上滑动的时候,首先RawGestureDetector收集了手势信息(里面处理了手势竞争的流程,下期展开叙述),之后将手势信息回调到Scrollable中。Scrollable接受到信息后,通过ScrollPosition进行滑动控制包含(1)修改偏移量,通知Viewport绘制不同的区域 (2)通知ScrollController,进行观察者的通知。

2 从一次点击探寻Flutter的事件分发原理

2.1 触摸事件传递

在这里插入图片描述

首先我们知道,用户的任何交互行为,一定是在原生设备上进行。所以我们事件分发肯定是从Native侧传递到Flutter,下面一张图描述了这个过程
在这里插入图片描述
一次点击响应可以分解为两个事件,一个手指按下的Down事件,一个手指抬起的Up事件。以安卓为例,首先这两个事件依次从Java层传递到了C++,最终传递至Dart。在Dart部分,我们注意到经过zone.runUnaryGuarded方法之后会调用到window.onPointDataPacket方法处(window是Flutter中一个非常核心的概念,是作为一个与Native交互的源对象,后面单独介绍),查看GestureBinding初始化的过程得知这个方法会执行_handlePointerDataPacket

mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
   
  @override
  void initInstances() {
   
    super.initInstances();
    _instance = this;
   //将_handlePointerDataPacket设置为 window.onPointerDataPacket回调
   window.onPointerDataPacket = _handlePointerDataPacket;
  }
}

注:mixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法、变量而不必成为其子类。该方法中表示GestureBinding继承BindingBase(抽象类)。

GestureBinding# _handlePointerDataPacket(ui.PointerDataPacket packet)

//未处理的事件队列
final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
//这里的packet是一个点的信息
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.
  // 将data中的数据,映射到为逻辑像素 
  // window.devicePixelRatio 一个逻辑像素对应的设备像素,比如nex6:3.5
  _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
  if (!locked)
    _flushPointerEventQueue();
}

这个方法首先会根据设备的属性将传递来数据映射到为逻辑像素后添加至队列,下一步调用_flushPointerEventQueue()
GestureBinding# _flushPointerEventQueue()

void _flushPointerEventQueue() {
   
  assert(!locked);
  while (_pendingPointerEvents.isNotEmpty)
    //直接调用_handlePointerEvent
    _handlePointerEvent(_pendingPointerEvents.removeFirst());
}

GestureBinding# _handlePointerEvent(PointerEvent event)
/// 此处的key是event.pointer,pointer是不会重复的,每个down事件的时候会去+1

final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{
   };

void _handlePointerEvent(PointerEvent event) {
   
  HitTestResult hitTestResult;
  if (event is PointerDownEvent || event is PointerSignalEvent) {
   
    //down事件进行hitTest
    hitTestResult = HitTestResult();
    hitTest(hitTestResult, event.position);
    if (event is PointerDownEvent) {
   
      // dowmn事件的话对这个hitTest集合赋值
      _hitTests[event.pointer] = hitTestResult;
    }
  } else if (event is PointerUpEvent || event is PointerCancelEvent) {
   
    // up事件标识这次操作已经结束,所以移除
    hitTestResult = _hitTests.remove(event.pointer);
  } else if (event.down) {
   
    // move事件也应该被分发在down事件初始点击的区域  比如点击了列表中的A item这个时候开始滑动,那处理这个事件的始终只是列表和A item, 只是如果滑动的话事件是由列表进行处理
    hitTestResult = _hitTests[event.pointer];
  }
  if (hitTestResult != null ||
      event is PointerHoverEvent ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) {
   
    dispatchEvent(event, hitTestResult);
  }
}

调用最终走到_handlePointerEvent(PointerEvent event),因为点击事件肯定是从Down事件开始,在PointerDownEvent的流程中先声明了一个HitTestResult()对象,之后调用 hitTest(hitTestResult, event.position)

2.2 HitTest收集响应控件与分发

///renderview:负责绘制的root节点
RenderView get renderView => _pipelineOwner.rootNode;
///绘制树的owner,负责绘制,布局,合成
PipelineOwner get pipelineOwner => _pipelineOwner;
@override
void hitTest(HitTestResult result, Offset position) {
   
  assert(renderView != null);
  renderView.hitTest(result, position: position);
  super.hitTest(result, position);
  =>
  GestureBinding#hitTest(HitTestResult result, Offset position) {
   
    result.add(HitTestEntry(this));
  }
}

这个hitTest方法被RendererBinding重写,里面调用了renderView的hitTest(result, position: position)renderView是绘制树的根节点,是所有Widget的祖先

RenderView#hitTest(BoxHitTestResult result, { @required Offset position })

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

RenderBox#hitTest(BoxHitTestResult result, { @required Offset position })

///作用:给出指定position的所有绘制控件
///返回true,当这个控件或者他的子控件位于给定的position的时候,添加这个绘制的对象到给定的hitResult中 这样标志当前的控件已经吸收了这个点击事件,其他控件不响应
///返回false,表示这个事件交给在当前对象之后的控件处理,
///例如一个row里面,多个区域可以响应点击,只要如果第一块能响应点击的话,那后续就不用判断是否能响应了
///调用方需要将全局的坐标转换为RenderBox关联的坐标,Renderbox负责判断这个坐标是否包含在当前的范围里
///这个方法依赖于最新的layout而不是paint,因为判断区域只要布局即可 
bool hitTest(BoxHitTestResult result, {
    @required Offset position }) {
   
  if (_size.contains(position)) {
   
    // 对于每一个child调用自己的 hitTest,所以布局最深的子wiget放在最开始
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
   
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
  }
  return false;
}

而RenderView的hitTest(BoxHitTestResult result, { @required Offset position })最终调用RenderBox的hitTest(BoxHitTestResult result, { @required Offset position })hitTestChildrenhitTestSelf是两个抽象方法(因为Widget可能有一个child或者多个child),查看具体实现发现其实逻辑和这儿差不多,也是先判断自己是否在这次点击的Postion范围内,然后递归调用子Widget的hitTest。观察这个方法结构,我们知道,如果一个Widget越深,则越先被添加进HitTestResult中。这样的流程执行下来,HitTestResult就得到了这次点击事件坐标上所有能响应的控件集合。需要注意,GestureBinding中最后把自己添加到Result的结尾

//GestureBinding中最后把自己添加到result中
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
   
  result.add(HitTestEntry(this));
}

在这里插入图片描述
GestureBinding#void dispatchEvent(PointerEvent event, HitTestResult hitTestResult)

_handlePointerEvent() 
if (hitTestResult != null ||
      event is PointerHoverEvent ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) {
   
    dispatchEvent(event,
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值