# 實例:圓形漸變進度條(自繪)
本節我們實現一個圓形漸變進度條,它支持:
1. 支持多種漸變色。
2. 任意弧度;整個進度可以不是整圓。
3. 可以自定義粗細、兩端是否圓角等樣式。
可以發現要實現這樣的一個進度條是無法通過現有組件組合而成的,所以我們通過自繪方式實現。
實現代碼如下:
```
import 'dart:math';
import 'package:flutter/material.dart';
class GradientCircularProgressIndicator extends StatelessWidget {
GradientCircularProgressIndicator({
this.stokeWidth = 2.0,
@required this.radius,
@required this.colors,
this.stops,
this.strokeCapRound = false,
this.backgroundColor = const Color(0xFFEEEEEE),
this.totalAngle = 2 * pi,
this.value
});
///粗細
final double stokeWidth;
/// 圓的半徑
final double radius;
///兩端是否為圓角
final bool strokeCapRound;
/// 當前進度,取值范圍 [0.0-1.0]
final double value;
/// 進度條背景色
final Color backgroundColor;
/// 進度條的總弧度,2*PI為整圓,小于2*PI則不是整圓
final double totalAngle;
/// 漸變色數組
final List<Color> colors;
/// 漸變色的終止點,對應colors屬性
final List<double> stops;
@override
Widget build(BuildContext context) {
double _offset = .0;
// 如果兩端為圓角,則需要對起始位置進行調整,否則圓角部分會偏離起始位置
// 下面調整的角度的計算公式是通過數學幾何知識得出,讀者有興趣可以研究一下為什么是這樣
if (strokeCapRound) {
_offset = asin(stokeWidth / (radius * 2 - stokeWidth));
}
var _colors = colors;
if (_colors == null) {
Color color = Theme
.of(context)
.accentColor;
_colors = [color, color];
}
return Transform.rotate(
angle: -pi / 2.0 - _offset,
child: CustomPaint(
size: Size.fromRadius(radius),
painter: _GradientCircularProgressPainter(
stokeWidth: stokeWidth,
strokeCapRound: strokeCapRound,
backgroundColor: backgroundColor,
value: value,
total: totalAngle,
radius: radius,
colors: _colors,
)
),
);
}
}
//實現畫筆
class _GradientCircularProgressPainter extends CustomPainter {
_GradientCircularProgressPainter({
this.stokeWidth: 10.0,
this.strokeCapRound: false,
this.backgroundColor = const Color(0xFFEEEEEE),
this.radius,
this.total = 2 * pi,
@required this.colors,
this.stops,
this.value
});
final double stokeWidth;
final bool strokeCapRound;
final double value;
final Color backgroundColor;
final List<Color> colors;
final double total;
final double radius;
final List<double> stops;
@override
void paint(Canvas canvas, Size size) {
if (radius != null) {
size = Size.fromRadius(radius);
}
double _offset = stokeWidth / 2.0;
double _value = (value ?? .0);
_value = _value.clamp(.0, 1.0) * total;
double _start = .0;
if (strokeCapRound) {
_start = asin(stokeWidth/ (size.width - stokeWidth));
}
Rect rect = Offset(_offset, _offset) & Size(
size.width - stokeWidth,
size.height - stokeWidth
);
var paint = Paint()
..strokeCap = strokeCapRound ? StrokeCap.round : StrokeCap.butt
..style = PaintingStyle.stroke
..isAntiAlias = true
..strokeWidth = stokeWidth;
// 先畫背景
if (backgroundColor != Colors.transparent) {
paint.color = backgroundColor;
canvas.drawArc(
rect,
_start,
total,
false,
paint
);
}
// 再畫前景,應用漸變
if (_value > 0) {
paint.shader = SweepGradient(
startAngle: 0.0,
endAngle: _value,
colors: colors,
stops: stops,
).createShader(rect);
canvas.drawArc(
rect,
_start,
_value,
false,
paint
);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
```
下面我們來測試一下,為了盡可能多的展示GradientCircularProgressIndicator的不同外觀和用途,這個示例代碼會比較長,并且添加了動畫,建議讀者將此示例運行起來觀看實際效果,我們先看看其中的一幀動畫的截圖:

