circular_percent_indicator.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. //import 'dart:math';
  2. import 'package:flutter/material.dart';
  3. import 'dart:math' as math;
  4. enum CircularStrokeCap { butt, round, square }
  5. enum ArcType { HALF, FULL, CUSTOM }
  6. // ignore: must_be_immutable
  7. class CircularPercentIndicator extends StatefulWidget {
  8. ///Percent value between 0.0 and 1.0
  9. final double percent;
  10. final double radius;
  11. ///Width of the progress bar of the circle
  12. final double lineWidth;
  13. ///Width of the unfilled background of the progress bar
  14. final double backgroundWidth;
  15. ///Color of the background of the circle , default = transparent
  16. final Color fillColor;
  17. ///First color applied to the complete circle
  18. final Color backgroundColor;
  19. Color get progressColor => _progressColor;
  20. Color _progressColor;
  21. ///true if you want the circle to have animation
  22. final bool animation;
  23. ///duration of the animation in milliseconds, It only applies if animation attribute is true
  24. final int animationDuration;
  25. ///widget at the top of the circle
  26. final Widget header;
  27. ///widget at the bottom of the circle
  28. final Widget footer;
  29. ///widget inside the circle
  30. final Widget center;
  31. final LinearGradient linearGradient;
  32. ///The kind of finish to place on the end of lines drawn, values supported: butt, round, square
  33. final CircularStrokeCap circularStrokeCap;
  34. ///the angle which the circle will start the progress (in degrees, eg: 0.0, 45.0, 90.0)
  35. final double startAngle;
  36. /// set true if you want to animate the linear from the last percent value you set
  37. final bool animateFromLastPercent;
  38. /// set false if you don't want to preserve the state of the widget
  39. final bool addAutomaticKeepAlive;
  40. /// set the arc type
  41. final ArcType arcType;
  42. /// set a circular background color when use the arcType property
  43. final Color arcBackgroundColor;
  44. /// set true when you want to display the progress in reverse mode
  45. final bool reverse;
  46. /// Creates a mask filter that takes the progress shape being drawn and blurs it.
  47. final MaskFilter maskFilter;
  48. /// set a circular curve animation type
  49. final Curve curve;
  50. /// set true when you want to restart the animation, it restarts only when reaches 1.0 as a value
  51. /// defaults to false
  52. final bool restartAnimation;
  53. /// Callback called when the animation ends (only if `animation` is true)
  54. final VoidCallback onAnimationEnd;
  55. /// Display a widget indicator at the end of the progress. It only works when `animation` is true
  56. final Widget widgetIndicator;
  57. /// Set to true if you want to rotate linear gradient in accordance to the [startAngle].
  58. final bool rotateLinearGradient;
  59. CircularPercentIndicator(
  60. {Key key,
  61. this.percent = 0.0,
  62. this.lineWidth = 5.0,
  63. this.startAngle = 0.0,
  64. @required this.radius,
  65. this.fillColor = Colors.transparent,
  66. this.backgroundColor = const Color(0xFFB8C7CB),
  67. Color progressColor,
  68. this.backgroundWidth = -1, //negative values ignored, replaced with lineWidth
  69. this.linearGradient,
  70. this.animation = false,
  71. this.animationDuration = 500,
  72. this.header,
  73. this.footer,
  74. this.center,
  75. this.addAutomaticKeepAlive = true,
  76. this.circularStrokeCap,
  77. this.arcBackgroundColor,
  78. this.arcType,
  79. this.animateFromLastPercent = false,
  80. this.reverse = false,
  81. this.curve = Curves.linear,
  82. this.maskFilter,
  83. this.restartAnimation = false,
  84. this.onAnimationEnd,
  85. this.widgetIndicator,
  86. this.rotateLinearGradient = false})
  87. : super(key: key) {
  88. if (linearGradient != null && progressColor != null) {
  89. throw ArgumentError('Cannot provide both linearGradient and progressColor');
  90. }
  91. _progressColor = progressColor ?? Colors.red;
  92. assert(startAngle >= 0.0);
  93. assert(curve != null);
  94. if (percent < 0.0) {
  95. throw Exception("Percent value must be a double between 0.0 and 1.0");
  96. }
  97. if (arcType == null && arcBackgroundColor != null) {
  98. throw ArgumentError('arcType is required when you arcBackgroundColor');
  99. }
  100. }
  101. @override
  102. _CircularPercentIndicatorState createState() => _CircularPercentIndicatorState();
  103. }
  104. class _CircularPercentIndicatorState extends State<CircularPercentIndicator> with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
  105. AnimationController _animationController;
  106. Animation _animation;
  107. double _percent = 0.0;
  108. @override
  109. void dispose() {
  110. if (_animationController != null) {
  111. _animationController.dispose();
  112. }
  113. super.dispose();
  114. }
  115. @override
  116. void initState() {
  117. if (widget.animation) {
  118. _animationController = AnimationController(vsync: this, duration: Duration(milliseconds: widget.animationDuration));
  119. _animation = Tween(begin: 0.0, end: widget.percent).animate(
  120. CurvedAnimation(parent: _animationController, curve: widget.curve),
  121. )..addListener(() {
  122. setState(() {
  123. _percent = _animation.value;
  124. });
  125. if (widget.restartAnimation && _percent == 1.0) {
  126. _animationController.repeat(min: 0, max: 1.0);
  127. }
  128. });
  129. _animationController.addStatusListener((status) {
  130. if (widget.onAnimationEnd != null && status == AnimationStatus.completed) {
  131. widget.onAnimationEnd();
  132. }
  133. });
  134. _animationController.forward();
  135. } else {
  136. _updateProgress();
  137. }
  138. super.initState();
  139. }
  140. void _checkIfNeedCancelAnimation(CircularPercentIndicator oldWidget) {
  141. if (oldWidget.animation && !widget.animation && _animationController != null) {
  142. _animationController.stop();
  143. }
  144. }
  145. @override
  146. void didUpdateWidget(CircularPercentIndicator oldWidget) {
  147. super.didUpdateWidget(oldWidget);
  148. if (oldWidget.percent != widget.percent || oldWidget.startAngle != widget.startAngle) {
  149. if (_animationController != null) {
  150. _animationController.duration = Duration(milliseconds: widget.animationDuration);
  151. _animation = Tween(begin: widget.animateFromLastPercent ? oldWidget.percent : 0.0, end: widget.percent).animate(
  152. CurvedAnimation(parent: _animationController, curve: widget.curve),
  153. );
  154. _animationController.forward(from: 0.0);
  155. } else {
  156. _updateProgress();
  157. }
  158. }
  159. _checkIfNeedCancelAnimation(oldWidget);
  160. }
  161. _updateProgress() {
  162. setState(() {
  163. _percent = widget.percent;
  164. });
  165. }
  166. @override
  167. Widget build(BuildContext context) {
  168. super.build(context);
  169. var items = List<Widget>();
  170. if (widget.header != null) {
  171. items.add(widget.header);
  172. }
  173. items.add(
  174. Container(
  175. height: widget.radius,
  176. width: widget.radius,
  177. child: Stack(
  178. children: [
  179. CustomPaint(
  180. painter: CirclePainter(
  181. progress: math.min(360, _percent * 360),
  182. progressColor: widget.progressColor,
  183. progressOver: math.max(0, _percent -1),
  184. backgroundColor: widget.backgroundColor,
  185. startAngle: widget.startAngle,
  186. circularStrokeCap: widget.circularStrokeCap,
  187. radius: (widget.radius / 2) - widget.lineWidth / 2,
  188. lineWidth: widget.lineWidth,
  189. backgroundWidth: //negative values ignored, replaced with lineWidth
  190. widget.backgroundWidth >= 0.0 ? (widget.backgroundWidth) : widget.lineWidth,
  191. arcBackgroundColor: widget.arcBackgroundColor,
  192. arcType: widget.arcType,
  193. reverse: widget.reverse,
  194. linearGradient: widget.linearGradient,
  195. maskFilter: widget.maskFilter,
  196. rotateLinearGradient: widget.rotateLinearGradient),
  197. child: (widget.center != null) ? Center(child: widget.center) : Container(),
  198. ),
  199. if (widget.widgetIndicator != null && widget.animation)
  200. Positioned.fill(
  201. child: Transform.rotate(
  202. angle: radians((widget.circularStrokeCap != CircularStrokeCap.butt && widget.reverse) ? -15 : 0),
  203. child: Transform.rotate(
  204. angle: radians((widget.reverse ? -360 : 360) * _percent),
  205. child: Transform.translate(
  206. offset: Offset(
  207. (widget.circularStrokeCap != CircularStrokeCap.butt) ? widget.lineWidth / 2 : 0,
  208. (-widget.radius / 2 + widget.lineWidth / 2),
  209. ),
  210. child: widget.widgetIndicator,
  211. ),
  212. ),
  213. ),
  214. ),
  215. ],
  216. ),
  217. ),
  218. );
  219. if (widget.footer != null) {
  220. items.add(widget.footer);
  221. }
  222. return Material(
  223. color: widget.fillColor,
  224. child: Container(
  225. child: Column(
  226. mainAxisAlignment: MainAxisAlignment.center,
  227. mainAxisSize: MainAxisSize.min,
  228. children: items,
  229. ),
  230. ),
  231. );
  232. }
  233. @override
  234. bool get wantKeepAlive => widget.addAutomaticKeepAlive;
  235. }
  236. class CirclePainter extends CustomPainter {
  237. final Paint _paintBackground = Paint();
  238. final Paint _paintLine = Paint();
  239. final Paint _paintBackgroundStartAngle = Paint();
  240. final double lineWidth;
  241. final double backgroundWidth;
  242. final double progress;
  243. final double progressOver;
  244. final double radius;
  245. final Color progressColor;
  246. final Color backgroundColor;
  247. final CircularStrokeCap circularStrokeCap;
  248. final double startAngle;
  249. final LinearGradient linearGradient;
  250. final Color arcBackgroundColor;
  251. final ArcType arcType;
  252. final bool reverse;
  253. final MaskFilter maskFilter;
  254. final bool rotateLinearGradient;
  255. final Paint _paintScale = Paint()..strokeWidth = 1;
  256. CirclePainter(
  257. {this.lineWidth,
  258. this.backgroundWidth,
  259. this.progress,
  260. this.progressOver,
  261. @required this.radius,
  262. this.progressColor,
  263. this.backgroundColor,
  264. this.startAngle = 0.0,
  265. this.circularStrokeCap = CircularStrokeCap.round,
  266. this.linearGradient,
  267. this.reverse,
  268. this.arcBackgroundColor,
  269. this.arcType,
  270. this.maskFilter,
  271. this.rotateLinearGradient}) {
  272. _paintBackground.color = backgroundColor;
  273. _paintBackground.style = PaintingStyle.stroke;
  274. _paintBackground.strokeWidth = backgroundWidth;
  275. if (circularStrokeCap == CircularStrokeCap.round) {
  276. _paintBackground.strokeCap = StrokeCap.round;
  277. } else if (circularStrokeCap == CircularStrokeCap.butt) {
  278. _paintBackground.strokeCap = StrokeCap.butt;
  279. } else {
  280. _paintBackground.strokeCap = StrokeCap.square;
  281. }
  282. if (arcBackgroundColor != null) {
  283. _paintBackgroundStartAngle.color = arcBackgroundColor;
  284. _paintBackgroundStartAngle.style = PaintingStyle.stroke;
  285. _paintBackgroundStartAngle.strokeWidth = lineWidth;
  286. if (circularStrokeCap == CircularStrokeCap.round) {
  287. _paintBackgroundStartAngle.strokeCap = StrokeCap.round;
  288. } else if (circularStrokeCap == CircularStrokeCap.butt) {
  289. _paintBackgroundStartAngle.strokeCap = StrokeCap.butt;
  290. } else {
  291. _paintBackgroundStartAngle.strokeCap = StrokeCap.square;
  292. }
  293. }
  294. _paintLine.color = progressColor;
  295. _paintLine.style = PaintingStyle.stroke;
  296. _paintLine.strokeWidth = lineWidth;
  297. _paintLine.strokeJoin = StrokeJoin.round;
  298. if (circularStrokeCap == CircularStrokeCap.round) {
  299. _paintLine.strokeCap = StrokeCap.round;
  300. } else if (circularStrokeCap == CircularStrokeCap.butt) {
  301. _paintLine.strokeCap = StrokeCap.butt;
  302. } else {
  303. _paintLine.strokeCap = StrokeCap.square;
  304. }
  305. }
  306. @override
  307. void paint(Canvas canvas, Size size) {
  308. final center = Offset(size.width / 2, size.height / 2);
  309. double fixedStartAngle = startAngle;
  310. final rectForArc = Rect.fromCircle(center: center, radius: radius);
  311. double startAngleFixedMargin = 1.0;
  312. if (arcType != null) {
  313. if (arcType == ArcType.CUSTOM) {
  314. startAngleFixedMargin = 1 - (fixedStartAngle - 180).abs() * 2 / 360;
  315. } else if (arcType == ArcType.FULL) {
  316. fixedStartAngle = 220;
  317. startAngleFixedMargin = 172 / fixedStartAngle;
  318. } else {
  319. fixedStartAngle = 270;
  320. startAngleFixedMargin = 135 / fixedStartAngle;
  321. }
  322. }
  323. if (arcType == ArcType.HALF) {
  324. canvas.drawArc(rectForArc, radians(-90.0 + fixedStartAngle), radians(360 * startAngleFixedMargin), false, _paintBackground);
  325. } else if (arcType == ArcType.CUSTOM) {
  326. canvas.drawArc(rectForArc, radians(-90.0 + fixedStartAngle), radians(360 * startAngleFixedMargin), false, _paintBackground);
  327. } else {
  328. canvas.drawCircle(center, radius, _paintBackground);
  329. }
  330. if (maskFilter != null) {
  331. _paintLine.maskFilter = maskFilter;
  332. }
  333. if (linearGradient != null) {
  334. if (rotateLinearGradient && progress > 0) {
  335. double correction = 0;
  336. if (_paintLine.strokeCap == StrokeCap.round || _paintLine.strokeCap == StrokeCap.square) {
  337. if (reverse) {
  338. correction = math.atan(_paintLine.strokeWidth / 2 / radius);
  339. } else {
  340. correction = math.atan(_paintLine.strokeWidth / 2 / radius);
  341. }
  342. }
  343. _paintLine.shader = SweepGradient(
  344. transform:
  345. reverse ? GradientRotation(radians(-90 - progress + startAngle) - correction) : GradientRotation(radians(-90.0 + startAngle) - correction),
  346. startAngle: radians(0),
  347. endAngle: radians(360),
  348. tileMode: TileMode.clamp,
  349. colors: reverse ? linearGradient.colors.reversed.toList() : linearGradient.colors)
  350. .createShader(
  351. Rect.fromCircle(
  352. center: center,
  353. radius: radius,
  354. ),
  355. );
  356. } else if (!rotateLinearGradient) {
  357. _paintLine.shader = linearGradient.createShader(
  358. Rect.fromCircle(
  359. center: center,
  360. radius: radius,
  361. ),
  362. );
  363. }
  364. }
  365. if (arcBackgroundColor != null) {
  366. canvas.drawArc(
  367. Rect.fromCircle(center: center, radius: radius),
  368. radians(-90.0 + fixedStartAngle),
  369. radians(360 * startAngleFixedMargin),
  370. false,
  371. _paintBackgroundStartAngle,
  372. );
  373. }
  374. if (arcType == ArcType.CUSTOM) {
  375. if(progressOver > 0) {
  376. canvas.save();
  377. var rect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset(center.dy, lineWidth + 4), width: 2, height: 3), Radius.circular(5.0));
  378. canvas.translate(center.dx, center.dy);
  379. canvas.rotate(radians(fixedStartAngle));
  380. canvas.translate(-center.dx, -center.dy);
  381. var count = 60;
  382. var r = radians(360 * startAngleFixedMargin) / count;
  383. for (var i = 0; i <= count; i++) {
  384. if (progressOver >= i / count) {
  385. _paintScale.color = _setColor(i, count);
  386. } else {
  387. _paintScale.color = backgroundColor;
  388. }
  389. canvas.drawRRect(rect, _paintScale);
  390. canvas.translate(center.dx, center.dy);
  391. canvas.rotate(r);
  392. canvas.translate(-center.dx, -center.dy);
  393. }
  394. canvas.restore();
  395. }
  396. }
  397. if (reverse) {
  398. final start = radians(360 * startAngleFixedMargin - 90.0 + fixedStartAngle);
  399. final end = radians(-progress * startAngleFixedMargin);
  400. canvas.drawArc(
  401. Rect.fromCircle(
  402. center: center,
  403. radius: radius,
  404. ),
  405. start,
  406. end,
  407. false,
  408. _paintLine,
  409. );
  410. } else {
  411. final start = radians(-90.0 + fixedStartAngle);
  412. final end = radians(progress * startAngleFixedMargin);
  413. canvas.drawArc(
  414. Rect.fromCircle(
  415. center: center,
  416. radius: radius,
  417. ),
  418. start,
  419. end,
  420. false,
  421. _paintLine,
  422. );
  423. }
  424. }
  425. final Color _o = const Color(0xff8DF7FF);
  426. final Color _o1 = const Color(0xff16A2FF);
  427. _setColor(int val,int mCount) {
  428. int r = 0, g = 0, b = 0;
  429. r = (_o.red + (_o1.red - _o.red) * val / mCount).toInt();
  430. g = (_o.green + (_o1.green - _o.green) * val / mCount).toInt();
  431. b = (_o.blue + (_o1.blue - _o.blue) * val / mCount).toInt();
  432. return Color.fromRGBO(r,g,b, 1.0);
  433. }
  434. @override
  435. bool shouldRepaint(CustomPainter oldDelegate) {
  436. return true;
  437. }
  438. }
  439. num radians(num deg) => deg * (math.pi / 180.0);