Flutter 波浪圆形进度条

3,000 阅读4分钟

先上效果图

实现步骤

绘制进度条

  1. 先绘制绘制波浪
	    void drawWave(Canvas canvas, Offset center, double radius,
	        double waveOffsetPercent, Paint paint) {
	      double waveOffset = -(waveOffsetPercent * radius * 2);
		  //对画布进行圆形裁剪
	      canvas.save();
	      Path clipPath = Path()
	        ..addOval(Rect.fromCircle(center: center, radius: radius));
	      canvas.clipPath(clipPath);
	    
		  //表示出上图所示的point(上图中p)以及controlPoint(上图中c)
	      double waveProgressHeightY = (1 - percent) * radius * 2;
	      Offset point1 = Offset(waveOffset, waveProgressHeightY);
	      Offset point2 = Offset(waveOffset + radius, waveProgressHeightY);
	      Offset point3 = Offset(waveOffset + radius * 2, waveProgressHeightY);
	      Offset point4 = Offset(waveOffset + radius * 3, waveProgressHeightY);
	      Offset point5 = Offset(waveOffset + radius * 4, waveProgressHeightY);
	      Offset point6 = Offset(point5.dx, radius * 2 + halfWaveHeight);
	      Offset point7 = Offset(point1.dx, radius * 2 + halfWaveHeight);
	      Offset controlPoint1 =
	          Offset(waveOffset + radius * 0.5, waveProgressHeightY - halfWaveHeight);
	      Offset controlPoint2 =
	          Offset(waveOffset + radius * 1.5, waveProgressHeightY + halfWaveHeight);
	      Offset controlPoint3 =
	          Offset(waveOffset + radius * 2.5, waveProgressHeightY - halfWaveHeight);
	      Offset controlPoint4 =
	          Offset(waveOffset + radius * 3.5, waveProgressHeightY + halfWaveHeight);
		  //完成path的链接
	      Path wavePath = Path()
	        ..moveTo(point1.dx, point1.dy)
	        ..quadraticBezierTo(
	            controlPoint1.dx, controlPoint1.dy, point2.dx, point2.dy)
	        ..quadraticBezierTo(
	            controlPoint2.dx, controlPoint2.dy, point3.dx, point3.dy)
	        ..quadraticBezierTo(
	            controlPoint3.dx, controlPoint3.dy, point4.dx, point4.dy)
	        ..quadraticBezierTo(
	            controlPoint4.dx, controlPoint4.dy, point5.dx, point5.dy)
	        ..lineTo(point6.dx, point6.dy)
	        ..lineTo(point7.dx, point7.dy)
	        ..close();
		  //完成绘制
	      canvas.drawPath(wavePath, paint);
	      canvas.restore();
	    }
  1. 绘制层叠的波浪,跟第一步一样的绘制方法,可以将颜色与偏移值跟第一个波浪错开

  2. 绘制圆形进度内容,这一步需要注意的是在绘制进度的时候,需要对画布进行旋转,具体绘制内容如下

     void drawCircleProgress(
         Canvas canvas,
         Offset center,
         double radius,
         Size size) {
       //画进度条圆框背景
       canvas.drawCircle(center, radius, circleProgressBGPaint);
       //保存画布状态
       canvas.save();
       //逆时针旋转画布90度
       canvas.rotate(degreeToRadian(-90));
       canvas.translate(
           -(size.height + size.width) / 2, -(size.height - size.width) / 2);
       //画进度条圆框进度
       canvas.drawArc(
           Rect.fromCircle(center: center, radius: radius),
           degreeToRadian(0),
           degreeToRadian(percent * 360),
           false,
           circleProgressPaint);
       //恢复画布状态
       canvas.restore();
     }
    

绘制工作基本就差不多完成了

让波浪动起来

利用动画更改波浪的偏移值,并使动画不停的进行重复。两个波浪偏移的速度设置成不一致的,让波浪看起来更协调

	  void initState() {
	    super.initState();
	    waveAnimation = AnimationController(
	      vsync: this,
	      duration: widget.waveAnimationDuration,
	    );
	    waveAnimation.addListener(waveAnimationListener);
	    lightWaveAnimation = AnimationController(
	      vsync: this,
	      duration: widget.lightWaveAnimationDuration,
	    );
		//在lightWaveAnimationListener中获取波浪最新的偏移值,刷新状态
	    lightWaveAnimation.addListener(lightWaveAnimationListener);
	    waveAnimation.repeat();
	    lightWaveAnimation.repeat();
	  }

