Flutter中的自定义带渐变的圆弧进度条-带动画

2,887 阅读3分钟

简介

Flutter中自定义view及动画一直以来自觉比较难搞,这两天研究下版本雅思设计UI发现有一个圆弧进度条效果比较炫,所以拿来先用flutter实现一把。

效果图

image.png

上代码

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Gradient Arc Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ArcPage(),
    );
  }
}

class ArcPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Gradient Arc Example'),
      ),
      body: Center(
        child: Transform.rotate(
          angle: -(math.pi + math.pi / 4 + math.asin(22 * 0.5 / 100)),
          // 0,
          child: ArcView(),
        ),
      ),
    );
  }
}

class ArcView extends StatefulWidget {
  double width = 200;
  double height = 200;
  @override
  State<StatefulWidget> createState() {

    return ArcViewState();
  }
}

class ArcViewState extends State<ArcView> with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Curve curve;
  // double progress=0;
  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration:const Duration(seconds: 1),lowerBound:0,upperBound:60,vsync: this);
    controller.addListener(() {
      setState(() {
        print(controller.value);
      });
    });
    controller.forward();
  }
  @override
  void dispose() {
    super.dispose();
    controller.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: (){
        controller.forward(from: 0);
      },
      child: CustomPaint(
        painter: ArcPainter(
          strokeWidth: 22,
          strokeCap: StrokeCap.round,
          gradientColors: const [Color(0xFF54D6A7), Color(0xFF00CDDA)],
          bgColor: const Color(0xFFF3F4F6),
          progress: controller.value,
          roundColor: Colors.white,
          width: 200,//需要与widget.width一致
        ),
        size: Size(widget.width, widget.height), // 调整大小以适应你的需求
      ),
    );
  }
}

class ArcPainter extends CustomPainter {
  double width;

  ///圆弧粗细
  double strokeWidth;

  ///圆弧边缘显示样式
  StrokeCap strokeCap;

  ///进度条渐变色
  List<Color> gradientColors;

  ///背景颜色
  Color bgColor;

  ///百分比进度,自己内部转换为具体度数
  double progress;

  ///圆点颜色
  Color roundColor;

  ArcPainter(
      {required this.width,
      required this.strokeWidth,
      required this.strokeCap,
      required this.gradientColors,
      required this.bgColor,
      required this.progress,
      required this.roundColor});

  @override
  void paint(Canvas canvas, Size size) {
    final rect = Rect.fromLTWH(0, 0, size.width, size.height);

    /// 修正后的开始角度,弧度制
    final startAngle = math.asin(strokeWidth * 0.5 / (width / 2));

    /// 扫过的角度,弧度制
    final sweepAngle = (math.pi + math.pi / 2) * progress / 100;

    var gradient = SweepGradient(
      center: Alignment.center,
      colors: gradientColors, // 渐变颜色列表
      startAngle: 0, // 渐变起始位置
      endAngle: 2 * math.pi, // 渐变结束位置
    );

    final paint = Paint()
      ..shader = gradient.createShader(rect) // 使用渐变作为画笔的颜色
      ..style = PaintingStyle.stroke
      ..strokeCap = strokeCap
      ..strokeWidth = strokeWidth;
    final bgPaint = Paint()
      ..color = bgColor
      ..style = PaintingStyle.stroke
      ..strokeCap = strokeCap
      ..strokeWidth = strokeWidth;

    canvas.drawArc(rect, startAngle, math.pi + math.pi / 2, false, bgPaint);
    canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
    final double drawRadius = width / 2;
    final double center = width / 2;

    ///修正原点初始角度 90度
    var deg = degToRad(90) + startAngle + sweepAngle;
    final double dx = center + drawRadius * math.sin(deg);
    final double dy = center - drawRadius * math.cos(deg);
    Offset offsetCenter = Offset(dx, dy);
    final roundPaint = Paint()
      ..style = PaintingStyle.fill
      ..color = roundColor;
    // ..strokeWidth = (outerRadius - innerRadius);
    canvas.drawCircle(offsetCenter, strokeWidth / 2 - 2, roundPaint);
  }

  /// 度数转类似于π的那种角度
  double degToRad(double deg) => deg * (math.pi / 180.0);

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

技术点

画圆弧

canvas.drawArc(rect, startAngle, math.pi + math.pi / 2, false, bgPaint);

画圆弧基本上没什么难度,只要调用相关api就可以

圆弧顶点圆角设计

final paint = Paint()
      ..shader = gradient.createShader(rect) // 使用渐变作为画笔的颜色
      ..style = PaintingStyle.stroke
      ..strokeCap = strokeCap//注意这里设置成 StrokeCap.round
      ..strokeWidth = strokeWidth;

要让圆弧顶点变成圆角,只要在画圆弧的画笔设置StrokeCap.round即可,另外由于圆弧增加圆角,额外占用了圆环一部分空间,所以在设置圆弧渐变时此圆角所在会被绘制成渐变色末尾颜色。此处是坑,需留意。同样下面也给出了利用偏移角度,来解决此问题

圆弧渐变

var gradient = SweepGradient(
  center: Alignment.center,
  colors: gradientColors, // 渐变颜色列表
  startAngle: 0, // 渐变起始位置
  endAngle: 2 * math.pi, // 渐变结束位置
);

final paint = Paint()
  ..shader = gradient.createShader(rect) // 使用渐变作为画笔的颜色
  ..style = PaintingStyle.stroke
  ..strokeCap = strokeCap
  ..strokeWidth = strokeWidth;

圆弧渐变是比较难的一个东西,在不了解其他几个渐变模式下是非常难搞的。曾经一度以为圆环渐变的效果不能实现。后来经搜索得出,用此渐变效果能完美达到设计需要 此处endAngle需要进行仔细推敲,因为渐变结束位置有可能并不是2π。

画白点按钮

///修正原点初始角度 90度
var deg = degToRad(90) + startAngle + sweepAngle;
final double dx = center + drawRadius * math.sin(deg);
final double dy = center - drawRadius * math.cos(deg);
Offset offsetCenter = Offset(dx, dy);
final roundPaint = Paint()
  ..style = PaintingStyle.fill
  ..color = roundColor;
// ..strokeWidth = (outerRadius - innerRadius);
canvas.drawCircle(offsetCenter, strokeWidth / 2 - 2, roundPaint);

画白点按钮是需要一定数学功底的,它需要计算出当前小圆点的中心点坐标。才能准确画出白点位置。

注意

想要动画顺利执行,下面自定义view中的方法必须返回true或者根据实际需要返回。做这个的时候这个地方是默认false,我一度认为flutter 的setState失效。然后并不是,请主动避坑。

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