本节我们实现一个圆形背景渐变进度条,它支持:

  1. 支持多种背景渐变色。
  2. 任意弧度;进度条可以不是整圆。
  3. 可以自定义粗细、两端是否圆角等样式。

可以发现要实现这样的一个进度条是无法通过现有组件组合而成的,所以我们通过自绘方式实现,代码如下:

  1. import 'dart:math';
  2. import 'package:flutter/material.dart';
  3. class GradientCircularProgressIndicator extends StatelessWidget {
  4. GradientCircularProgressIndicator(
  5. {this.strokeWidth = 2.0,
  6. @required this.radius,
  7. @required this.colors,
  8. this.stops,
  9. this.strokeCapRound = false,
  10. this.backgroundColor = const Color(0xFFEEEEEE),
  11. this.totalAngle = 2 * pi,
  12. this.value});
  13. ///粗细
  14. final double strokeWidth;
  15. /// 圆的半径
  16. final double radius;
  17. ///两端是否为圆角
  18. final bool strokeCapRound;
  19. /// 当前进度,取值范围 [0.0-1.0]
  20. final double value;
  21. /// 进度条背景色
  22. final Color backgroundColor;
  23. /// 进度条的总弧度,2*PI为整圆,小于2*PI则不是整圆
  24. final double totalAngle;
  25. /// 渐变色数组
  26. final List<Color> colors;
  27. /// 渐变色的终止点,对应colors属性
  28. final List<double> stops;
  29. @override
  30. Widget build(BuildContext context) {
  31. double _offset = .0;
  32. // 如果两端为圆角,则需要对起始位置进行调整,否则圆角部分会偏离起始位置
  33. // 下面调整的角度的计算公式是通过数学几何知识得出,读者有兴趣可以研究一下为什么是这样
  34. if (strokeCapRound) {
  35. _offset = asin(strokeWidth / (radius * 2 - strokeWidth));
  36. }
  37. var _colors = colors;
  38. if (_colors == null) {
  39. Color color = Theme.of(context).accentColor;
  40. _colors = [color, color];
  41. }
  42. return Transform.rotate(
  43. angle: -pi / 2.0, //开始弧度 [- _offset]
  44. child: CustomPaint(
  45. size: Size.fromRadius(radius),
  46. painter: _GradientCircularProgressPainter(
  47. strokeWidth: strokeWidth,
  48. strokeCapRound: strokeCapRound,
  49. backgroundColor: backgroundColor,
  50. value: value,
  51. total: totalAngle,
  52. radius: radius,
  53. colors: _colors,
  54. )),
  55. );
  56. }
  57. }
  58. //实现画笔
  59. class _GradientCircularProgressPainter extends CustomPainter {
  60. _GradientCircularProgressPainter(
  61. {this.strokeWidth: 10.0,
  62. this.strokeCapRound: false,
  63. this.backgroundColor = const Color(0xFFEEEEEE),
  64. this.radius,
  65. this.total = 2 * pi,
  66. @required this.colors,
  67. this.stops,
  68. this.value});
  69. final double strokeWidth;
  70. final bool strokeCapRound;
  71. final double value;
  72. final Color backgroundColor;
  73. final List<Color> colors;
  74. final double total;
  75. final double radius;
  76. final List<double> stops;
  77. @override
  78. void paint(Canvas canvas, Size size) {
  79. if (radius != null) {
  80. size = Size.fromRadius(radius);
  81. }
  82. double _offset = strokeWidth / 2.0;
  83. double _value = (value ?? .0);
  84. _value = _value.clamp(.0, 1.0) * total;
  85. double _start = .0;
  86. if (strokeCapRound) {
  87. _start = asin(strokeWidth / (size.width - strokeWidth));
  88. }
  89. Rect rect = Offset(_offset, _offset) &
  90. Size(size.width - strokeWidth, size.height - strokeWidth);
  91. var paint = Paint()
  92. ..strokeCap = strokeCapRound ? StrokeCap.round : StrokeCap.butt
  93. ..style = PaintingStyle.stroke
  94. ..isAntiAlias = true
  95. ..strokeWidth = strokeWidth;
  96. // 先画背景
  97. if (backgroundColor != Colors.transparent) {
  98. paint.color = backgroundColor;
  99. canvas.drawArc(rect, _start, total, false, paint);
  100. }
  101. // 再画前景,应用渐变
  102. if (_value > 0) {
  103. paint.shader = SweepGradient(
  104. startAngle: 0.0,
  105. endAngle: _value,
  106. colors: colors,
  107. stops: stops,
  108. ).createShader(rect);
  109. canvas.drawArc(rect, _start, _value, false, paint);
  110. }
  111. }
  112. @override
  113. bool shouldRepaint(CustomPainter oldDelegate) => true;
  114. }

