赞
踩
前期指路:
Flutter自绘组件:微信悬浮窗(一)
Flutter自绘组件:微信悬浮窗(二)
Flutter自绘组件:微信悬浮窗(三)
我们在系列第一、二篇文章中实现了悬浮窗的按钮形态,在系列第三篇文章中实现了悬浮窗的列表形态,现在需要做的就是把这两者有逻辑地结合起来。最终实现效果图对比如下:
两者之间的切换存在以下的逻辑:
当处于边缘按钮形态时,点击边缘按钮则切换至存在遮盖层的悬浮列表部分,且悬浮列表显示位置会根据当前悬浮按钮的位置进行显示,如果悬浮按钮位于屏幕下方且下方的长度不足以显示列表项,则将列表项置于悬浮按钮的上方进行显示。
当处于悬浮列表形态,点击遮盖层则会切换至按钮形态。
当处于悬浮列表形态时,关闭所有的悬浮列表项悬浮窗组件会隐形。
实现难点主要在于悬浮列表的动画控制。我们在系列第三篇实现悬浮列表项的时候,只实现了关闭动画,而没有实现入场动画。在边缘按钮形态切换至悬浮列表形态的时候使存在一个所有列表项从边缘往中心延伸的动画,而从悬浮列表形态切换至边缘按钮形态的时候存在一个所有列表项从中心往边缘缩减的动画,如何在实现这些逻辑的连接是难点。
我们在系列第三篇文章实现列表项关闭动画的时候是将关闭动画作为一个单独的动画且有列表项自身进行管理,由于我们此时存在了新的要求,所有列表项统一的进场和退场动画,我们可以把退场动画看作是进场动画的一个reverse
,当触发关闭事件的时候就是进行reverse
操作。由于父级状态的切换会触发子项的动画效果,因此所有列表项的进出场动画状态应该由父Widget
即FloatingWindow
来进行管理,我们在FloatingWindow
中使用一个变量isEntering
来管理列表项是否需要进行进场动画。因此我们需要对FloatingItem
的动画进行重写,我们可以把列表项的进场动画和关闭动画看作是同一个动画的不同形态,即forward
和reverse
。我们需要在_FloatingItemState
中定义一个静态共享变量animationControllers
,如下
/// [animationController] 所有列表项的动画控制器列表static List<AnimationController> animationControllers = [];
这个静态变量记录着所有列表项的动画控制器,这样在父级中就可以通过这个变量来对所有列表项的动画进行控制,达到所有列表项统一进场和出场的动画效果,为了更方便控制。在FloatingItem
中实现了以下静态方法用于控制动画:
- /// 全部列表项执行退场动画static void reverse(){
- for(int i = 0; i < _FloatingItemState.animationControllers.length; ++i) {
- if(!_FloatingItemState.animationControllers[i].toString().contains('DISPOSED')) _FloatingItemState.animationControllers[i].reverse();}}/// 全部列表项执行进场动画static void forward(){
- for(int i = 0; i < _FloatingItemState.animationControllers.length; ++i) {
- if(!_FloatingItemState.animationControllers[i].toString().contains('DISPOSED')) _FloatingItemState.animationControllers[i].forward();}}/// 每次更新时释放所有动画资源,清空动画控制器列表static void resetList(){
- for(int i = 0; i < _FloatingItemState.animationControllers.length;++i){
- if(!_FloatingItemState.animationControllers[i].toString().contains('DISPOSED')){
- _FloatingItemState.animationControllers[i].dispose();}} _FloatingItemState.animationControllers.clear(); _FloatingItemState.animationControllers = [];}
需要注意的是resetList
方法,这一方法中,每次组件由悬浮按钮和悬浮列表间变换的时候,会生成新的列表项,因此也会申请新动画资源,那么在申请新的动画资源时候,需要对旧的动画资源进行释放,避免造成内存泄漏。
- /// [FloatingItem]一个单独功能完善的列表项类class FloatingItem extends StatefulWidget {
- FloatingItem({
- @required this.top,@required this.isLeft,@required this.title,@required this.imageProvider,@required this.index,@required this.left,@required this.isEntering,this.width, Key key});/// [index] 列表项的索引值 int index;/// [top]列表项的y坐标值 double top;/// [left]列表项的x坐标值 double left;///[isLeft] 列表项是否在左侧,否则是右侧 bool isLeft;/// [title] 列表项的文字说明 String title;///[imageProvider] 列表项Logo的imageProvider ImageProvider imageProvider;///[width] 屏幕宽度的 1 / 2 double width;///[isEntering] 列表项是否触发进场动画 bool isEntering;@override _FloatingItemState createState() => _FloatingItemState();/// 全部列表项执行退场动画static void reverse(){
- for(int i = 0; i < _FloatingItemState.animationControllers.length; ++i) {
- if(!_FloatingItemState.animationControllers[i].toString().contains('DISPOSED')) _FloatingItemState.animationControllers[i].reverse();}}/// 全部列表项执行进场动画static void forward(){
- for(int i = 0; i < _FloatingItemState.animationControllers.length; ++i) {
- if(!_FloatingItemState.animationControllers[i].toString().contains('DISPOSED')) _FloatingItemState.animationControllers[i].forward();}}/// 每次更新时释放所有动画资源,清空动画控制器列表static void resetList(){
- for(int i = 0; i < _FloatingItemState.animationControllers.length;++i){
- if(!_FloatingItemState.animationControllers[i].toString().contains('DISPOSED')){
- _FloatingItemState.animationControllers[i].dispose();}} _FloatingItemState.animationControllers.clear(); _FloatingItemState.animationControllers = [];}}class _FloatingItemState extends State<FloatingItem> with TickerProviderStateMixin{
- /// [isPress] 列表项是否被按下 bool isPress = false;///[image] 列表项Logo的[ui.Image]对象,用于绘制Logo ui.Image image;/// [animationController] 列表关闭动画的控制器 AnimationController animationController;/// [animationController] 所有列表项的动画控制器列表static List<AnimationController> animationControllers = [];/// [animation] 列表项的关闭动画 Animation animation;@overridevoid initState() {
- // TODO: implement initState isPress = false;/// 获取Logo的ui.Image对象loadImageByProvider(widget.imageProvider).then((value) {
- setState(() {
- image = value;});});super.initState();}@override Widget build(BuildContext context) {
- return Positioned( left: widget.left, top: widget.top, child: GestureDetector(/// 监听按下事件,在点击区域内则将[isPress]设为true,若在关闭区域内则不做任何操作 onPanDown: (details) {
- if (widget.isLeft) {
- /// 点击区域内if (details.globalPosition.dx < widget.width) {
- setState(() {
- isPress = true;});}}else{
- /// 点击区域内if(details.globalPosition.dx < widget.width * 2 - 50){
- setState(() {
- isPress = true;});}}},/

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。