flutter custom circular progress bar

I don't know who wrote the original text, but after searching so many, none of them can be used, most of them don't refresh in real time. Under my own debug and learning, I have made improvements, this time I can use xdm directly.

Learning notes are also recorded here

Draw required elements
1. Paper: Canvas canvas object
2. Pen: Paint Brush Object
3.Shape: Path path object
4. Color: Color object

Use StreamController for real-time local refresh

Design sketch:

Program Main Entry

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

1. Define a description object class Progress

1.1: Define a description object class Progress

Pull out a description class for the attributes that need to be changed

///Information Description Class [value] is progress, between 0 and 1, progress bar color,
///Unfinished color [backgroundColor], radius of circle [radius], line width [strokeWidth]
///Number of dotCount s Display text [completeText] after completion of style
class Progress {
  double value;
  Color color;
  Color backgroundColor;
  double radius;
  double strokeWidth;
  int dotCount;
  TextStyle style;
  String completeText;
 
  Progress({this.value, //Initialization function
      this.color,
      this.backgroundColor,
      this.radius,
      this.strokeWidth,
      this.completeText="OK",
       this.style,
      this.dotCount = 40
      });
}

1.2 Custom ProgressPainter

Drawing logic

class ProgressPainter extends CustomPainter {
  Progress _progress;
  Paint _paint; //Drawn Objects
  Paint _arrowPaint;//Arrow Brush
  Path _arrowPath;//The path of the arrow
  double _radius;//radius
 
  ProgressPainter(
    this._progress,
  ) {
    _arrowPath=Path();
    _arrowPaint=Paint();
    _paint = Paint();
    _radius = _progress.radius - _progress.strokeWidth / 2;
  }
 
  @override
  void paint(Canvas canvas, Size size) { 
  //This is what we have to do when defining painter s, where canvas provides the core of our drawing.
  //Size tells us the size of the drawing board (determined by CustomPaint's size or child)
    Rect rect = Offset.zero & size;
    canvas.clipRect(rect); //Clipping Region
  }
 
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

2. Drawing

2.1: Draw progress bar

If you use the given radius directly, you will find that this is the case.The reason is simple, because Canvas draws a circle with a radius that is half the thickness of the inner circle plus a line.
So we need to correct the radius: by shifting half the line thickness and reducing the radius of half the line thickness.

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


Background draws circle directly, progress drawArc method, note that Flutter uses radian!!!.

The save operation saves all previous drawings and canvas states.Drawing and transforming operations after calling the function are recorded.When you call restore(), the operation between save and restore is merged with the previous content, and the status is updated using the locally updated StamController

drawProgress(Canvas canvas) {
  canvas.save();
  _paint//background
    ..style = PaintingStyle.stroke
    ..color = _progress.backgroundColor
    ..strokeWidth = _progress.strokeWidth;
  canvas.drawCircle(Offset(_radius, _radius), _radius, _paint);
  
  _paint//Speed of progress
    ..color = _progress.color
    ..strokeWidth = _progress.strokeWidth * 1.2
    ..strokeCap = StrokeCap.round;
  double sweepAngle = _progress.value * 360; //Completion Angle
  canvas.drawArc(Rect.fromLTRB(0, 0, _radius * 2, _radius * 2),
      -90 / 180 * pi, sweepAngle / 180 * pi, false, _paint);
  canvas.restore();
}

2.2: Draw arrows

Actually, the arrows are pretty good. It may be more convenient to note the combination of relativeLineTo and 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; //Unit length
  _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: Draw Points

When drawing points, be aware of color control, determine if the progress bar has arrived, and then change the color

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. Assembly and use

Use _streamController to update progress bar status

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 _timer;
  ///Countdown 6 seconds
  double totalTimeNumber = 10000;
  ///Current time
  double currentTimeNumber = 10000;
  StreamController<double> _streamController = StreamController();

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

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

  void startTimer() {
    ///100 ms interval execution time
    _timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
      ///100 milliseconds per execution minus 100
      currentTimeNumber -= 100;

      ///Cancel timer if count is complete
      if (currentTimeNumber <= 0) {
        _timer.cancel();
        currentTimeNumber = 0;
      }
      ///Stream Data Update
      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, //Containers fill the entire screen
          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. All Codes

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

///Information Description Class [value] is progress, between 0 and 1, progress bar color,
///Unfinished color [backgroundColor], radius of circle [radius], line width [strokeWidth]
///Number of dotCount s Display text [completeText] after completion of style
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 _timer;
  ///Countdown 6 seconds
  double totalTimeNumber = 10000;
  ///Current time
  double currentTimeNumber = 10000;
  StreamController<double> _streamController = StreamController();

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

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

  void startTimer() {
    ///100 ms interval execution time
    _timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
      ///100 milliseconds per execution minus 100
      currentTimeNumber -= 100;

      ///Cancel timer if count is complete
      if (currentTimeNumber <= 0) {
        _timer.cancel();
        currentTimeNumber = 0;
      }
      ///Stream Data Update
      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, //Containers fill the entire screen
          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); //Clipping Region
    canvas.translate(_progress.strokeWidth / 2, _progress.strokeWidth / 2);

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

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

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

    _paint//Speed of progress
      ..color = _progress.color
      ..strokeWidth = _progress.strokeWidth * 1.2
      ..strokeCap = StrokeCap.round;
    double sweepAngle = _progress.value * 360; //Completion Angle
    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) { //Arrow
    canvas.save();
    canvas.translate(_radius, _radius);// Move the drawing board to the center
    canvas.rotate((180 + _progress.value * 360) / 180 * pi);//Rotate corresponding angle
    var half = _radius / 2;//basic point
    var eg = _radius / 50; //Unit length
    _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) { //Draw Points
    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();
  }
}

Reference 1

Tags: html5 Flutter

Posted on Fri, 03 Sep 2021 12:37:18 -0400 by Heatmizer20