[-Flutter 自定義元件-] 圓形進度條

張風捷特烈發表於2019-08-02

今天寫個簡單的,自定義一個圓形進度條,並且加上小箭頭指向內圈進度。
進度條已上傳到公網,使用circle_progress: ^0.0.1,使用如下

[-Flutter 自定義元件-] 圓形進度條

[-Flutter 自定義元件-] 圓形進度條

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);
複製程式碼

[-Flutter 自定義元件-] 圓形進度條

背景直接畫圓,進度使用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結合使用,可能會更方便。

[-Flutter 自定義元件-] 圓形進度條

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:繪製點

繪製點的時候要注意顏色的把控,判斷進度條是否到達,然後更改顏色

[-Flutter 自定義元件-] 圓形進度條

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,期待與你的交流與切磋。

相關文章