Flutter 滑动控制

这篇具有很好参考价值的文章主要介绍了Flutter 滑动控制。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

核心结构

以PageView为例
pv基于scrollable进行定制,四个完成功能的主要组件:ScrollNotification、RawGestureDetector、ScrollController和ScrollPosition、ViewPort

  • ScrollNotification:封装Notificaiton获得该类通知,根据通知信息内的偏移判断页面是否切换,然后回调onPageChanged

  • RawGestureDetector:手势收集类,Scrollable的setCanDrag方法绑定了VerticalDragGestureRecognizer或者HorizontalDragGestureRecognizer用来收集两个方向的滑动信息

  • ScrollController和ScrollPosition:ScrollPosition是Scrollable中实际控制滑动的对象,在SrcollController的attach方法中,ScrollPosition会将ScrollController作为其观察者添加到Listeners中,常通过ScrollController.addListener方法添加滚动监听

  • ViewPort:接受来自ScrollPosition的偏移量,绘制区域来完成滑动

流程分析

  • 当手指滑动时,RawGestureDetector收集手势信息
  • 将手势信息回调到Scrollable中
  • Scrollable接收到信息后,通过ScrollPosition进行滑动控制
  • 修改偏移量,通过viewport绘制不同区域
  • 通知scrollcontroller,进行观察者通知

点击事件传递

原生经过c++引擎转发到flutter

这里挑取部分方法进行分析

GestureBinding _handlePointerDataPacket(ui.PointerDataPacket packet)

将data中的数据,映射到为逻辑像素,再转变为设备像素

//未处理的事件队列
final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
//这里的packet是一个点的信息
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
  // 将data中的数据,映射到为逻辑像素,再转变为设备像素
  _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
  if (!locked)
    _flushPointerEventQueue();
}

GestureBinding _flushPointerEventQueue()

将列表中的每个点击事件调用_handlePointerEvent进行处理

void _flushPointerEventQueue() {
  assert(!locked);
  while (_pendingPointerEvents.isNotEmpty)
     //处理每个点的点击事件
    _handlePointerEvent(_pendingPointerEvents.removeFirst());
}

GestureBinding _handlePointerEvent(PointerEvent event)

对down、up、move三种事件进行处理,包含:加入集合、取出、移除、分发

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);
  }
}

hittest

///renderview:负责绘制的root节点
RenderView get renderView => _pipelineOwner.rootNode;
///绘制树的owner,负责绘制,布局,合成
PipelineOwner get pipelineOwner => _pipelineOwner;

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));
  }
}

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,因为判断区域只要布局即可

对于每一个child调用自己的hitTest,所以布局最深的子wiget放在最开始

该方法先检查自己是否在范围内,是的话调用hitTestChildren,递归调用子Widget的hitTest,越深的widget越先被加入HitTestResult中。

执行完后,HitTestResult得到了点击事件坐标上所有能响应的控件集合,最终GestureBinding中最后把自己添加Result的结尾

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

当hitTestResult不为空时,进行事件分发,循环调用集合中每个对象的handleEvent,但不是所有的控件都会处理handlerEvent,大部分时候只有RenderPointerListener会处理。

handleEvent会根据不同的事件类型,回调到RawGestureDetector的相关手势处理中。

省流

down事件出现,hittest根据点击的position获得一个可以响应事件的object集合,该集合末尾为GestureBinding、
通过dispatchEvent进行分发事件,但不是所有空间的RenderObject子类都会处理handleEvent,大部分时候由嵌入RawGestureDetector中的RenderPointerListener处理、
handleEvent根据不同事件类型,回调到RawGestureDetector的相关手势处理

手势竞争

因为通常点击后会返回一组可响应事件的组件集合,需要交付给哪个组件进行处理?

  • GestureRecoginizer:手势识别器基类,基本上RenderPointListener中需要处理的手势事件,都会分发到它对应的GestureRecognizer,通过处理和竞技后再分发,常见有:OneSequenceGestureRecognizer 、 MultiTapGestureRecognizer 、VerticalDragGestureRecognizer 、TapGestureRecognizer 等等。
  • GestureArenaManager:手势竞技管理,管理竞技过程,胜出条件为:第一个竞技获胜的成员最后一个不被拒绝的成员
  • GestureArenaEntry:提供手势事件竞技信息的实体,内封装参与事件竞技的成员
  • GestureArenaMember:参与竞技的成员抽象对象,内部有acceptGesture和rejectGesture方法,它代表手势竞技的成员,默认GestureArenaRecognizer都实现了它,所有竞技的成员可以理解为GestureRecognizer间的竞争
  • _GetureArena:GestureArenaManager内的竞技场,持有参与竞技的members列表

当一个手势试图在竞技场开放时获胜 isOpen = true,它将成为一个带有“渴望获胜”属性的对象,
当竞技场关闭时,竞技场会试图寻找一个渴望获胜的对象成为新的参与者