完成波浪动起来的效果之后基本上就差不多了,但是会有一个很明显的问题,那就是在设置进度之后,进度条中的波浪是瞬间涨上去,看上去非常的不协调,所以我们还需要给进度条加上一个,进度更改时的动画效果。

让波浪缓缓升起来降下去

	  @override
	  void initState() {
	    super.initState();
	    controller = AnimationController(
	        vsync: this, duration: widget.progressAnimatedDuration);
	    controller.addStatusListener((status) {
		  //当动画结束时重置动画
	      if (status == AnimationStatus.completed) {
	        progressAnimation.removeListener(handleProgressChange);
	        controller.reset();
	      }
	    });
	  }
	
	  @override
	  Widget build(BuildContext context) {
		//当进度发生改变时,开始动画
	    if (currentValue != widget.value && !controller.isAnimating) {
	      progressAnimation =
	          controller.drive(IntTween(begin: currentValue, end: widget.value));
	      progressAnimation.addListener(handleProgressChange);
	      controller.forward();
	    }
	    ...
	  }

在进度改变时,我们通过一个动画来改变进度到指定的进度,这样就保证了,进度发生改变时,波浪不会瞬间涨上去。

完整代码

import 'dart:ui';
import 'package:flutter/material.dart';
import 'dart:math';

class AnimatedWaveProgress extends StatefulWidget {
  final int value;
  final Duration progressAnimatedDuration;
  final Color circleProgressColor;
  final Color circleProgressBGColor;
  final Color waveColor;
  final Color lightWaveColor;
  final Color progressTextColor;
  final double circleProgressWidth;
  final double waveHeight;
  final Text hintText;
  final double progressTextFontSize;
  final Duration waveAnimationDuration;
  final Duration lightWaveAnimationDuration;
  final int maxValue;
  final int minValue;

  AnimatedWaveProgress({
    @required this.value,
    @required this.progressAnimatedDuration,
    @required this.circleProgressColor,
    @required this.circleProgressBGColor,
    @required this.waveColor,
    @required this.lightWaveColor,
    this.minValue = 0,
    this.maxValue = 100,
    this.hintText,
    this.progressTextColor = Colors.blueGrey,
    this.progressTextFontSize = 20,
    this.circleProgressWidth = 5,
    this.waveHeight = 50,
    this.waveAnimationDuration = const Duration(seconds: 4),
    this.lightWaveAnimationDuration = const Duration(seconds: 6),
  });

  @override
  _AnimatedWaveProgressState createState() => _AnimatedWaveProgressState();
}

class _AnimatedWaveProgressState extends State<AnimatedWaveProgress>
    with TickerProviderStateMixin {
  AnimationController controller;
  Animation<int> progressAnimation;
  int currentValue = 0;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
        vsync: this, duration: widget.progressAnimatedDuration);
    controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        progressAnimation.removeListener(handleProgressChange);
        controller.reset();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    if (currentValue != widget.value && !controller.isAnimating) {
      progressAnimation =
          controller.drive(IntTween(begin: currentValue, end: widget.value));
      progressAnimation.addListener(handleProgressChange);
      controller.forward();
    }
    return WaveProgress(
      value: currentValue,
      circleProgressColor: widget.circleProgressColor,
      circleProgressBGColor: widget.circleProgressBGColor,
      waveColor: widget.waveColor,
      lightWaveColor: widget.lightWaveColor,
      hintText: widget.hintText,
      progressTextColor: widget.progressTextColor,
      circleProgressWidth: widget.circleProgressWidth,
      lightWaveAnimationDuration: widget.lightWaveAnimationDuration,
      maxValue: widget.maxValue,
      minValue: widget.minValue,
      progressTextFontSize: widget.progressTextFontSize,
      waveAnimationDuration: widget.waveAnimationDuration,
      waveHeight: widget.waveHeight,
    );
  }

  void handleProgressChange() {
    setState(() {
      currentValue = progressAnimation.value;
    });
  }
}

