【Flutter 问题系列第 79 篇】在 Flutter 中使用 ReorderableListView 实现拖拽排序列表组件的功能

news/2025/2/14 6:13:43/

这是【Flutter 问题系列第 79 篇】,如果觉得有用的话,欢迎关注专栏。

当前开发环境
Flutter 版本:3.10.5,Dart 版本:3.0.5,操作系统:macOS

文章目录

      • 一:效果演示
      • 二:ReorderableListView 源码分析
        • 2-1:必需属性
        • 2-2:可选属性
      • 三:如何使用 ReorderableListView
      • 四:如何指定组件中的部分区域进行拖拽
        • 4-1:问题分析
        • 4-2:解决方案

一:效果演示

在 Flutter 中,实现拖动某一个组件可以使用 Draggable,比如实现悬浮球功能。

除了拖拽一个组件外,在很多 App 中都会有对某个列表中的组件进行拖拽排序的功能。比如添加某个分类后,然后对这些分类进行拖拽排序。

下面以排序动漫排名为案例,动态演示图的效果如下

这种效果的话,使用 Draggable 组件的话就无法实现了。不过 Flutter 提供了另外一个实现拖拽排序列表的组件 ReorderableListView,上面的案例就是基于 ReorderableListView 实现的。

二:ReorderableListView 源码分析

查看 ReorderableListView 的源码可知,它继承自 StatefulWidget ,如下所示

/// 从预构建的小部件列表创建可重新排序的列表组件
class ReorderableListView extends StatefulWidget {ReorderableListView({super.key,required List<Widget> children, // 需要拖动排序的子组件列表required this.onReorder, // 拖拽完成后的回调。用于报告列表项已被拖到列表中的新位置,并且应用程序应更新项的顺序this.onReorderStart,this.onReorderEnd,this.itemExtent,this.prototypeItem,this.proxyDecorator,this.buildDefaultDragHandles = true,this.padding,this.header,this.footer,this.scrollDirection = Axis.vertical,this.reverse = false,this.scrollController,this.primary,this.physics,this.shrinkWrap = false,this.anchor = 0.0,this.cacheExtent,this.dragStartBehavior = DragStartBehavior.start,this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,this.restorationId,this.clipBehavior = Clip.hardEdge,
})

除了有一个默认构造 ReorderableListView 外,还有一个 ReorderableListView.builder 的构造,用于懒加载显示。

2-1:必需属性

情况一:使用 ReorderableListView 的默认构造

默认构造有两个必传属性 children 和 onReorder,部分源码如下所示。

ReorderableListView({required List<Widget> children,required this.onReorder,...
)}

children 就是我们将要拖动的 item 组件列表,这个没什么可说的。着重说一下属性 onReorder,它的类型是 ReorderCallback,源码如下所示

typedef ReorderCallback = void Function(int oldIndex, int newIndex);

其中 oldIndex 是拖拽完成前原 item 在列表中的索引,oldIndex 是拖拽完成后新的 item 在列表中的索引。

情况二:使用 ReorderableListView 的 builder 构造

builder 构造有三个必传属性,部分源码如下所示。

const ReorderableListView.builder({required this.itemBuilder,required this.itemCount,required this.onReorder,...
})

其中 itemCount 就是拖动列表的长度,onReorder 在默认构造中已作说明,不再赘述。着重说一下 itemBuilder 参数,它是 IndexedWidgetBuilder 类型,源码如下所示

typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);

其中 context 是回调当前组件的上下文,index 是当前构造 item 组件时的索引。

需要特别说明的是:

你需要在 itemBuilder 中,给你的 item 加一个唯一标识 Key,否则的话会报 All children of this widget must have a key. 的问题,这点在源码的断言中可以体现出来,如下图所示

在这里插入图片描述

2-2:可选属性

一:proxyDecorator

关于此属性,官方给出的解释太晦涩,用我的话来说它的作用可以理解为,拖动某一个组件时代替显示原组件。

什么?还是看不懂什么意思?那就上动态演示图,主打一个宠粉

这样 proxyDecorator 属性什么作用就很明显了吧,下面看一下它的源码,它是一个可空的 ReorderItemProxyDecorator 类型

typedef ReorderItemProxyDecorator = Widget Function(Widget child, int index, Animation<double> animation);

其中,child 和 index 分别是当前拖动中的组件及索引,如果你想在拖动时显示的还是当前拖动的组件,把 child 返回出去就行了,而animation 是回调的拖动动画。

如果你想自定义拖拽中显示的组件,那就天马行空式的使用 proxyDecorator 属性吧。

二:其它属性

至于其它的参数,从源码 ReorderableListView 的状态类 _ReorderableListViewState 的 build 方法可知,ReorderableListView 组件的本质其实就是 CustomScrollView,如下图所示

