「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。
前言
今天是复工的第一天,现在还是不太能接受假期就这么结束了,就当是一场梦,醒了很久还是很不能接受;
回正题,
这篇的主题在Flutter中实现拖拽排序可以怎么做;
当然,flutter 中本身就带有 ReorderableListView 这个控件,不过如果需要对 GridView、瀑布流这种来做一个拖拽排序的话,flutter本身并没有类似 ReorderableGridView 这种东西;
这时候就到了万能的 pub.dev 上场的时候了
reorderable 介绍
目前在pub上搜索reorder这个关键词,相关部分中,like最多的就是 reorderable,
按照其描述,提供了多种类型的可排序组件,
其中有一个ReorderableWrap 就是我需要的效果……额,应该说一部分;也就是这次要说的内容:长按拖拽排序;
reorderable 的实现原理
还是老样子,先带着问题分析过程:
先对其实现过程提出这么几个问题:
- 长按之后会在原地留下个虚影,这个是怎么实现的;
- 移动到其他Item的时候,是怎么判断插入到目标Item的前面还是后面的?
- 排序之后,其他Item依次往后或者往前移动,这个效果是怎么实现的?
要解决这些问题,首先要看下基本机构和原理:
要了解基本机构,就要来到build方法:
build 方法快速一览:
build方法最终返回的东西,其实默认是一个Wrap,也就是那个defaultBuildItemContainer构造出来的,所以可疑的部分来到了wrappedChildren:
而这个wrappedChildren,仅仅是对child包了一层?????看来核心是这个_wrap 方法?
不过值得警觉的是,这里好像,做了个排序,先记一下:
那么来看下这个值得怀疑的_wrap方法:
_wrap 方法快速一览
_wrap 方法挺长的,不过还是先看return 部分;
_wrap 的 return 这块 ,_buildContainerForMainAxis 方法是返回一个 wrap ,对于了解结构这块,好像意义不大;
_includeMovedAdjacentChildIfNeeded 有点意思,是根据 _ghostDisplayIndex 和 _currentDisplayIndex ,以及传入的 childDisplayIndex ,对构造的 dragTarget 进行一个改造,判断是否需要添加 disappearingPreChild ,以及添加的顺序,这里先放一下,毕竟这几个参数是干啥用的,也不知道;
下面这个 dragTarget ,就是Reorderables实现的核心点了;
dragTarget 是一个stack,由三层组成:containedDraggable.builder ; 位置在left、top的一半大小的preDragTarget,以及在right、bottom,同样一半大小的nextDragTrget;
其实应该有人看出来了,不止这里,前面也多次出现过 Draggable 和 DragTarget 相关的部分的词语;
所以其实核心技术点就是Draggable跟DragTarget的组合使用;preDragTarget 跟 nextDragTarget 这两块也确实的出现了DragTarget
而上面提到的 containedDraggable.builder ,其实就是Draggable:
所以结构也很清楚了,总结一下的话就是
在Wrap的容器内有数个Item,每个Item都是一个Stack,里面除了item的内容外,还分别在两边各放一半的SizeBox来当作DragTarget;
基本结构了解完,貌似部分问题其实就有答案了……
长按留下的虚影的实现方式:
Draggable提供了 childWhenDragging 方法,以供在拖拽的时候在原地留个占位;
那么将这个占位做个半透明处理,那不就是虚影了?
Item拖拽到指定位置后,移动位置的判断方式:
正如上面在基本结构分析中所述,每个Item,其实都包含了两个DragTarget,各占一半;拖拽到哪个DragTarget,哪个DragTarget做出响应即可:
依次移动动画的实现方式:
知道了位置之后,插入的动画怎么实现,就要看这个 onWillAccept方法,它是怎么处理响应的:
首先是对一些参数进行计算,做一些边界判断,以及判断滑动响应的Item是自己本身对应的,那就不做响应,之类的东西:
下面这块就是真正去响应操作,触发动画的部分了:
在这里触发一次 setState ,修改 _nextDisplayIndex , 触发_requestAnimationToNextIndex
参数这块先不管,setState总体完成后再看看具体作用,那么看下这个_requestAnimationToNextIndex 方法:
这里的作用就是设置 _ghostDisplayIndex、_currentDisplayIndex 这两个参数的值,并触发了两个AnimationController
话说,这个_ghostDisplayIndex、_currentDisplayIndex , 是不是在哪里见过???
好像,build方法中,会根据_currentDisplayIndex 做个排序?
那么setState的作用就清楚了,直接排序,并将已经处于 childWhenDragging 状态的Item 插入过去,这样,虚影就显示到目标位置了;
而这两个AnimationController的作用,就要看其调用位置了
在前面的贴图中,可以看到,很多部分,Item的显示内容,都会包一层东西,比如说 childWhenDragging 那块,child是一个 _makeAppearingWidget ,builder方法也是,会往List中加入 _makeDisappearingWidget 构造出的widget;
总结一下的话,就是会往目标位置跟拖动Item中的Item放入_makeAppearingWidget跟_makeDisappearingWidget包裹的Item
其实这两个Controller就是控制这两个Widget的,一个负责处理添加的时候后移操作动画,一个负责处理移走的前移操作动画;
而其实现方式也简单:
没错,用SizeTranstion,通过动画改变Item大小,把后面的Item挤过去……
总结一下:
setState 后build方法将要拖拽的Item移动到目标位置,这时候Item本身的大小为0,随着动画的播放,SizeTranstion 逐渐改变Item的大小,后面的部分就这么给挤过去了……(这方法我当时确实没想到,还可以这么玩~~~)
当然会对特殊情况做个小处理:
当发生跨行的时候,让跨行的第一个Item也执行由小变大的动画;
结语
这作者的操作确实让人开眼界,我当时还以为要通过OverLay之类的东西来搞,没想到其实实现方式没必要那么复杂,这不挺简单的嘛:
国际惯例,效果图:
下面就是搞搞这个的改造,加入那个文件夹功能;