【Flutter】基于 Draggable + DragTarget + GridView 还可以这么玩 —— 书架功能的实现
前言
这篇是对书架功能的一个小总结,顺便介绍下基本功能以及实现方式技术点,另外对最近的疯狂摸鱼做一番胡适式检讨;
开门见山的说,先放一下效果图:
总结一下的话,功能点就这么两个:
- 基于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读取对应信息即可:
这个ItemData中位置信息的维护工作,就由上面的那个自定义GridView顺带完成:
3、AnimatedContainer
AnimatedContainer 的作用就是提供一个重排序的交换动画
不过这块跟原版的AnimatedContainer相比,还是需要一定的自定义处理;
由于为了保存包括Dragable在内的Item状态,因此AnimatedContainer 的状态也因此得到了保存;由此带来的的一个问题就是:
这回导致动画会从上一次结束的偏移量开始;而重排序后的Item本身位置就发生了改变,再加上上一次动画偏移量的影响,最终效果就是位移动画异常,完全不是从原位置到目标位置的效果;
所以为了解决这个问题,需要对AnimatedContainer做一定的修改,我的做法是让其只播放从给定偏移量到偏移量0的一个动画:
这里仅仅重写这个方法,重设动画起点和终点位置即可:
基于Draggable + DragTarget 的手势处理
这里所说的手势处理,就是指什么时候是交换重排序手势,什么时候是合并文件的手势。以及停留和延迟判断手势这块的处理;
原理分析:
首先重排序和合并手势的区分,其实很简单,不少玩过阅读类APP的人脱口就能说出:
无非就是移动到小说封面边缘就是交换重排序,移动到封面中央停留一段时间就是合并操作嘛
按这个说法,思路也就出来了:
-
给DragTarget加个延时操作,用于停留操作的判断,重排序那块设置少点,比如说200ms,合并文件的那块就设置个一秒这种;
-
最底下的DragTarget 是负责重排序判断的DragTarget, 中央再放一个小区域,用来当合并手势操作的DragTarget;
方案:
关于如何加个延时操作,做法也很简单,参考 DelayedMultiDragGestureRecognizer 的实现方式,可以用timer加个延时回调,如果触发了其他手势,那么去掉或者重置timer即可;
至于区域这块的处理,就更简单了,直接用一个Stack处理一下就行;
结语
现在书架这块的功能点实现也基本实现了,剩下的工作除了润色一下,完成一下边缘功能,就到了帖子和社区这块的实现了;
转载自:https://juejin.cn/post/7067444814437941256