在这里插入图片描述

所以 ReorderableListView 的很多属性都是为 CustomScrollView 服务的,对 CustomScrollView 或者其父组件 ScrollView 不熟悉的,可以跳转查看官方文档。

到这里,铺垫工作算是完成了,下面开始说下如何使用 ReorderableListView。

三:如何使用 ReorderableListView

前面介绍源码看起来内容挺多的,用起来就很方便了。不过,说了那么多,如果不能学以致用,一切都是空谈。

下面以 ReorderableListView 的 builder 构造为例,说下 ReorderableListView 是如何使用的。

自定义一个 List,里面存储显示 item 组件所需的信息,这里我定义为 CartoonItem,伪代码如下所示

  Widget buildReorderableListView() {return ReorderableListView.builder(itemCount: list.length,itemBuilder: (context, index) {// 自定义 item,注意这里设置了 ValueKeyreturn CartoonItem(key: ValueKey(list[index].id), index: index, model: list[index]);},// 拖拽完成回调onReorder: (int oldIndex, int newIndex) {// 更新拖拽后的索引if (oldIndex < newIndex) {newIndex -= 1;}// 更新 list 数组list.insert(newIndex, list.removeAt(oldIndex));setState(() {});},// 拖拽代理(回显当前拖拽中的组件)proxyDecorator: (Widget child, int index, Animation<double> animation) {return AnimatedBuilder(animation: animation,builder: (BuildContext context, child) {return Material(color: Colors.transparent, shadowColor: Colors.transparent, child: child);},child: child,);},);}

全部代码就只有上面这些,这样就实现了使用 ReorderableListView 实现拖拽排序列表组件的功能,用起来是不是很方便。

四:如何指定组件中的部分区域进行拖拽

4-1:问题分析

效果是实现了,可此时产品同学提出,我不想让点击整个卡片区域进行拖动,我想让用户只有拖动卡片最后面的拖动标识 icon 时,才可以拖拽。

这个怎么实现呢?

没有思路的话,那就去 ReorderableListView 的官方文档上找一找,看看有没有什么头绪。

官方文档给出了一个拖动排序的案例,当你点击卡片准备拖拽时,你发现拖拽后没有响应,好像没有作用一样。但你点击卡片后面的拖拽标识时,它竟然可以直接拖动了,动态效果演示图如下

在这里插入图片描述

这不就实现了产品需要的效果了吗?真是踏破铁鞋无觅处,得来全不费功夫啊。

你转念又一想,不对啊,用的是同一个组件 ReorderableListView 啊,怎么在手机上和在网页上的操作刚好是相反的呢。

可以肯定的是,源码中肯定对平台进行了判断。至于如何处理的,这个时候就需要再去看 ReorderableListView 的源码了。

ReorderableListView 源码的 _ReorderableListViewState 类的 _itemBuilder 方法中,渲染 item 时有一个对平台的判断,如下图所示

在这里插入图片描述
果然不出所料,移动端 iOS、android、fuchsia 的话,用的是 ReorderableDelayedDragStartListener,桌面端 linux、windows、macOS 的话用的 ReorderableDragStartListener,桌面端的话,增加了对拖拽方向的判断,不过最终都是用的 ReorderableDragStartListener。

这也是为什么在移动端和桌面端操作不同的根本原因了。

知道了原因,改起来就简单了,直接把 ReorderableListView 的源码改一下不就行了,伪代码如下

 ...switch (Theme.of(context).platform) {case TargetPlatform.iOS:case TargetPlatform.android:case TargetPlatform.fuchsia:Stack(key: itemGlobalKey,children: <Widget>[itemWithSemantics,Positioned.directional(textDirection: Directionality.of(context),top: 0,bottom: 0,end: 8,child: Align(alignment: AlignmentDirectional.centerEnd,child: ReorderableDragStartListener(index: index,child: your item child, // 传入自定义的 child),),),],);...

首先说下结论,这种方式当然是可以的。不过改起来有点麻烦,如果自定义的 child 需要额外传参的话,你还要同步带进来复制到源码中,而且后续 Flutter 升级对此进行修改的话,你还要去关注它修改了哪些地方,自己再去做适配。

那有没有更好的方案呢?

当然有,还是看源码,既然桌面端用的是 ReorderableDragStartListener,那就看一下它的源码,如下图所示

在这里插入图片描述

可以看出来,ReorderableDragStartListener 最终还是通过 Listener 实现的,这也就意味着,被 ReorderableDragStartListener 包括的组件,就可以响应到拖拽事件的通知,那事情就简单多了,接着往下看。

4-2:解决方案

如果想满足产品同学定义的只能通过拖拽标识 icon 进行拖动的话,两步实现。

第一步:

需要把 ReorderableDragStartListener 套在你需要响应拖拽事件的组件之外,

伪代码如下所示

ReorderableDragStartListener(index: index,child: Image.asset(R.ic_drag, width: 16, height: 16),
),

第二步:

设置 ReorderableListView 的属性 buildDefaultDragHandles 为 false。

默认是 true,代表在桌面平台上,拖拽句柄叠加在每项后边缘的中心,在移动平台上长按任意位置开始拖动。

但因为第一步我们已经重定义了拖拽句柄,所以在移动平台上,可以指定任意位置进行拖拽。

完整测试代码放在了 GitHub 的公开项目上了,需要的可自行查看。

至此,关于如何在 Flutter 中使用 ReorderableListView 实现拖拽排序列表组件的功能,便介绍完毕了。

你的问题得到解决了吗?欢迎在评论区留言。

赠人玫瑰,手有余香,如果觉得文章不错,希望可以给个一键三连,感谢。


结束语

Google 的 Flutter 越来越火,截止 2023年10月24日 GitHub 标星已达 158K,Flutter 毅然是一种趋势,所以作为前端开发者,没有理由不趁早去学习。

无论你是 Flutter 新手还是已经入门了,不妨先点个关注,后续我会将 Flutter 中的常用组件(含有源码分析、组件的用法及注意事项)以及可能遇到的问题写到 CSDN 博客中,希望自己学习的同时,也可以帮助更多的人。

http://www.ppmy.cn/news/1173142.html

相关文章

J2EE的N层体系结构

J2EE平台采用了多层分布式应用程序模型&#xff0c;实现不同逻辑功能的应用程序被封装到不同的构件中&#xff0c;处于不同层次的构件可被分别部署到不同的机器中。 RMI/IIOP&#xff1a;RMI&#xff08;Remote Method Invocation&#xff0c;远程方法调用&#xff09;是Java的…

企业数字化建设有哪些路线可以选择?

企业数字化建设涉及利用数字技术来提高行业的效率、准确性和协作性。在选择企业实施数字化建设的路线时&#xff0c;应该考虑组织的需求和目标的各个方面。可以考虑以下一些路线&#xff1a; 1.项目管理软件&#xff1a;实施项目管理软件&#xff0c;可以更好地规划、调度和跟…

二十三种设计模式全面解析-工厂模式:创造对象的魔法工厂

在软件开发中&#xff0c;有一种神奇的设计模式被称为工厂模式&#xff0c;它能为我们创造对象的魔法工厂。无论你是初学者还是有经验的开发人员&#xff0c;掌握工厂模式都是非常重要的。本文将以通俗易懂的方式&#xff0c;全面解析工厂模式&#xff0c;深入探讨如何使用工厂…

Go+VsCode配置环境

大家好&#xff0c;我叫徐锦桐&#xff0c;个人博客地址为www.xujintong.com。平时记录一下学习计算机过程中获取的知识&#xff0c;还有日常折腾的经验&#xff0c;欢迎大家来访。 这个教的是如何用Vscode链接远程的服务器&#xff0c;然后在服务器上配置环境&#xff0c;在服…

【RTOS学习】事件组 | 任务通知

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《RTOS学习》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 事件组 | 任务通知 &#x1f681;事件组&#x1f6f4;大概原理&#x1f6f4;使用事件组的函数同步…

flutter开发实战-hero动画简单实现

flutter开发实战-hero动画简单实现 使用Flutter的Hero widget创建hero动画。 将hero从一个路由飞到另一个路由。 将hero 的形状从圆形转换为矩形,同时将其从一个路由飞到另一个路由的过程中进行动画处理。 Flutter Hero动画 Hero 指的是可以在路由(页面)之间“飞行”的 widge…

visual studio Qt 开发环境中手动添加 Q_OBJECT 导致编译时出错的问题

问题简述 创建项目的时候&#xff0c;已经添加了类文件&#xff0c;前期认为不需要信号槽&#xff0c;就没有添加宏Q_OBJECT,后面项目需要&#xff0c;又加入了宏Q_OBJECT&#xff0c;但是发现只是添加了一个宏Q_OBJECT&#xff0c;除此之外没有改动其它的代码&#xff0c;原本…

pycharm转移缓存目录

原来的缓存目录为C:\Users\86176\AppData\Local\JetBrains&#xff0c;各种配置文件、缓存文件随着pycharm的使用堆积在这里&#xff0c;导致C盘逐渐爆满。 因此需要将缓存目录转移至D盘。首先需要了解缓存目录的知识。 PyCharm 和其他 JetBrains 的 IDE 通常会有两个关键的目…