class WaveProgress extends StatefulWidget {
  final Color circleProgressColor;
  final Color circleProgressBGColor;
  final Color waveColor;
  final Color lightWaveColor;
  final Color progressTextColor;
  final double circleProgressWidth;
  final double waveHeight;
  final Text hintText;
  final double progressTextFontSize;
  final Duration waveAnimationDuration;
  final Duration lightWaveAnimationDuration;
  final int maxValue;
  final int minValue;
  final int value;

  @override
  _WaveProgressState createState() => _WaveProgressState();

  WaveProgress({
    @required value,
    @required this.circleProgressColor,
    @required this.circleProgressBGColor,
    @required this.waveColor,
    @required this.lightWaveColor,
    this.minValue = 0,
    this.maxValue = 100,
    this.hintText,
    this.progressTextColor = Colors.blueGrey,
    this.progressTextFontSize = 20,
    this.circleProgressWidth = 5,
    this.waveHeight = 50,
    this.waveAnimationDuration = const Duration(seconds: 4),
    this.lightWaveAnimationDuration = const Duration(seconds: 6),
  }) : this.value = value.clamp(minValue, maxValue);
}

class _WaveProgressState extends State<WaveProgress>
    with TickerProviderStateMixin<WaveProgress> {
  AnimationController waveAnimation;
  AnimationController lightWaveAnimation;

  @override
  void initState() {
    super.initState();
    waveAnimation = AnimationController(
      vsync: this,
      duration: widget.waveAnimationDuration,
    );
    waveAnimation.addListener(waveAnimationListener);
    lightWaveAnimation = AnimationController(
      vsync: this,
      duration: widget.lightWaveAnimationDuration,
    );
    lightWaveAnimation.addListener(lightWaveAnimationListener);
    waveAnimation.repeat();
    lightWaveAnimation.repeat();
  }

  @override
  void dispose() {
    waveAnimation?.removeListener(waveAnimationListener);
    waveAnimation?.removeListener(lightWaveAnimationListener);
    waveAnimation?.dispose();
    lightWaveAnimation?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    double percent = widget.value.toDouble() / widget.maxValue;
    percent = percent.clamp(0, 1);
    List<Widget> children = <Widget>[
      CustomPaint(
        painter: WaveProgressPainter(
          circleProgressWidth: widget.circleProgressWidth,
          circleProgressBGColor: widget.circleProgressBGColor,
          waveColor: widget.waveColor,
          lightWaveOffsetPercent: lightWaveAnimation.value,
          percent: percent,
          lightWaveColor: widget.lightWaveColor,
          waveOffsetPercent: waveAnimation.value,
          circleProgressColor: widget.circleProgressColor,
          waveHeight: widget.waveHeight,
        ),
        willChange: true,
        isComplex: true,
        size: Size.infinite,
      ),
      Center(
        child: Text(
          '${(percent * 100).ceil()}%',
          style: TextStyle(
              color: widget.progressTextColor,
              fontSize: widget.progressTextFontSize),
        ),
      ),
    ];
    if (widget.hintText != null) {
      children.add(Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          widget.hintText,
          SizedBox.fromSize(
            size: Size.fromHeight(100),
          ),
        ],
      ));
    }
    return Stack(
      alignment: Alignment.center,
      children: children,
    );
  }

  void waveAnimationListener() {
    setState(() {});
  }

  void lightWaveAnimationListener() {}
}

class WaveProgressPainter extends CustomPainter {
  double circleProgressWidth;
  double percent = 0.5;
  Color circleProgressColor;
  Color circleProgressBGColor;
  Color waveColor;
  Color lightWaveColor;
  Paint circleProgressBGPaint;
  Paint circleProgressPaint;
  Paint wavePaint;
  Paint lightWavePaint;

  //波浪的高度(波峰与波谷的差值)
  double waveHeight;

  //波浪横向的偏移百分比
  double waveOffsetPercent;

  //浅色波浪横向的偏移百分比
  double lightWaveOffsetPercent;

  double get halfWaveHeight => waveHeight / 2;

