flutter 自定义圆形进度条

原文是谁写的我也不知道,但是搜了这么多,没一个能用的,大多数的都没实时刷新,在自己的debug和学习下,进行了改进,这次可以直接用了xdm

学习的笔记也记录在这里了

绘制需要的要素
1.纸: Canvas 画布对象
2.笔: Paint 画笔对象
3.形: Path 路径对象
4.色: Color 颜色对象

使用StreamController来进行实时的局部刷新

效果图:
在这里插入图片描述

程序主入口

void main() => runApp(MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: Scaffold(
        appBar: AppBar(
          title: Text("Flutter 之旅"),
        ),
        body: CircleProgressWidget(
            progress: Progress(
                backgroundColor: Colors.grey,
                value: 0.0,
                radius: 100,
                completeText: "完成",
                color: Color(0xff46bcf6),
                strokeWidth: 4)
        ), 
    )
));

1.定义描述对象类Progress

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自定义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) { 
  //这个是我们定义painter时必须实现的方法,其中canvas就是提供出我们绘制的核心,
  //size是告诉我们画板的大小(通过CustomPaint的size或者child确定)
    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中使用的是弧度!!。

save操作会保存此前的所有绘制内容和canvas状态。在调用该函数之后的绘制操作和变换操作,会重新记录。当你调用restore()之后,会把save到restore之间所进行的操作与之前的内容进行合并,之后再使用局部更新的StreamController来更新状态

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();
}

3.组装使用

使用_streamController来更新进度条状态

class CircleProgressWidget extends StatefulWidget {
  final Progress progress;

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

  @override
  _CircleProgressWidgetState createState() => _CircleProgressWidgetState(this.progress);
}

class _CircleProgressWidgetState extends State<CircleProgressWidget> {

  Progress progress;
  _CircleProgressWidgetState(this.progress);

  ///计时器
  Timer _timer;
  ///倒计时6秒
  double totalTimeNumber = 10000;
  ///当前的时间
  double currentTimeNumber = 10000;
  StreamController<double> _streamController = StreamController();

  @override
  void initState(){
    startTimer();
  }

  @override
  void dispose(){
    _streamController.close();
    _timer.cancel();
  }

  void startTimer() {
    ///间隔100毫秒执行时间
    _timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
      ///间隔100毫秒执行一次 每次减100
      currentTimeNumber -= 100;

      ///如果计完成取消定时
      if (currentTimeNumber <= 0) {
        _timer.cancel();
        currentTimeNumber = 0;
      }
      ///流数据更新
      progress.value = (totalTimeNumber-currentTimeNumber)/totalTimeNumber;
      _streamController.add((totalTimeNumber-currentTimeNumber)/totalTimeNumber);
    });
  }

  @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 Scaffold(
        body: Container(
          width: MediaQuery.of(context).size.width,
          height: MediaQuery.of(context).size.height, //容器填充满整个屏幕
          child: StreamBuilder<double>(
            stream: _streamController.stream,
            initialData: 0,
            builder: (BuildContext context, AsyncSnapshot<double> snapshot) {
              return Stack(
                alignment: Alignment.center,
                children: [
                  Text(
                    widget.progress.value == 1.0 ? widget.progress.completeText : "${(100 * widget.progress.value).toStringAsFixed(1)} %",
                    style: widget.progress.style ??
                        TextStyle(fontSize: widget.progress.radius / 6),
                  ),
                  Container(
                    width: widget.progress.radius * 2,
                    height: widget.progress.radius * 2,
                    child: CustomPaint(
                      painter: ProgressPainter(widget.progress),
                    ),
                  ),

                ],
              );
            },
          ),
        )
    );
  }
}

4.全部代码

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

///信息描述类 [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 CircleProgressWidget extends StatefulWidget {
  final Progress progress;

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

  @override
  _CircleProgressWidgetState createState() => _CircleProgressWidgetState(this.progress);
}

class _CircleProgressWidgetState extends State<CircleProgressWidget> {

  Progress progress;
  _CircleProgressWidgetState(this.progress);

  ///计时器
  Timer _timer;
  ///倒计时6秒
  double totalTimeNumber = 10000;
  ///当前的时间
  double currentTimeNumber = 10000;
  StreamController<double> _streamController = StreamController();

  @override
  void initState(){
    startTimer();
  }

  @override
  void dispose(){
    _streamController.close();
    _timer.cancel();
  }

  void startTimer() {
    ///间隔100毫秒执行时间
    _timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
      ///间隔100毫秒执行一次 每次减100
      currentTimeNumber -= 100;

      ///如果计完成取消定时
      if (currentTimeNumber <= 0) {
        _timer.cancel();
        currentTimeNumber = 0;
      }
      ///流数据更新
      progress.value = (totalTimeNumber-currentTimeNumber)/totalTimeNumber;
      _streamController.add((totalTimeNumber-currentTimeNumber)/totalTimeNumber);
    });
  }

  @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 Scaffold(
        body: Container(
          width: MediaQuery.of(context).size.width,
          height: MediaQuery.of(context).size.height, //容器填充满整个屏幕
          child: StreamBuilder<double>(
            stream: _streamController.stream,
            initialData: 0,
            builder: (BuildContext context, AsyncSnapshot<double> snapshot) {
              return Stack(
                alignment: Alignment.center,
                children: [
                  Text(
                    widget.progress.value == 1.0 ? widget.progress.completeText : "${(100 * widget.progress.value).toStringAsFixed(1)} %",
                    style: widget.progress.style ??
                        TextStyle(fontSize: widget.progress.radius / 6),
                  ),
                  Container(
                    width: widget.progress.radius * 2,
                    height: widget.progress.radius * 2,
                    child: CustomPaint(
                      painter: ProgressPainter(widget.progress),
                    ),
                  ),

                ],
              );
            },
          ),
        )
    );
  }
}

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();
  }
}

参考1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值