circular_percent_indicator.dart 15 KB

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