Flow CustomPainter 实现扇形菜单动画

43 阅读1分钟

videoGif.gif



import 'dart:math';
import 'dart:ui';

import 'package:flutter/material.dart';

class TestPage extends StatefulWidget {
  const TestPage({super.key});

  @override
  State<StatefulWidget> createState() => _TestPageState();

}

class _TestPageState extends State<TestPage> with SingleTickerProviderStateMixin{


  @override
  Widget build(BuildContext context) {

    return  Material(
      color: Colors.white,
      child: Center(
        child: Stack(
          children: [
            SectorWidget(180,15,items: [
              Container(
                width: 100,
                height: 20,
                alignment: Alignment.center,
                child: const Text("我是第一个",style: TextStyle(
                    fontSize: 10,
                    color: Colors.white
                ),),
              ),
              Container(
                width: 100,
                height: 20,
                alignment: Alignment.center,
                child: const Text("--------",style: TextStyle(
                    fontSize: 10,
                    color: Colors.white
                ),),
              ),
              Container(
                height: 20,
                width: 100,
                alignment: Alignment.center,
                child: const Text("我是第而个",style: TextStyle(
                    fontSize: 10,
                    color: Colors.white
                ),),
              ),
              Container(
                width: 100,
                height: 20,
                alignment: Alignment.center,
                child: const Text("--------",style: TextStyle(
                    fontSize: 10,
                    color: Colors.white
                ),),
              ),
              Container(
                width: 100,
                height: 20,
                alignment: Alignment.center,
                child: const Text("我是第三个",style: TextStyle(
                    fontSize: 10,
                    color: Colors.white
                ),),
              ),
              Container(
                width: 100,
                height: 20,
                alignment: Alignment.center,
                child: const Text("--------",style: TextStyle(
                    fontSize: 10,
                    color: Colors.white
                ),),
              ),
              Container(
                width: 100,
                height: 20,
                alignment: Alignment.center,
                child: const Text("我是第四个",style: TextStyle(
                    fontSize: 10,
                    color: Colors.white
                ),),
              )
            ],)
          ],
        ),
      ),
    );
  }
}

class SectorWidget extends StatefulWidget {

  final double size;

  final double smallRadius;

  final List<Widget> items; //size 为 4

  const SectorWidget(this.size,this.smallRadius,{required this.items,super.key});

  @override
  State<StatefulWidget> createState() => _SectorWidgetState();


}

class _SectorWidgetState extends State<SectorWidget> with SingleTickerProviderStateMixin{

  late AnimationController controller;
  late Animation<double> animation;
  ValueNotifier<bool> isExpand = ValueNotifier(false);

  @override
  void initState() {
    super.initState();
    controller = AnimationController(vsync: this,duration: const Duration(milliseconds: 300));
    animation = Tween(begin: 0.0, end: 1.0).animate(controller);

    controller.addListener(() {
      setState(() {

      });
    });

    controller.addStatusListener((status) {
      if(status == AnimationStatus.completed){

      }else if(status == AnimationStatus.dismissed){
        isExpand.value = false;
      }
    });

  }

  @override
  Widget build(BuildContext context) {

    return ValueListenableBuilder<bool>(
      valueListenable: isExpand,
      builder: (context,value,child){
        return  SizedBox(
            width: widget.size,
            height: widget.size,
            child: Stack(
              children: [
                if(isExpand.value)SizedBox(
                  width: widget.size,
                  height: widget.size,
                  child: CustomPaint(
                    painter: SectorCustomPainter(animation,widget.smallRadius),
                  ),
                ),
                if(isExpand.value) Flow(delegate: CusFlowDelegate(animation.value,widget.smallRadius + 70),children: widget.items),
                Positioned(bottom: 7,left: 7,child: GestureDetector(
                  onTap: (){
                    if(controller.isAnimating)return;
                    if(isExpand.value){
                      controller.reverse();
                    }else {
                      isExpand.value = true;
                      controller.forward();
                    }
                  },
                  child: Icon(Icons.add,size: 50,),
                ),),
              ],
            ));
      },
    );
  }

}

class CusFlowDelegate extends FlowDelegate {
  double percent;
  double itemTrans;
  CusFlowDelegate(this.percent,this.itemTrans);

  @override
  void paintChildren(FlowPaintingContext context) {
    var size = context.size;
    var childCount = context.childCount;

    var eachSweep = (pi / 2)/ (childCount + 1);
    for(int i= 0;i<childCount;i++){
      var childSize = context.getChildSize(i)!;
      var _paintTransform = Matrix4.identity()
        ..translate(0.0,size.height - childSize.height/2)
        ..rotateZ(- (i + 1) * eachSweep * percent )
        ..translate(itemTrans)
      ;
      context.paintChild(i,transform: _paintTransform);
    }

  }

  @override
  bool shouldRepaint(covariant CusFlowDelegate oldDelegate) {
    return percent != oldDelegate.percent;
  }
}


class SectorCustomPainter extends CustomPainter {
  Animation<double> animation;
  double smallRadius;
  SectorCustomPainter(this.animation,this.smallRadius):super(repaint: animation);
  @override
  void paint(Canvas canvas, Size size) {
    var clipPath = Path();
    clipPath.moveTo(0, 0);
    clipPath.addArc(Rect.fromLTRB(-size.width, 0, size.width, size.height * 2), 0, - pi /2 * animation.value);
    clipPath.lineTo(0, size.height);

    canvas.clipPath(clipPath);

    var path = Path();
    path.moveTo(0, 0);
    path.arcTo(Rect.fromLTRB(-size.width,0.0,size.width,size.height * 2), -pi/2, pi/2, true);
    path.lineTo(smallRadius, size.height);
    path.arcToPoint(Offset(0, size.height - smallRadius ),radius: Radius.circular(smallRadius),clockwise: true,largeArc: false);
    canvas.drawPath(path, Paint()
      ..shader = const LinearGradient(
        begin: Alignment.topCenter,
        end: Alignment.bottomCenter,
        colors: [Colors.red, Colors.orange],
      ).createShader(Rect.fromLTWH(0.0, 0.0, size.width, size.height)));
    
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {

    return false;
  }

}