  WaveProgressPainter({
    @required this.percent,
    @required this.circleProgressColor,
    @required this.circleProgressBGColor,
    @required this.waveColor,
    @required this.lightWaveColor,
    @required this.waveOffsetPercent,
    @required this.lightWaveOffsetPercent,
    this.circleProgressWidth = 5,
    this.waveHeight = 60,
  }) {
    percent = percent.clamp(0, 1);
    circleProgressBGPaint = Paint()
      ..color = circleProgressBGColor
      ..strokeWidth = circleProgressWidth
      ..style = PaintingStyle.stroke
      ..isAntiAlias = true;
    circleProgressPaint = Paint()
      ..color = circleProgressColor
      ..strokeWidth = circleProgressWidth
      ..style = PaintingStyle.stroke
      ..isAntiAlias = true;
    wavePaint = Paint()
      ..color = waveColor
      ..style = PaintingStyle.fill
      ..isAntiAlias = true;
    lightWavePaint = Paint()
      ..color = lightWaveColor
      ..style = PaintingStyle.fill
      ..isAntiAlias = true;
  }

  @override
  void paint(Canvas canvas, Size size) {
    Offset center = size.center(Offset(0, 0));
    double radius = size.shortestSide / 2;
    //绘制浅色波浪
    drawWave(canvas, center, radius, lightWaveOffsetPercent, lightWavePaint);
    //绘制深色波浪
    drawWave(canvas, center, radius, waveOffsetPercent, wavePaint);
    drawCircleProgress(canvas, center, radius, size);
  }

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

  void drawCircleProgress(
      Canvas canvas,
      Offset center,
      double radius,
      Size size) {
    //画进度条圆框背景
    canvas.drawCircle(center, radius, circleProgressBGPaint);
    //保存画布状态
    canvas.save();
    //逆时针旋转画布90度
    canvas.rotate(degreeToRadian(-90));
    canvas.translate(
        -(size.height + size.width) / 2, -(size.height - size.width) / 2);
    //画进度条圆框进度
    canvas.drawArc(
        Rect.fromCircle(center: center, radius: radius),
        degreeToRadian(0),
        degreeToRadian(percent * 360),
        false,
        circleProgressPaint);
    //恢复画布状态
    canvas.restore();
  }

  ///角度转弧度
  num degreeToRadian(num deg) => deg * (pi / 180.0);

  ///弧度转角度
  num radianToDegree(num rad) => rad * (180.0 / pi);

  void drawWave(Canvas canvas, Offset center, double radius,
      double waveOffsetPercent, Paint paint) {
    double waveOffset = -(waveOffsetPercent * radius * 2);
    canvas.save();
    Path clipPath = Path()
      ..addOval(Rect.fromCircle(center: center, radius: radius));
    canvas.clipPath(clipPath);

    double waveProgressHeightY = (1 - percent) * radius * 2;
    Offset point1 = Offset(waveOffset, waveProgressHeightY);
    Offset point2 = Offset(waveOffset + radius, waveProgressHeightY);
    Offset point3 = Offset(waveOffset + radius * 2, waveProgressHeightY);
    Offset point4 = Offset(waveOffset + radius * 3, waveProgressHeightY);
    Offset point5 = Offset(waveOffset + radius * 4, waveProgressHeightY);
    Offset point6 = Offset(point5.dx, radius * 2 + halfWaveHeight);
    Offset point7 = Offset(point1.dx, radius * 2 + halfWaveHeight);
    Offset controlPoint1 =
        Offset(waveOffset + radius * 0.5, waveProgressHeightY - halfWaveHeight);
    Offset controlPoint2 =
        Offset(waveOffset + radius * 1.5, waveProgressHeightY + halfWaveHeight);
    Offset controlPoint3 =
        Offset(waveOffset + radius * 2.5, waveProgressHeightY - halfWaveHeight);
    Offset controlPoint4 =
        Offset(waveOffset + radius * 3.5, waveProgressHeightY + halfWaveHeight);
    Path wavePath = Path()
      ..moveTo(point1.dx, point1.dy)
      ..quadraticBezierTo(
          controlPoint1.dx, controlPoint1.dy, point2.dx, point2.dy)
      ..quadraticBezierTo(
          controlPoint2.dx, controlPoint2.dy, point3.dx, point3.dy)
      ..quadraticBezierTo(
          controlPoint3.dx, controlPoint3.dy, point4.dx, point4.dy)
      ..quadraticBezierTo(
          controlPoint4.dx, controlPoint4.dy, point5.dx, point5.dy)
      ..lineTo(point6.dx, point6.dy)
      ..lineTo(point7.dx, point7.dy)
      ..close();
    canvas.drawPath(wavePath, paint);
    canvas.restore();
  }
}