上两讲中讲解了微信悬浮窗按钮形态的实现,在本章中讲解如何实现悬浮窗列表形态。废话不多说,先上效果对比图。
效果对比
实现难点
这部分的难点主要有以下:
- 列表的每一项均是不规则的图形。
- 该项存在多个动画,如关闭时从屏幕中间返回至屏幕边缘的动画,关闭某项后该项往下的所有项向上平移的动画,以及出现时由屏幕边缘伸展至屏幕中间的动画。
- 列表中存在动画的衔接,如某列表项关闭是会有从中间返回至屏幕边缘的消失动画,且在消失之后,该列表项下面的列表项会产生一个往上移动的动画效果,如何做到这两个动画的无缝链接?
实现思路
列表项非规则图形,依旧按照按钮形态的方法,使用CustomPainter
和CustomPaint
进行自定义图形的绘制。多个动画,根据触发的条件和环境不同,选择直接使用AnimationController
进行管理或编写一个AnimatedWidget
的子类,在父组件中进行管理。至于动画衔接部分,核心是状态管理。不同的列表项同属一个Widget
,当其中一个列表项关闭完成后通知父组件列表,然后父组件再控制该列表项下的所有列表项进行一个自下而上的平移动画,直至到达关闭的列表项原位置。
这个组件的关键词列表
和动画
,可能很多人已经想到了十分简单的实现方法,就是使用AnimatedList
组件,它其内包含了增、删、插入时动画的接口,实现起来十分方便,但在本次中为了更深入了解状态管理和培养逻辑思维,并没有使用到这个组件,而是通过InheritedWidget
和Notification
的方法,完成了状态的传递,从而实现动画的衔接。在下一篇文章中会使用AnimatedList
重写,读者可以把两种实现进行一个对比,加深理解。
使用到的新类
AnimationWidget
:链接 :《Flutter实战》--动画结构
Notification
和NotificationListener
:链接:《Flutter实战》--Notification
InheritedWidget
: 链接:《Flutter实战 》--数据共享
列表项图解及绘制代码
图解对比如下:
image
在设计的时候我把列表项的宽度设为屏幕的宽度的一般再加上50.0,左右列表项在中间的内容部分的布局是完全一样的,只是在外层部分有所不同,在绘制的时候,我分别把列表项的背景部分(背景阴影,外边缘,以及内层)、Logo部分、文字部分、交叉部分分别封装成了一个函数,避免了重复代码的编写,需要注意的是绘制Logo的Image
对象的获取,在上一章中有讲到,此处不再详述。其他详情看代码及注释:
/// [FloatingItemPainter]:画笔类,绘制列表项
列表项的实现代码
实现完列表项的绘制代码FloatingItemPainter
类,你还需要一个画布CustomPaint
和事件逻辑。一个完整列表项类除了绘制代码外还需要补充绘制区域的定位,列表项手势方法的捕捉(关闭和点击事件,关闭动画的逻辑处理。对于定位,纵坐标是根据传进来的top
值决定的,对于列表项的Letf
值则是根据列表项位于左侧 / 右侧的,左侧很好理解就为0。而右侧的坐标,由于列表项的长度为width + 50.0
,因此列表项位于右侧时,横坐标为width - 50.0
,如下图:
对于关闭动画,则是对横坐标Left
取动画值来实现由中间收缩回边缘的动画效果。
对于事件的捕捉,需要确定当前列表项的点击区域和关闭区域。在事件处理的时候需要考虑较为极端的情况,就是把UI使用者不当正常人来看。正常的点击包括按下和抬起两个事件,但如果存在按下后拖拽出区域的情况呢?这时即使抬起后列表项还是处于选中的状态,还需要监听一个onTapCancel
的事件,当拖拽离开列表项监听区域时将列表项设为未选中状态。
FloatingItem
类的具体代码及解析如下:
/// [FloatingItem]一个单独功能完善的列表项类
对于ClickNotification
类,看一下代码:
import
它继承自Notification
,自定义了一个通知用于处理列表项点击或关闭时整个列表发生的变化。单个列表项在执行完关闭动画后分发通知,通知父级进行一个列表项上移填补被删除列表项位置的的动画。
列表动画
单个列表项的关闭动画,我们已经在FlotingItem
中实现了。而列表动画是,列表项关闭后,索引在其后的其他列表项向上平移填充的动画,示意图如下:
已知单个列表项的关闭动画是由自身管理实现的,那么单个列表项关闭后引起的列表动画由谁进行管理呢?自然是由列表进行管理。每个列表项除了原始的第一个列表项都可能会发生向上平移的动画,因此我们需要对单个的列表项再进行一层AnimatedWidget
的加装,方便动画的传入与管理,具体代码如下:
FloatingItemAnimatedWidget:
/// [FloatingItemAnimatedWidget] 列表项进行动画类封装,方便传入平移向上动画
代码中引用到了一个新类FloatingWindowSharedDataWidget
,它是一个InheritedWidget
,共享了FloatingWindowModel
类型的数据,FloatingWindowModel
中包括了悬浮窗用到的一些数据,例如判断列表在左侧或右侧的isLeft
,列表的数据dataList
等,避免了父组件向子组件传数据时大量参数的编写,一定程度上增强了可维护性,例如FloatingItemAnimatedWidget
中只需要传入索引值就可以在共享数据中提取到相应列表项的数据。FloatingWindowSharedDataWidget
和FloatingWindowModel
的代码及注释如下:
FloatingWindowSharedDataWidget
/// [FloatingWindowSharedDataWidget]悬浮窗数据共享Widget
FloatingWindowModel
/// [FloatingWindowModel] 表示悬浮窗共享的数据
列表的实现
上述已经实现了单个列表项并进行了动画的封装,现在只需要实现列表,监听列表项的点击和关闭事件并执行相应的操作。为了方便,我们实现了一个作为列表的FloatingItems
类然后实现了一个悬浮窗类TestWindow
来对列表的操作进行监听和管理,在以后的文章中还会继续完善TestWindow
类和FloatingWindowModel
类,把前两节的实现的FloatingButton
加进去并实现联动。目前的具体实现代码和注释如下:
FloatingItems
/// [FloatingItems] 列表
TestWindow
/// [TestWindow] 悬浮窗
main代码
void
总结
对于列表项的编写,难度就在于状态的管理上和动画的管理上,绘制上来来去去还是那几个函数。组件存在多个复杂动画,每个动画由谁进行管理,如何触发,状态量如何传递,都是需要认真思考才能解决的提出的解决方案,本篇文章采用了一个比较“原始”的方式进行实现,但能使对状态的管理和动画的管理有更深入的理解,在下篇文章中采用更为简单的方式进行实现,通过AnimatedList
即动画列表来实现。