下面我们来测试一下,为了尽可能多的展示GradientCircularProgressIndicator的不同外观和用途,这个示例代码会比较长,并且添加了动画,建议读者将此示例运行起来观看实际效果,我们先看看其中的一帧动画的截图:
自绘实例:圆形背景渐变进度条 - 图1

示例代码:

  1. import 'package:flutter/material.dart';
  2. import 'dart:math';
  3. void main() => runApp(new MyApp());
  4. class MyApp extends StatelessWidget {
  5. @override
  6. Widget build(BuildContext context) {
  7. return MaterialApp(
  8. debugShowCheckedModeBanner: true,
  9. title: 'GradientCircularProgressRoute',
  10. theme: ThemeData(
  11. primarySwatch: Colors.blue,
  12. ),
  13. home: Scaffold(
  14. appBar: AppBar(title: Text("GradientCircularProgressRoute")),
  15. body: GradientCircularProgressRoute(),
  16. ));
  17. }
  18. }
  19. class GradientCircularProgressRoute extends StatefulWidget {
  20. @override
  21. GradientCircularProgressRouteState createState() {
  22. return new GradientCircularProgressRouteState();
  23. }
  24. }
  25. class GradientCircularProgressRouteState
  26. extends State<GradientCircularProgressRoute> with TickerProviderStateMixin {
  27. AnimationController _animationController;
  28. @override
  29. void initState() {
  30. super.initState();
  31. _animationController =
  32. new AnimationController(vsync: this, duration: Duration(seconds: 3));
  33. bool isForward = true;
  34. _animationController.addStatusListener((status) {
  35. if (status == AnimationStatus.forward) {
  36. isForward = true;
  37. } else if (status == AnimationStatus.completed ||
  38. status == AnimationStatus.dismissed) {
  39. if (isForward) {
  40. _animationController.reverse();
  41. } else {
  42. _animationController.forward();
  43. }
  44. } else if (status == AnimationStatus.reverse) {
  45. isForward = false;
  46. }
  47. });
  48. _animationController.forward();
  49. }
  50. @override
  51. void dispose() {
  52. _animationController.dispose();
  53. super.dispose();
  54. }
  55. @override
  56. Widget build(BuildContext context) {
  57. return SingleChildScrollView(
  58. child: Center(
  59. child: Column(
  60. crossAxisAlignment: CrossAxisAlignment.center,
  61. children: <Widget>[
  62. AnimatedBuilder(
  63. animation: _animationController,
  64. builder: (BuildContext context, Widget child) {
  65. return Padding(
  66. padding: const EdgeInsets.symmetric(vertical: 16.0),
  67. child: Column(
  68. children: <Widget>[
  69. Wrap(
  70. spacing: 10.0,
  71. runSpacing: 16.0,
  72. children: <Widget>[
  73. GradientCircularProgressIndicator(
  74. // No gradient
  75. colors: [Colors.blue, Colors.blue],
  76. radius: 50.0,
  77. strokeWidth: 3.0,
  78. value: _animationController.value,
  79. ),
  80. GradientCircularProgressIndicator(
  81. colors: [Colors.red, Colors.orange],
  82. radius: 50.0,
  83. strokeWidth: 3.0,
  84. value: _animationController.value,
  85. ),
  86. GradientCircularProgressIndicator(
  87. colors: [Colors.red, Colors.orange, Colors.red],
  88. radius: 50.0,
  89. strokeWidth: 5.0,
  90. value: _animationController.value,
  91. ),
  92. GradientCircularProgressIndicator(
  93. colors: [Colors.teal, Colors.cyan],
  94. radius: 50.0,
  95. strokeWidth: 5.0,
  96. strokeCapRound: true,
  97. value: CurvedAnimation(
  98. parent: _animationController,
  99. curve: Curves.decelerate)
  100. .value,
  101. ),
  102. TurnBox(
  103. turns: 1 / 8,
  104. child: GradientCircularProgressIndicator(
  105. colors: [Colors.red, Colors.orange, Colors.red],
  106. radius: 50.0,
  107. strokeWidth: 5.0,
  108. strokeCapRound: true,
  109. backgroundColor: Colors.red[50],
  110. totalAngle: 1.5 * pi,
  111. value: CurvedAnimation(
  112. parent: _animationController,
  113. curve: Curves.ease)
  114. .value),
  115. ),
  116. RotatedBox(
  117. quarterTurns: 1,
  118. child: GradientCircularProgressIndicator(
  119. colors: [Colors.blue[700], Colors.blue[200]],
  120. radius: 50.0,
  121. strokeWidth: 3.0,
  122. strokeCapRound: true,
  123. backgroundColor: Colors.transparent,
  124. value: _animationController.value),
  125. ),
  126. GradientCircularProgressIndicator(
  127. colors: [
  128. Colors.red,
  129. Colors.amber,
  130. Colors.cyan,
  131. Colors.green[200],
  132. Colors.blue,
  133. Colors.red
  134. ],
  135. radius: 50.0,
  136. strokeWidth: 5.0,
  137. strokeCapRound: true,
  138. value: _animationController.value,
  139. ),
  140. ],
  141. ),
  142. GradientCircularProgressIndicator(
  143. colors: [Colors.blue[700], Colors.blue[200]],
  144. radius: 100.0,
  145. strokeWidth: 20.0,
  146. value: _animationController.value,
  147. ),
  148. Padding(
  149. padding: const EdgeInsets.symmetric(vertical: 16.0),
  150. child: GradientCircularProgressIndicator(
  151. colors: [Colors.blue[700], Colors.blue[300]],
  152. radius: 100.0,
  153. strokeWidth: 20.0,
  154. value: _animationController.value,
  155. strokeCapRound: true,
  156. ),
  157. ),
  158. //剪裁半圆
  159. ClipRect(
  160. child: Align(
  161. alignment: Alignment.topCenter,
  162. heightFactor: .5,
  163. child: Padding(
  164. padding: const EdgeInsets.only(bottom: 8.0),
  165. child: SizedBox(
  166. //width: 100.0,
  167. child: TurnBox(
  168. turns: .75,
  169. child: GradientCircularProgressIndicator(
  170. colors: [Colors.teal, Colors.cyan[500]],
  171. radius: 100.0,
  172. strokeWidth: 8.0,
  173. value: _animationController.value,
  174. totalAngle: pi,
  175. strokeCapRound: true,
  176. ),
  177. ),
  178. ),
  179. ),
  180. ),
  181. ),
  182. SizedBox(
  183. height: 104.0,
  184. width: 200.0,
  185. child: Stack(
  186. alignment: Alignment.center,
  187. children: <Widget>[
  188. Positioned(
  189. height: 200.0,
  190. top: .0,
  191. child: TurnBox(
  192. turns: .75,
  193. child: GradientCircularProgressIndicator(
  194. colors: [Colors.teal, Colors.cyan[500]],
  195. radius: 100.0,
  196. strokeWidth: 8.0,
  197. value: _animationController.value,
  198. totalAngle: pi,
  199. strokeCapRound: true,
  200. ),
  201. ),
  202. ),
  203. Padding(
  204. padding: const EdgeInsets.only(top: 10.0),
  205. child: Text(
  206. "${(_animationController.value * 100).toInt()}%",
  207. style: TextStyle(
  208. fontSize: 25.0,
  209. color: Colors.blueGrey,
  210. ),
  211. ),
  212. )
  213. ],
  214. ),
  215. ),
  216. ],
  217. ),
  218. );
  219. },
  220. ),
  221. ],
  222. ),
  223. ),
  224. );
  225. }
  226. }
  227. class GradientCircularProgressIndicator extends StatelessWidget {
  228. GradientCircularProgressIndicator({
  229. this.strokeWidth = 2.0,
  230. @required this.radius,
  231. @required this.colors,
  232. this.stops,
  233. this.strokeCapRound = false,
  234. this.backgroundColor = const Color(0xFFEEEEEE),
  235. this.totalAngle = 2 * pi,
  236. this.value
  237. });
  238. ///粗细
  239. final double strokeWidth;
  240. /// 圆的半径
  241. final double radius;
  242. ///两端是否为圆角
  243. final bool strokeCapRound;
  244. /// 当前进度,取值范围 [0.0-1.0]
  245. final double value;
  246. /// 进度条背景色
  247. final Color backgroundColor;
  248. /// 进度条的总弧度,2*PI为整圆,小于2*PI则不是整圆
  249. final double totalAngle;
  250. /// 渐变色数组
  251. final List<Color> colors;
  252. /// 渐变色的终止点,对应colors属性
  253. final List<double> stops;
  254. @override
  255. Widget build(BuildContext context) {
  256. double _offset = .0;
  257. // 如果两端为圆角,则需要对起始位置进行调整,否则圆角部分会偏离起始位置
  258. // 下面调整的角度的计算公式是通过数学几何知识得出,读者有兴趣可以研究一下为什么是这样
  259. if (strokeCapRound) {
  260. _offset = asin(strokeWidth / (radius * 2 - strokeWidth));
  261. }
  262. var _colors = colors;
  263. if (_colors == null) {
  264. Color color = Theme
  265. .of(context)
  266. .accentColor;
  267. _colors = [color, color];
  268. }
  269. return Transform.rotate(
  270. angle: -pi / 2.0 - _offset,
  271. child: CustomPaint(
  272. size: Size.fromRadius(radius),
  273. painter: _GradientCircularProgressPainter(
  274. strokeWidth: strokeWidth,
  275. strokeCapRound: strokeCapRound,
  276. backgroundColor: backgroundColor,
  277. value: value,
  278. total: totalAngle,
  279. radius: radius,
  280. colors: _colors,
  281. )
  282. ),
  283. );
  284. }
  285. }
  286. //实现画笔
  287. class _GradientCircularProgressPainter extends CustomPainter {
  288. _GradientCircularProgressPainter({
  289. this.strokeWidth: 10.0,
  290. this.strokeCapRound: false,
  291. this.backgroundColor = const Color(0xFFEEEEEE),
  292. this.radius,
  293. this.total = 2 * pi,
  294. @required this.colors,
  295. this.stops,
  296. this.value
  297. });
  298. final double strokeWidth;
  299. final bool strokeCapRound;
  300. final double value;
  301. final Color backgroundColor;
  302. final List<Color> colors;
  303. final double total;
  304. final double radius;
  305. final List<double> stops;
  306. @override
  307. void paint(Canvas canvas, Size size) {
  308. if (radius != null) {
  309. size = Size.fromRadius(radius);
  310. }
  311. double _offset = strokeWidth / 2.0;
  312. double _value = (value ?? .0);
  313. _value = _value.clamp(.0, 1.0) * total;
  314. double _start = .0;
  315. if (strokeCapRound) {
  316. _start = asin(strokeWidth/ (size.width - strokeWidth));
  317. }
  318. Rect rect = Offset(_offset, _offset) & Size(
  319. size.width - strokeWidth,
  320. size.height - strokeWidth
  321. );
  322. var paint = Paint()
  323. ..strokeCap = strokeCapRound ? StrokeCap.round : StrokeCap.butt
  324. ..style = PaintingStyle.stroke
  325. ..isAntiAlias = true
  326. ..strokeWidth = strokeWidth;
  327. // 先画背景
  328. if (backgroundColor != Colors.transparent) {
  329. paint.color = backgroundColor;
  330. canvas.drawArc(
  331. rect,
  332. _start,
  333. total,
  334. false,
  335. paint
  336. );
  337. }
  338. // 再画前景,应用渐变
  339. if (_value > 0) {
  340. paint.shader = SweepGradient(
  341. startAngle: 0.0,
  342. endAngle: _value,
  343. colors: colors,
  344. stops: stops,
  345. ).createShader(rect);
  346. canvas.drawArc(
  347. rect,
  348. _start,
  349. _value,
  350. false,
  351. paint
  352. );
  353. }
  354. }
  355. @override
  356. bool shouldRepaint(CustomPainter oldDelegate) => true;
  357. }
  358. class TurnBox extends StatefulWidget {
  359. const TurnBox(
  360. {Key key,
  361. this.turns = .0, //旋转的“圈”数,一圈为360度,如0.25圈即90度
  362. this.speed = 200, //过渡动画执行的总时长
  363. this.child})
  364. : super(key: key);
  365. final double turns;
  366. final int speed;
  367. final Widget child;
  368. @override
  369. _TurnBoxState createState() => new _TurnBoxState();
  370. }
  371. class _TurnBoxState extends State<TurnBox> with SingleTickerProviderStateMixin {
  372. AnimationController _controller;
  373. @override
  374. void initState() {
  375. super.initState();
  376. _controller = new AnimationController(
  377. vsync: this, lowerBound: -double.infinity, upperBound: double.infinity);
  378. _controller.value = widget.turns;
  379. }
  380. @override
  381. void dispose() {
  382. _controller.dispose();
  383. super.dispose();
  384. }
  385. @override
  386. Widget build(BuildContext context) {
  387. return RotationTransition(
  388. turns: _controller,
  389. child: widget.child,
  390. );
  391. }
  392. @override
  393. void didUpdateWidget(TurnBox oldWidget) {
  394. super.didUpdateWidget(oldWidget);
  395. //旋转角度发生变化时执行过渡动画
  396. if (oldWidget.turns != widget.turns) {
  397. _controller.animateTo(
  398. widget.turns,
  399. duration: Duration(milliseconds: widget.speed ?? 200),
  400. curve: Curves.easeOut,
  401. );
  402. }
  403. }
  404. }

怎么样,很炫酷吧!GradientCircularProgressIndicator已经被添加进了笔者维护的flukit组件库中了,读者如果有需要,可以直接依赖flukit包。