[-Flutter 自组篇-] 圆形进度条

5,512 阅读3分钟

今天写个简单的,自定义一个圆形进度条,并且加上小箭头指向内圈进度。
进度条已上传到公网,使用circle_progress: ^0.0.1,使用如下

void main() => runApp(MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: Scaffold(
        appBar: AppBar(
          title: Text("Flutter 之旅"),
        ),
        body: TestStateful() //内置案例
    )
));


1.准备阶段

1.1:定义描述对象类Progress

将需要变化的属性抽离出一个描述类,传参方便些

///信息描述类 [value]为进度,在0~1之间,进度条颜色[color],
///未完成的颜色[backgroundColor],圆的半径[radius],线宽[strokeWidth]
///小点的个数[dotCount] 样式[style] 完成后的显示文字[completeText]
class Progress {
  double value;
  Color color;
  Color backgroundColor;
  double radius;
  double strokeWidth;
  int dotCount;
  TextStyle style;
  String completeText;

  Progress({this.value,
      this.color,
      this.backgroundColor,
      this.radius,
      this.strokeWidth,
      this.completeText="OK",
       this.style,
      this.dotCount = 40
      });
}

1.2:自定义组件类CircleProgressWidget

这便是我们的组件

class CircleProgressWidget extends StatefulWidget {
  final Progress progress;
  CircleProgressWidget({Key key, this.progress}) : super(key: key);
  @override
  _CircleProgressWidgetState createState() => _CircleProgressWidgetState();
}

class _CircleProgressWidgetState extends State<CircleProgressWidget> {
  @override
  Widget build(BuildContext context) {
  
    return Container();
  }
}

1.3:自定义ProgressPainter

我们的绘制逻辑在这里进行

class ProgressPainter extends CustomPainter {
  Progress _progress;
  Paint _paint;
  Paint _arrowPaint;//箭头的画笔
  Path _arrowPath;//箭头的路径
  double _radius;//半径

  ProgressPainter(
    this._progress,
  ) {
    _arrowPath=Path();
    _arrowPaint=Paint();
    _paint = Paint();
    _radius = _progress.radius - _progress.strokeWidth / 2;
  }

  @override
  void paint(Canvas canvas, Size size) {
    Rect rect = Offset.zero & size;
    canvas.clipRect(rect); //裁剪区域
  }

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

2.绘制

2.1:绘制进度条

如果直接用给定的半径,你会发现是这样的。原因很简单,因为Canvas画圆半径是内圆加一半线粗。
于是我们需要校正一下半径:通过平移一半线粗再缩小一半线粗的半径。

_radius = _progress.radius - _progress.strokeWidth / 2;

 @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(_progress.strokeWidth / 2, _progress.strokeWidth / 2);

背景直接画圆,进度使用drawArc方法,要注意的是Flutter中使用的是弧度!!。

drawProgress(Canvas canvas) {
  canvas.save();
  _paint//背景
    ..style = PaintingStyle.stroke
    ..color = _progress.backgroundColor
    ..strokeWidth = _progress.strokeWidth;
  canvas.drawCircle(Offset(_radius, _radius), _radius, _paint);
  
  _paint//进度
    ..color = _progress.color
    ..strokeWidth = _progress.strokeWidth * 1.2
    ..strokeCap = StrokeCap.round;
  double sweepAngle = _progress.value * 360; //完成角度
  canvas.drawArc(Rect.fromLTRB(0, 0, _radius * 2, _radius * 2),
      -90 / 180 * pi, sweepAngle / 180 * pi, false, _paint);
  canvas.restore();
}

2.2:绘制箭头

其实箭头还是蛮好画的,注意relativeLineTo和lineTo结合使用,可能会更方便。

drawArrow(Canvas canvas) {
  canvas.save();
  canvas.translate(_radius, _radius);
  canvas.rotate((180 + _progress.value * 360) / 180 * pi);
  var half = _radius / 2;
  var eg = _radius / 50; //单位长
  _arrowPath.moveTo(0, -half - eg * 2);//1
  _arrowPath.relativeLineTo(eg * 2, eg * 6);//2
  _arrowPath.lineTo(0, -half + eg * 2);//3
  _arrowPath.lineTo(0, -half - eg * 2);//1
  _arrowPath.relativeLineTo(-eg * 2, eg * 6);
  _arrowPath.lineTo(0, -half + eg * 2);
  _arrowPath.lineTo(0, -half - eg * 2);
  canvas.drawPath(_arrowPath, _arrowPaint);
  canvas.restore();
}

2.3:绘制点

绘制点的时候要注意颜色的把控,判断进度条是否到达,然后更改颜色

void drawDot(Canvas canvas) {
  canvas.save();
  int num = _progress.dotCount;
  canvas.translate(_radius, _radius);
  for (double i = 0; i < num; i++) {
    canvas.save();
    double deg = 360 / num * i;
    canvas.rotate(deg / 180 * pi);
    _paint
      ..strokeWidth = _progress.strokeWidth / 2
      ..color = _progress.backgroundColor
      ..strokeCap = StrokeCap.round;
    if (i * (360 / num) <= _progress.value * 360) {
      _paint..color = _progress.color;
    }
    canvas.drawLine(
        Offset(0, _radius * 3 / 4), Offset(0, _radius * 4 / 5), _paint);
    canvas.restore();
  }
  canvas.restore();
}

2.4:拼装

也许你会问Text呢?在Canvas里画Text好麻烦,这里用一个Stack包一下挺方便的

class _CircleProgressWidgetState extends State<CircleProgressWidget> {
  @override
  Widget build(BuildContext context) {
    var progress = Container(
      width: widget.progress.radius * 2,
      height: widget.progress.radius * 2,
      child: CustomPaint(
        painter: ProgressPainter(widget.progress),
      ),
    );
    String txt = "${(100 * widget.progress.value).toStringAsFixed(1)} %";
    var text = Text(
      widget.progress.value == 1.0 ? widget.progress.completeText : txt,
      style: widget.progress.style ??
          TextStyle(fontSize: widget.progress.radius / 6),
    );
    return Stack(
      alignment: Alignment.center,
      children: <Widget>[progress,text],
    );
  }
}

OK,这样就可以了。


3.使用

3.1:简单使用
CircleProgressWidget(
        progress: Progress(
            backgroundColor: Colors.grey,
            value: 0.8,
            radius: 100,
            completeText: "完成",
            color: Color(0xff46bcf6),
            strokeWidth: 4))

3.2:组件代码全览

拿去用吧,谢谢,不送。

import 'dart:math';

import 'package:flutter/material.dart';

class CircleProgressWidget extends StatefulWidget {
  final Progress progress;