完整的代碼:
```
import 'dart:math';
import 'package:flutter/material.dart';
import '../widgets/index.dart';
class GradientCircularProgressRoute extends StatefulWidget {
@override
GradientCircularProgressRouteState createState() {
return new GradientCircularProgressRouteState();
}
}
class GradientCircularProgressRouteState
extends State<GradientCircularProgressRoute> with TickerProviderStateMixin {
AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController =
new AnimationController(vsync: this, duration: Duration(seconds: 3));
bool isForward = true;
_animationController.addStatusListener((status) {
if (status == AnimationStatus.forward) {
isForward = true;
} else if (status == AnimationStatus.completed ||
status == AnimationStatus.dismissed) {
if (isForward) {
_animationController.reverse();
} else {
_animationController.forward();
}
} else if (status == AnimationStatus.reverse) {
isForward = false;
}
});
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
AnimatedBuilder(
animation: _animationController,
builder: (BuildContext context, Widget child) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
children: <Widget>[
Wrap(
spacing: 10.0,
runSpacing: 16.0,
children: <Widget>[
GradientCircularProgressIndicator(
// No gradient
colors: [Colors.blue, Colors.blue],
radius: 50.0,
stokeWidth: 3.0,
value: _animationController.value,
),
GradientCircularProgressIndicator(
colors: [Colors.red, Colors.orange],
radius: 50.0,
stokeWidth: 3.0,
value: _animationController.value,
),
GradientCircularProgressIndicator(
colors: [Colors.red, Colors.orange, Colors.red],
radius: 50.0,
stokeWidth: 5.0,
value: _animationController.value,
),
GradientCircularProgressIndicator(
colors: [Colors.teal, Colors.cyan],
radius: 50.0,
stokeWidth: 5.0,
strokeCapRound: true,
value: CurvedAnimation(
parent: _animationController,
curve: Curves.decelerate)
.value,
),
TurnBox(
turns: 1 / 8,
child: GradientCircularProgressIndicator(
colors: [Colors.red, Colors.orange, Colors.red],
radius: 50.0,
stokeWidth: 5.0,
strokeCapRound: true,
backgroundColor: Colors.red[50],
totalAngle: 1.5 * pi,
value: CurvedAnimation(
parent: _animationController,
curve: Curves.ease)
.value),
),
RotatedBox(
quarterTurns: 1,
child: GradientCircularProgressIndicator(
colors: [Colors.blue[700], Colors.blue[200]],
radius: 50.0,
stokeWidth: 3.0,
strokeCapRound: true,
backgroundColor: Colors.transparent,
value: _animationController.value),
),
GradientCircularProgressIndicator(
colors: [
Colors.red,
Colors.amber,
Colors.cyan,
Colors.green[200],
Colors.blue,
Colors.red
],
radius: 50.0,
stokeWidth: 5.0,
strokeCapRound: true,
value: _animationController.value,
),
],
),
GradientCircularProgressIndicator(
colors: [Colors.blue[700], Colors.blue[200]],
radius: 100.0,
stokeWidth: 20.0,
value: _animationController.value,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: GradientCircularProgressIndicator(
colors: [Colors.blue[700], Colors.blue[300]],
radius: 100.0,
stokeWidth: 20.0,
value: _animationController.value,
strokeCapRound: true,
),
),
//剪裁半圓
ClipRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: .5,
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: SizedBox(
//width: 100.0,
child: TurnBox(
turns: .75,
child: GradientCircularProgressIndicator(
colors: [Colors.teal, Colors.cyan[500]],
radius: 100.0,
stokeWidth: 8.0,
value: _animationController.value,
totalAngle: pi,
strokeCapRound: true,
),
),
),
),
),
),
SizedBox(
height: 104.0,
width: 200.0,
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Positioned(
height: 200.0,
top: .0,
child: TurnBox(
turns: .75,
child: GradientCircularProgressIndicator(
colors: [Colors.teal, Colors.cyan[500]],
radius: 100.0,
stokeWidth: 8.0,
value: _animationController.value,
totalAngle: pi,
strokeCapRound: true,
),
),
),
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(
"${(_animationController.value * 100).toInt()}%",
style: TextStyle(
fontSize: 25.0,
color: Colors.blueGrey,
),
),
)
],
),
),
],
),
);
},
),
],
),
),
);
}
}
```
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示