【Flutter】300行代码实现拖拽排序?这次就来挑战一下

1,736 阅读5分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

前言

在前面一篇文章中,对 ReOrderables 是如何实现拖拽排序做了个简单的分析,在简单的分析完成之后,除了学到了一些骚操作之外,有一个想法也不由得冒了出来:

这代码,咋这么多计算和调整部分……

作为社会主义新时代的优秀饭桶,像这种充斥着计算与修改的代码,那肯定是懒得看的;

那么问题来了:

有没有不计算,代码少的方式来实现这个拖拽排序呢?

效果设计

拖拽排序这块,其实最复杂的部分就是排序动画的处理,在 reOrderables 中,可以说绝大部份代码都是为了定位排序位置,最后通过调整Size,用AnimationController辅助处理,这种方式来实现一个折叠动画;

说实话,效果是有了,但是这代码确实让人看的容易晕;

在 Android 中,其实更多的排序动画是类似这种平移动画的:

demo.gif

所以这次我也计划使用这种动画实现(其实就是懒);

当然,该有的虚影之类的也是应该有的东西;

实现方案:

获取各个Item的位置信息

要想实现上面的平移动画,第一步就是知道各个Item自己本身在GridView中的位置坐标;

关于获取位置的实现方式,大部分方案都是通过给各个Item加上GlobalKey,然后获取renderObject来测量;

不过这次玩点花的,我想尽量少些代码的同时,减少GlobalKey的数量;

众所周知,sliver在layout方法结束后,会调用 didFinishLayout 方法,来通知layout结束,并公布当前缓存的第一个和最后一个Item的index;

既然这样,我在layout方法结束的时候,获取一下各个Item所在的起始位置不就行了?

提供一个共享Item位置的 InheritedWidget

这个InheritedWidget的作用,就是保存两个列表,分别存放Item的实际位置和重排序位置:

image.png

至于这两个List干啥用的,后面再揭秘;

PS : 其实用List存放位置信息是不合适的,因为List中的第一个Item,并不一定是GridView中index = 0 的那个Item,所以最后处理的时候要做一个index的转换,比较麻烦;

我这里偷懒,先用List存了

给我一个Key,还你一片Offset

继承GridView,重写buildChildLayout , 给SliverGrid设置一个GlobalKey:

image.png

这个 Key 的作用,就是获取 SliverGrid 的 Element;而通过 Element 就可以拿到 各个child 对应的Element;

不过这里稍微不同的是,由于所说方法名是didFinishLayout,但是实际上,这里是performLayout方法的最后一步而已,所以performLayout方法实际上是没有完成的;

因此parent默认是不允许访问child的size的,如果调用renderOject.localToGlobal就会报错;

既然这样,可以用另一种方式获取child的位置信息,比如说,ParentData:

image.png

PS :其实Key这玩意也可以不用设置,在前面对Sliver的分析中已经知道了,调用 SliverChildDelegate 的 didFinishLayout方法的就是 SliverGrid 的 Element ,说白了,如果勤奋点,把 sliver那块重写下,就可以把Element传过来了;

同时因为 Element 可以视为 BuildContext , context 也能拿到,进而也能用来获取InheritedWidget,共享数据也很方便,当然这里是放到build方法中获取InheitedWidget来偷懒;

所以我感觉最合适的方法是重写sliver以及这个SliverChildDelegate……

懒人福音 AnimatedContainer

在这里对build的Item进行一定的处理,首先最外层包一层AnimatedContainer:

image.png

AnimatedContainer 可以说是一个全自动动画播放器,没有AnimationController和Tween,仅仅一个setState就可以方便的实现切换动画;

而前面存放的两个List中的内容,就在这里用上了:

只需要把两个Item的Offset一减,setState之后就能实现位置平移动画;

PS:这个方案来自一位德国小哥的项目

不过我感觉他也写的挺麻烦的,没有结合DragTarget,而是仅仅通过Draggable返回的手势信息来做的碰撞检测,看的有点眼花……😵‍💫

PPS : 我知道他为什么不用DragTarget了……GridView本身的hitTest,是根据主轴交叉轴信息修改了点击坐标位置的,但是AnimatedContainer 中加入Transform后,这个坐标再次经过修改后,就根本命中不了任何一个Item…………所以,当Item通过Transform改变位置后,Draggable和DragTarget 是不会响应任何手势的……

至于这两个List中的内容是如何改变的,就是下面这部分了:

Pull My "Draggable" Trigger !

这里就来到了熟悉的Draggable+DragTarget部分:

在上图中,最后构造出的Item是一个Stack,其中原本 Item 应有的 child 传递给了buildDraggable方法构造Draggable;

另一部分就是DragTarget ,关键部分就在这里,其所做的事其实就是检测到碰撞后,根据自己和碰撞Item的index,重排序一下:

image.png

最后通过回调,触发外面的setState,启动动画即可:

结语:

两个文件共计不到300行:

image.png

image.png

虽说不到300行实现了目标效果,不过bug还是有的,比如说再次滑动切换DragTarget的时候,位置信息就算错了,估计某个状态没存上,onDragEnd之后通知保存数据这块也没做处理

但是300行确实能实现核心逻辑,剩下的东西都是些边边角角的部分,多也多不到哪去……吧,大概吧~~~~~

回正题:国际惯例,看下效果:

ezgif-3-6b87ca3544.webp

更新:

后来看了下,这块还真没法用Draggable+DragTarget 的方案,还真只能靠Draggale返回的手势回调来做计算…………

原因就是AnimatedContainer 这块中的PPS补充部分……

不计算这个目标流产了……

通过计算的方式估计要加个200行代码……

或者说有什么方案能让GridView的Item移动,但DragTarget不移动呢………… 比如说用Stack往Grid上面覆盖一层,用来放DragTarget呢 …………