【Flutter】基于 Draggable + DragTarget + GridView 还可以这么玩 —— 书架功能的实现

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

前言

这篇是对书架功能的一个小总结,顺便介绍下基本功能以及实现方式技术点,另外对最近的疯狂摸鱼做一番胡适式检讨;

开门见山的说,先放一下效果图:

demo.webp

总结一下的话,功能点就这么两个:

  • 基于GridView的重排序功能;
  • 基于Draggable + DragTarget 的手势处理

虽说功能点就这么两个,但是涉及到的知识点也确实不少,挨个总结一下,看看是不是跟你设想的方案一样:

实现与技术点总结

基于GridView的重排序功能:

原理解析:

如果仅仅指这个标题所述的效果,相信不少人闭着眼用舌头都能敲出好几种方案,比如说最基本的 GridView.builder + setState 改变 数据源的顺序的方案;

如果在这个基础上,加一个长按Item排序呢?

以普遍理性而言,这时候那批用舌头敲代码的人,应该应该要睁开眼睛了;

对于他们来说,现在提出的方案,可能是 GestureDector + GestureBinding.hitTest ,然后遍历 HitTestResult.path ?再次点,给所有Item 加上GlobalKey ,不断Item,获取手势位置是否在Item范围内,进而得知重排序的顺序并 setState ;

如果在加个 Item长按拖拽移动 / 虚影 呢?

那就给长按的Item 加个OverLay ?

如果再加个重排序动画呢 ?

全体 Item 都加 Tansform ? 然后再各个计算 Item transform的 结果;

可能到这里,有人就感觉,上面这套方案最后实现出来的效果,会是一坨稀饭;但实际上,这套方案确实是可行的,也是我的实现的最最最基本的原理,只不过其中绝大部份东西,Flutter都已经提供好了,只需要正确组合使用即可;

技术选型:

根据上面的基本思路,我选择的技术方案是 自定义GridView + Draggable + DragTarget + AnimateCotnainer的方案:

1、自定义GridView

为什么需要一个自定义的GridView呢?其作用是什么

主要作用是提供重排序的功能;

当然,这时候可能有彦祖发弹幕提问:

之前不是说了,直接用 setState 调整数据源的顺序,不就可以实现重排序了么?

当然,setState 确实可以实现重排序,但是由此会导致 itemBuilder 重建Item ,进而导致item本身重建,最终导致 item中所持有的状态全部初始化;

这会导致什么问题呢?这时候如果你使用Draggable,就会发现其中的状态因为初始化,很多东西就显示异常,比如说childWhenDragging 显示的Item 被回复为原 child,原先记录的手势信息也会被初始化,导致回调触发异常;

所以说,还不能触发Item本身的重建,或者说,只能允许重建Item的内容,其本身不允许被修改(感觉说的有些绕,总之,做法就是打脏Item本身,而非GridView,打脏GridView就会导致Item重建);

这时候我选择的方案是调整 GridView 中 child 的Element 和 RenderObject的顺序,而非数据源的顺序,并在必要的时候,打脏Item本身使其重建,这样既能保持原有Item中的状态,又调整了其在GridView中的顺序;

2、Draggable+DragTarget

这两个在重排序这个主题下的作用,就是当作获取被操作Item和目标Item位置的作用;

不过需要注意的一点是,由于上面的方案调整的是各个Item在GridView的RenderObject层面的位置,而非GirdView的数据源的位置,那么就会出现一个问题:

如何获取Draggable和DragTarget在触发重排序的时候,其各自在RenderObject层面中的位置?

在这里,我的做法,是在各个Item外面再包一层InheritedWidget,其中存储着包含RenderObject位置信息在内的各种信息,在触发重排序的时候,获取这个InheritedWidget读取对应信息即可:

image.png

image.png

这个ItemData中位置信息的维护工作,就由上面的那个自定义GridView顺带完成:

image.png

3、AnimatedContainer

AnimatedContainer 的作用就是提供一个重排序的交换动画

不过这块跟原版的AnimatedContainer相比,还是需要一定的自定义处理;

由于为了保存包括Dragable在内的Item状态,因此AnimatedContainer 的状态也因此得到了保存;由此带来的的一个问题就是:

这回导致动画会从上一次结束的偏移量开始;而重排序后的Item本身位置就发生了改变,再加上上一次动画偏移量的影响,最终效果就是位移动画异常,完全不是从原位置到目标位置的效果;

所以为了解决这个问题,需要对AnimatedContainer做一定的修改,我的做法是让其只播放从给定偏移量到偏移量0的一个动画:

这里仅仅重写这个方法,重设动画起点和终点位置即可:

image.png

基于Draggable + DragTarget 的手势处理

这里所说的手势处理,就是指什么时候是交换重排序手势,什么时候是合并文件的手势。以及停留和延迟判断手势这块的处理;

原理分析:

首先重排序和合并手势的区分,其实很简单,不少玩过阅读类APP的人脱口就能说出:

无非就是移动到小说封面边缘就是交换重排序,移动到封面中央停留一段时间就是合并操作嘛

按这个说法,思路也就出来了:

  • 给DragTarget加个延时操作,用于停留操作的判断,重排序那块设置少点,比如说200ms,合并文件的那块就设置个一秒这种;

  • 最底下的DragTarget 是负责重排序判断的DragTarget, 中央再放一个小区域,用来当合并手势操作的DragTarget;

方案:

关于如何加个延时操作,做法也很简单,参考 DelayedMultiDragGestureRecognizer 的实现方式,可以用timer加个延时回调,如果触发了其他手势,那么去掉或者重置timer即可;

至于区域这块的处理,就更简单了,直接用一个Stack处理一下就行;

image.png

结语

现在书架这块的功能点实现也基本实现了,剩下的工作除了润色一下,完成一下边缘功能,就到了帖子和社区这块的实现了;

Guess you like

Origin juejin.im/post/7067444814437941256