GestureBinding handleEvent(PointerEvent event, HitTestEntry entry)

导航事件去触发GestureRecognizer的handleEvent,一般 PointerDownEvent 在 route 执行中不怎么处理。

gestureArena 就是 GestureArenaManager

down事件驱动竞技场关闭

 // from HitTestTarget
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);
  }
}

GestureArenaManager void close(int pointer)

在完成事件分发后调用,阻止新成员进入竞技

void close(int pointer) {
   //拿到上面 addPointer 时添加的成员封装
  final _GestureArena state = _arenas[pointer];
  //关闭竞技场
  state.isOpen = false;
  //决出胜者
  _tryToResolveArena(pointer, state);
}

GestureArenaManager _tryToResolveArena(int pointer, _GestureArena state)

void _tryToResolveArena(int pointer, _GestureArena state) {
  if (state.members.length == 1) {
    //只有一个竞技成员的话,直接获胜,触发对应空间的acceptGesture
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  }
  else if (state.members.isEmpty) {
    //无竞技成员
    _arenas.remove(pointer);
  } 
  else if (state.eagerWinner != null) {
    //多个竞技成员
    _resolveInFavorOf(pointer, state, state.eagerWinner);
  }
}

GestureArenaManager sweep(int pointer)

迫使竞技场得出一个决胜者
sweep通常是在up事件发生之后。它确保了竞争不会造成卡顿,从而阻止用户与应用程序交互。

void sweep(int pointer) {
  ///获取竞争的对象
  final _GestureArena state = _arenas[pointer];
  if (state.isHeld) {
    state.hasPendingSweep = true;
    return;
  }
  _arenas.remove(pointer);
  if (state.members.isNotEmpty) {
    //第一个竞争者获取胜利,就是Widget树中最深的组件
    state.members.first.acceptGesture(pointer);
    for (int i = 1; i < state.members.length; i++)
      ///让其他的竞争者拒绝接收手势
      state.members[i].rejectGesture(pointer);
  }
}

BaseTapGestureRecognizer acceptGesture(int pointer)

标志手势竞争胜利,调用_checkDown(),若已经处理过则不再此处理,未处理过则调用handleTapDown


void acceptGesture(int pointer) {
  //标志已经获得了手势的竞争
  super.acceptGesture(pointer);
  if (pointer == primaryPointer) {
    _checkDown();
    _wonArenaForPrimaryPointer = true;
    _checkUp();
  }
}
void _checkDown() {
   //如果已经处理过了,就不会再次处理!!
   if (_sentTapDown) {
     return;
   }
   //交给子控件处理down事件
   handleTapDown(down: _down);
   _sentTapDown = true;
}

BaseTapGestureRecognizer _checkUp()

若非胜利者或事件为空则返回
否则处理up事件,并重置

void _checkUp() {
  ///_up为空或者不是手势竞争的胜利者,则直接返回
  if (!_wonArenaForPrimaryPointer || _up == null) {
    return;
  }
  handleTapUp(down: _down, up: _up);
  _reset();
}

TapGestureRecognizer#handleTapUp({PointerDownEvent down, PointerUpEvent up})

先执行onTapUp再到onTap,完成一次点击事件的识别文章来源地址https://www.toymoban.com/news/detail-622549.html

void handleTapUp({PointerDownEvent down, PointerUpEvent up}) {
  final TapUpDetails details = TapUpDetails(
    globalPosition: up.position,
    localPosition: up.localPosition,
  );
  switch (down.buttons) {
    case kPrimaryButton:
      if (onTapUp != null)
        invokeCallback<void>('onTapUp', () => onTapUp(details));
      if (onTap != null)
        invokeCallback<void>('onTap', onTap);
      break;
    case kSecondaryButton:
      if (onSecondaryTapUp != null)
        invokeCallback<void>('onSecondaryTapUp',
          () => onSecondaryTapUp(details));
      break;
    default:
  }
}

省流:

  • 事件从Native层通过C++传递到Dart层,通过映射为逻辑像素后在GestureBinding中进行处理
  • 手势都从down开始,down阶段,HitTest从负责绘制树的根结点开始,递归将可响应事件的控件添加到HitTestResult中,最后添加GesureBinidng到末尾,对result中每个对象进行事件分发
  • RawGestureDetector会进行处理事件,down阶段,会将竞争者添加到_GestureArena进行竞争,最后回到GestureBinding关闭竞技场,若只有一个RawGestureDetector进行竞争,则直接acceptGesture,但依然不会触发onTap,up事件结束后触发onTapup后再触发onTap
  • 当多个RawGestureDetector竞争,在sweep中选取第一个为胜利者
  • 滑动则是move阶段就产生胜利者

