zl程序教程

您现在的位置是:首页 >  工具

当前栏目

flutter圆形动画菜单,Flow流式布局动画圆形菜单

动画flutter 布局 菜单 Flow 圆形 流式
2023-09-11 14:14:53 时间

题记
—— 执剑天涯,从你的点滴积累开始,所及之处,必精益求精,即是折腾每一天。

重要消息


最终实现的效果如图1所示。
在这里插入图片描述
对于上述这种圆形动态菜单,可通过以下几步来实现:
第一步就是构建圆形菜单的数据,代码如下:

 ///   流式布局 Flow 圆形菜单
  ///构建菜单所使用到的图标
  List<Icon> iconList =[
    Icon(Icons.add,color: Colors.white,),
    Icon(Icons.wallpaper,color: Colors.white,),
    Icon(Icons.message,color: Colors.white,),
    Icon(Icons.share,color: Colors.white,),
    Icon(Icons.home,color: Colors.white,),
  ];

  //lib/code10/main_data1005.dart
  /// Flow 流式布局 构建菜单数据Widget
  List<Widget>  buildTestData(){
    List<Widget> childWidthList = [];
    for (int i = 0; i < iconList.length; i++) {
      ///每个菜单添加InkWell点击事件
      Widget itemContainer = InkWell(onTap: (){
        ///打开或者关闭菜单
        colseOrOpen();
        ///点击菜单其他的操作
      },child:new Container(
        ///圆形背景
        decoration: BoxDecoration(
            color: Colors.blue[300],
            borderRadius: BorderRadius.all(Radius.circular(23))
        ),
        alignment: Alignment.center, height: 44, width: 44,
        child: iconList[i],
      ),);
      childWidthList.add(itemContainer,);
    }
    return childWidthList;
  }

在这里笔者使用的是Icon,读者在实际项目开发中,可灵活应用,如配置成Image或者是其他的Widget。
第二步就是构建页面的主布局,一般用到这种圆形菜单的方式,是放在页面的右下角的,在这里通过Stack层叠布局来构建,代码如下:

  ///页面的主体
  buildBody2() {
    return Scaffold(
      appBar: AppBar(
        title: Text("流式布局"),
      ),
      body: Stack(children: [
        Flow(
          ///代理
          delegate: TestFlowDelegate(radiusRate: _rad),
          ///使用到的子Widget
          children: buildTestData(),
        )
        ],),
    );
  }

这里通过Flow来实现动态圆形菜单的,children就是在第一步创建的菜单数据,delegate配置的是自定义的FlowDelegate用来排版子Widget的。
第三步就是对TestFlowDelegate来自定义排版的实现,继承于FlowDelegate,代码如下:

///  流式布局 Flow 计算
class TestFlowDelegate extends FlowDelegate {

  ///菜单的内边距
  EdgeInsets padding;
  ///菜单展开的初始角度 (弧度)
  double initAngle;
  ///半径变化的比率
  ///一般从0到1 菜单展开
  ///从1-0菜单关闭
  double radiusRate;

  TestFlowDelegate({this.radiusRate=0, this.padding=EdgeInsets.zero, this.initAngle=0});

  @override
  void paintChildren(FlowPaintingContext context) {
    calculWrapSpacingChild2(context);
  }


  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return oldDelegate != this;
  }

  //  是否需要重新布局。
  @override
  bool shouldRelayout(FlowDelegate oldDelegate) {
    return true;
  }
  //设置Flow的尺寸
  @override
  Size getSize(BoxConstraints constraints) {
    //指定Flow的大小
    return super.getSize(constraints);
  }

  //  设置每个child的布局约束条件
  @override
  BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
    return super.getConstraintsForChild(i, constraints);
  }
}