  CircleProgressWidget({Key key, this.progress}) : super(key: key);

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

///信息描述类 [value]为进度,在0~1之间,进度条颜色[color],
///未完成的颜色[backgroundColor],圆的半径[radius],线宽[strokeWidth]
///小点的个数[dotCount] 样式[style] 完成后的显示文字[completeText]
class Progress {
  double value;
  Color color;
  Color backgroundColor;
  double radius;
  double strokeWidth;
  int dotCount;
  TextStyle style;
  String completeText;

  Progress(
      {this.value,
      this.color,
      this.backgroundColor,
      this.radius,
      this.strokeWidth,
      this.completeText = "OK",
      this.style,
      this.dotCount = 40});
}

class _CircleProgressWidgetState extends State<CircleProgressWidget> {
  @override
  Widget build(BuildContext context) {
    var progress = Container(
      width: widget.progress.radius * 2,
      height: widget.progress.radius * 2,
      child: CustomPaint(
        painter: ProgressPainter(widget.progress),
      ),
    );
    String txt = "${(100 * widget.progress.value).toStringAsFixed(1)} %";
    var text = Text(
      widget.progress.value == 1.0 ? widget.progress.completeText : txt,
      style: widget.progress.style ??
          TextStyle(fontSize: widget.progress.radius / 6),
    );
    return Stack(
      alignment: Alignment.center,
      children: <Widget>[progress,text],
    );
  }
}

class ProgressPainter extends CustomPainter {
  Progress _progress;
  Paint _paint;
  Paint _arrowPaint;
  Path _arrowPath;
  double _radius;

  ProgressPainter(
    this._progress,
  ) {
    _arrowPath = Path();
    _arrowPaint = Paint();
    _paint = Paint();
    _radius = _progress.radius - _progress.strokeWidth / 2;
  }

  @override
  void paint(Canvas canvas, Size size) {
    Rect rect = Offset.zero & size;
    canvas.clipRect(rect); //裁剪区域
    canvas.translate(_progress.strokeWidth / 2, _progress.strokeWidth / 2);

    drawProgress(canvas);
    drawArrow(canvas);
    drawDot(canvas);
  }

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

  drawProgress(Canvas canvas) {
    canvas.save();
    _paint//背景
      ..style = PaintingStyle.stroke
      ..color = _progress.backgroundColor
      ..strokeWidth = _progress.strokeWidth;
    canvas.drawCircle(Offset(_radius, _radius), _radius, _paint);

    _paint//进度
      ..color = _progress.color
      ..strokeWidth = _progress.strokeWidth * 1.2
      ..strokeCap = StrokeCap.round;
    double sweepAngle = _progress.value * 360; //完成角度
    print(sweepAngle);
    canvas.drawArc(Rect.fromLTRB(0, 0, _radius * 2, _radius * 2),
        -90 / 180 * pi, sweepAngle / 180 * pi, false, _paint);
    canvas.restore();
  }

  drawArrow(Canvas canvas) {
    canvas.save();
    canvas.translate(_radius, _radius);// 将画板移到中心
    canvas.rotate((180 + _progress.value * 360) / 180 * pi);//旋转相应角度
    var half = _radius / 2;//基点
    var eg = _radius / 50; //单位长
    _arrowPath.moveTo(0, -half - eg * 2);
    _arrowPath.relativeLineTo(eg * 2, eg * 6);
    _arrowPath.lineTo(0, -half + eg * 2);
    _arrowPath.lineTo(0, -half - eg * 2);
    _arrowPath.relativeLineTo(-eg * 2, eg * 6);
    _arrowPath.lineTo(0, -half + eg * 2);
    _arrowPath.lineTo(0, -half - eg * 2);
    canvas.drawPath(_arrowPath, _arrowPaint);
    canvas.restore();
  }

  void drawDot(Canvas canvas) {
    canvas.save();
    int num = _progress.dotCount;
    canvas.translate(_radius, _radius);
    for (double i = 0; i < num; i++) {
      canvas.save();
      double deg = 360 / num * i;
      canvas.rotate(deg / 180 * pi);
      _paint
        ..strokeWidth = _progress.strokeWidth / 2
        ..color = _progress.backgroundColor
        ..strokeCap = StrokeCap.round;
      if (i * (360 / num) <= _progress.value * 360) {
        _paint..color = _progress.color;
      }
      canvas.drawLine(
          Offset(0, _radius * 3 / 4), Offset(0, _radius * 4 / 5), _paint);
      canvas.restore();
    }
    canvas.restore();
  }
}

结语

本文到此接近尾声了,如果想快速尝鲜Flutter,《Flutter七日》会是你的必备佳品;如果想细细探究它,那就跟随我的脚步,完成一次Flutter之旅。
另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,本人微信号:zdl1994328,期待与你的交流与切磋。