到了这里,关于Flutter 滑动控制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Flutter 小技巧之滑动控件即将“抛弃” shrinkWrap 属性

    相信对于 Flutter 开发的大家来说, ListView 的 shrinkWrap 配置都不会陌生,如下图所示,每当遇到类似的 unbounded error 的时候,总会有第一反应就是给 ListView 加上 shrinkWrap: true 就可以解决问题,那为什么现在会说 shrinkWrap 即将被“抛弃”呢? 其实说完全“抛弃”也不大严谨,从

    2024年02月16日
    浏览(51)
  • Flutter笔记:手写并发布一个人机滑动验证码插件

    Flutter笔记 手写一个人机滑块验证码 作者 : 李俊才 (jcLee95):https://blog.csdn.net/qq_28550263 邮箱 : 291148484@163.com 本文地址 :https://blog.csdn.net/qq_28550263/article/details/133529459 写 Flutter 项目时,遇到需要滑块验证码功能。滑块验证码属于人机验证码的一种,看起来像是在一个图片

    2024年02月07日
    浏览(36)
  • Flutter组件-ListView滑动到指定位置(SingleChildScrollView 实现锚点效果)

    ListView 组件默认内容比较多的时候具有延迟加载的特性。  SingleChildScrollView 不支持基于 Sliver 的延迟实例化模型,也就是使用 SingleChildScrollView  默认没有延迟加载的特性。  SingleChildScrollView 类似于 Android 中的 ScrollView,它只能接收一个子组件,由于默认没  有延迟加载

    2024年02月11日
    浏览(38)
  • Flutter Scrollbar滑动条与SingleChildScrollView的结合使用的小细节

    我在业务开发中,ListView是竖向滑动的,然后 ListView中的每一个小条目比较长,我需要横向滑动,所以 就有了 ListView中多个SingleChildScrollView(横向滑动),但是在视觉上,我期望告知用户可以横向滑动,所以有了 Scrollbar 结合 SingleChildScrollView 来使用。 但是两者来使用,多多少少

    2024年01月18日
    浏览(40)
  • 第4天:基础入门-APP架构&小程序&H5+Vue语言&Web封装&原生开发&Flutter

    1.原生开发 安卓一般使用java语言开发,当然现在也有kotlin语言进行开发。如何开发就涉及到具体编程了,这里就不详说了。简单描述就是使用安卓提供的一系列控件来实现页面,复杂点的页面可以通过自定义控件来实现。 2.使用H5语言开发 使用H5开发的好处有很多,可多端复

    2024年04月10日
    浏览(47)
  • Android应用-Flutter实现丝滑的滑动删除、移动排序等-Dismissible控件详解

    Dismissible 是 Flutter 中用于实现可滑动删除或拖拽操作的一个有用的小部件。主要用于在用户对列表项或任何其他可滑动的元素执行删除或拖动操作时,提供一种简便的实现方式。 列表项删除: 允许用户在列表中通过滑动手势删除某个项。 左右滑动: 提供可自定义的背景,当

    2024年02月04日
    浏览(50)
  • 【Flutter】下载安装Flutter并使用学习dart语言

    安装flutter, 并使用flutter内置的dartSDK学习使用dart语言。 编辑器: Android Studio fluuter 版本 : flutter_windows_3.13.1 内置dartSDK : 3.1.0 dart路径路径: flutter安装路径bincachedart-sdk flutter下载地址 官网的下载描述蛮详细的,直接用就行。 Android Studio 需要到官网下载安装包。 如果你c盘容

    2024年02月09日
    浏览(47)
  • Flutter学习四:Flutter开发基础(六)调试Flutter应用

    目录 0 引言 1 调试Flutter应用 1.1 日志与断点 1.1.1 debugger() 声明 1.1.2 print和debugPrint 1.1.3 调试模式、中间模式、发布模式 1.1.4 断点 1.2 调试应用程序层 1.2.1 转储Widgets树 1.2.2  转储渲染树 1.2.3 转储Layer树 1.2.4 转储语义树 1.2.5 调度(打印帧的开始和结束) 1.2.6 可视化调试

    2024年02月12日
    浏览(56)
  • Flutter系列文章-Flutter 插件开发

    在本篇文章中,我们将学习如何开发 Flutter 插件,实现 Flutter 与原生平台的交互。我们将详细介绍插件的开发过程,包括如何创建插件项目、实现方法通信、处理异步任务等。最后,我们还将演示如何将插件打包并发布到 Flutter 社区。 在 Flutter 项目中,你可能需要与原生平台

    2024年02月11日
    浏览(39)
  • flutter系列之:使用AnimationController来控制动画效果

    目录 简介 构建一个要动画的widget 让图像动起来 总结 之前我们提到了flutter提供了比较简单好用的AnimatedContainer和SlideTransition来进行一些简单的动画效果,但是要完全实现自定义的复杂的动画效果,还是要使用AnimationController。 今天我们来尝试使用AnimationController来实现一个拖

    2024年02月05日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包