Flutter 学习6:绘制动画

Categories: flutter

这个是我学习Flutter的一个系列文章:

  1. Flutter 学习1:开发环境、开发工具、初始化一个项目
  2. Flutter 学习2:从main.dart文件说起
  3. Flutter 学习3:转场、导航
  4. Flutter 学习4:集成到原有的项目中
  5. Flutter 学习5:开发Dart包和插件包
  6. Flutter 学习6:绘制动画
  7. Flutter 学习7:Dart语言基础

前面文章写了关于Flutter插件的开发,最近写了一个仿照微信拍照功能的的开发包。中间那个拍照按钮有一个在拍摄视频的时候有一个动画效果。于是就学习了下关于Flutter动画绘制的过程。

绘制图形

在Flutter中关于绘图有一个专门的类:CustomPainter 。它是一个抽象类,需要实现两个方法:

@override
  void paint(Canvas canvas, Size size) {
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return null;
  }

看字面意思就能理解,一个是在画板上画图,一个是告诉它什么时候需要重新绘图。

这个CustomPainter是一个画板工具,不是一个widget,那它画出来的内容要展现出来,所以有一个专门的widget (CustomPaint)。CustomPaint有一个属性是painter,就是CustomPainter

绘制一个圆:

class DemoPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final double center = size.width * 0.5;
    final double radius = size.width * 0.5;
    // 画笔
    Paint paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.red;
    // 圆的中心点位置
    final Offset centerOffset = Offset(center, center);
    //画圆 canvas还有很多draw方法,可以画方 画圆弧 画线 画图片等等。。
    canvas.drawCircle(centerOffset, radius, paint);
  }

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

widget使用:

Container(
    width: 200,
    height: 200,
    color: Colors.teal,
    alignment: Alignment.center,
    child: CustomPaint(
    painter: DemoPainter(),
    size: Size(100, 100),
    )
 )

显示效果:

动画控制

Flutter有一个AnimationController动画控制器。这个控制器就是按照给定的时间按照线性生成从0到1的数字来控制动画一帧帧执行。 这个控制器还可以绑定一个时钟SingleTickerProviderStateMixin,可以控制Widget和动画执行同步进行,显示Widget的时候执行动画,不显示的时候暂停。 还是画上面那个圆,让它转360度把圆画出来。

class DemoPainter extends CustomPainter {
  final double progress;

  DemoPainter({this.progress});

  @override
  void paint(Canvas canvas, Size size) {
    final double center = size.width * 0.5;
    final double radius = size.width * 0.5;
    // 画笔
    Paint paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.red;
    // 圆的中心点位置
    final Offset centerOffset = Offset(center, center);
    // canvas.drawCircle(centerOffset, radius, paint);
    final Rect rect = Rect.fromCircle(center: centerOffset, radius: radius);
    final double startAngle = 0;
    final double angle = 360.0 * progress;
    final double sweepAngle = (angle * (pi / 180.0));
    // 画圆弧 按照角度来画圆弧,后面看效果图会发现起点从0开始画的时候是3点钟方向开始的
    canvas.drawArc(rect, startAngle, sweepAngle, true, paint);
  }

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

这里增加了一个progress进度参数,是控制器的value从0-1。然后因为是慢慢画出一个圆所以用了canvas.drawArc

控制器如何使用呢:

class _DemoViewState extends State<DemoView> with SingleTickerProviderStateMixin {
//定义一个控制器
AnimationController controller;

@override
void initState() {
  super.initState();
  //初始化控制器 duration是动画执行时间
  controller = AnimationController(duration: Duration(seconds: 2), vsync: this)
  //设置监听 有变化就刷新Widget
    ..addListener((){
    setState(() {});
  })
  ..repeat();
}

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

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Painter Demo'),
      ),
      body: Center(
        child: Container(
          width: 200,
          height: 200,
          color: Colors.teal,
          alignment: Alignment.center,
          child: CustomPaint(
            painter: DemoPainter(progress: controller.value),
            size: Size(100, 100),
          ),
        ),
      ),
    );
  }

}

效果:

微信拍视频按钮动画效果

文章开始说了要做一个类似微信拍照这样的功能控件,它上面的那个拍照按钮,按住就是拍视频,这时候这个按钮会有一个动画效果。 效果图:

这个效果看动画是分两段,第一段是按钮的底部半透明圆变大,中间白色的圆变小。第二段就是进度条绿色的进度条变化。按照这个思路所以我代码里面用了两个动画控制器:

//因为用了两个控制器所以这里要用TickerProviderStateMixin
class _DemoViewState extends State<DemoView> with TickerProviderStateMixin {
   AnimationController preController;
   AnimationController controller;
   
    @override
    void initState() {
      super.initState();
      //第一个控制器控制两个圆变大变小, 0.5秒内执行完成
      preController = AnimationController(duration: Duration(milliseconds: 500), vsync: this)
        ..addListener(() {
            setState(() {});
        })
        ..addStatusListener((status) {
          if (status == AnimationStatus.completed) {
            // 按钮过渡动画完成后启动录制视频的进度条动画
            controller.forward();
          }
        });
        //第二个控制器控制进度条,15秒内执行完成
      controller = AnimationController(duration: Duration(seconds: 15), vsync: this)
        ..addListener((){
        setState(() {});
      });
    }
    
    @override
    void dispose() {
        preController.dispose();
        controller.dispose();
        super.dispose();
    }
......
}

然后这个CustomPainter的代码就是画两个圆和一个进度条:

class CircleRecordPainter extends CustomPainter {
final double preProgress; //第一阶段控制器进度
  final double progress; //第二阶段控制器进度
  

  final Color buttonColor = Colors.white;
  final double progressWidth = 5;
  final Color progressColor = Colors.green;
  final back90 = degToRad(-90.0);//往前推90度 从12点钟方向开始
  
  Color progressBackgroundColor;
  Paint bottomPaint;
  Paint circlePaint;
  Paint progressPaint;
  

  CircleRecordPainter(this.preProgress, this.progress)
      {
      //初始化画笔
        progressBackgroundColor = buttonColor.withOpacity(0.7);
        bottomPaint  = Paint()
          ..style = PaintingStyle.fill
          ..color = progressBackgroundColor;
        circlePaint  = Paint()
          ..style = PaintingStyle.fill
          ..color = buttonColor;
        progressPaint  = Paint()
          ..style = PaintingStyle.stroke
          ..color = progressColor
          ..strokeWidth = progressWidth;
      }

  @override
  void paint(Canvas canvas, Size size) {
    // 底部最大的圆
    final double drawRadius = size.width * 0.5;
    final double center = size.width * 0.5;
    final Offset offsetCenter = Offset(center, center);
    final double bottomCircleRadius = drawRadius * (1 + (preProgress / 2));
    canvas.drawCircle(offsetCenter, bottomCircleRadius, bottomPaint);
    // 白色的圆
    final double circleRadiusStart = drawRadius - progressWidth;
    final double circleRadius = circleRadiusStart * (1 / (1 + preProgress));
    canvas.drawCircle(offsetCenter, circleRadius, circlePaint);
    // 进度条
    if (progress > 0) {
      final double angle = 360.0 * progress;
      final double sweepAngle = degToRad(angle);
      final double progressCircleRadius = bottomCircleRadius - progressWidth;
      final Rect arcRect = Rect.fromCircle(center: offsetCenter, radius: progressCircleRadius);
      //这里画弧度的时候它默认起点是从3点钟方向开始的所以这里的开始角度调整了90度让它从12点钟方向开始走进度
      canvas.drawArc(arcRect, back90, sweepAngle, false, progressPaint);
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

The End !