对于排版子Widget的关键就是计算每个子Widget的坐标了,代码如下:

  ///  流式布局 Flow 计算
  void calculWrapSpacingChild2(FlowPaintingContext context) {
    ///初始绘制位置为Flow布局的左上角
    double x = 0.0;
    double y = 0.0;

    //获取当前画布的最小边长,width与height谁小取谁
    double radius = context.size.shortestSide;

    ///默认将所有的子Widget绘制到左下角
    x = radius;
    y= radius;

    ///角度
    double a = 0.5/(context.childCount-1)*4;

    //计算每一个子widget的位置
    for (var i = 0; i < context.childCount-1; i++) {
      ///获取第i个子Widget的大小
      Size itemChildSize =  context.getChildSize(i);
      ///计算每个子Widget 的坐标
      x= context.size.width -itemChildSize.width*1.4- cos(a*i+initAngle)*radius/3*radiusRate;
      y= context.size.height - itemChildSize.height*2 - sin(a*i+initAngle)*radius/3*radiusRate;
      ///在Flow中进行绘制
      context.paintChild(i, transform: new Matrix4.translationValues(x, y, 0.0));
    }

    ///最后一个做为菜单选项
    int lastIndex = context.childCount-1;
    Size lastChildSize= context.getChildSize(lastIndex);
    double lastx= context.size.width -lastChildSize.width*1.4;
    double lasty= context.size.height - lastChildSize.height*2;
    ///绘制这个菜单在左下角
    context.paintChild(lastIndex, transform: new Matrix4.translationValues(lastx, lasty, 0.0));

  }

在这里使用到了radiusRate,默认为0,此时子Widget的坐标计算可简化成如下所示:

///计算每个子Widget 的坐标
x= context.size.width -itemChildSize.width*1.4;

y= context.size.height - itemChildSize.height*2;

在页面刚刚创建的时候,菜单还未打开时,radiusRate的值为0,context.size是获取的当前Flow的大小,而此时创建的Flow是填充屏幕的,通过计算此时所有的菜单图标全部位置页面的右下角,下图所分析。
在这里插入图片描述
当radiusRate的值通过一定时间从0变化到1时,在上述的基础上,对于子Widget在x方向上又减去了一个cos值,就相当于是向右移动了,radiusRate不断变大,对于最终计算出来的x坐标是不变变小的。

cos(a*i+initAngle)*radius/3*radiusRate;

对于y值减去了一个sin值,radiusRate不断变大,对于最终计算出来的y坐标是不变变小,然后就是不断向上移动,代码如下:

sin(a*i+initAngle)*radius/3*radiusRate

x与y每次变化的值不一样,所以就呈现出了一个动态圆形的变换过程,如下图所分析的sin与cos坐标计算。
在这里插入图片描述
最后一步就是通过动画控制器来控制菜单的打开与结束的从0-1再从1-0的变化过程,首先来看创建动画控制器的创建,一般用在初始化函数中,代码如下:

///   流式布局 Flow 圆形菜单
class _PageState extends State     with SingleTickerProviderStateMixin {

  ///动画控制器
  AnimationController _controller;
  ///变化比率
  double _rad = 0.0;
  ///是否执行完动画,或者说是动画停止
  bool _closed = true;

  @override
  void initState() {
    super.initState();
    ///创建动画控制器
    ///执行时间为200毫秒
    _controller =
    AnimationController(duration: Duration(milliseconds: 200), vsync: this)
    ///设置监听,每当动画执行时就会实时回调此方法
      ..addListener((){
        setState(() {
          ///从0到1
          _rad =_controller.value;
          print("$_rad ");
        });

      })
      ///设置状态监听
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          print("正向执行完毕 ");
          _closed = !_closed;
        }else if(status == AnimationStatus.dismissed){
          print("反向执行完毕 ");
          _closed = !_closed;
        }

      });

  }
... 省略

}

然后就是动画控制的开始与结束,代码如下:

  ///控制菜单的打开或者关闭
  void colseOrOpen() {
    if (_closed) {
      _controller.reset();
      _controller.forward();
    } else {
      _controller.reverse();
    }
  }

最后就是退出页面时,释放资源,代码如下:

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

完毕

在这里插入图片描述
在这里插入图片描述