1+ import 'package:flutter/material.dart' ;
2+ import 'package:vector_math/vector_math_64.dart' as math;
3+
4+ enum CircularStrokeCap { butt, round, square }
5+
6+ enum ArcType {
7+ HALF ,
8+ FULL ,
9+ }
10+
11+ class CircularPercentIndicator extends StatefulWidget {
12+ ///Percent value between 0.0 and 1.0
13+ final double percent;
14+ final double radius;
15+
16+ ///Width of the line of the Circle
17+ final double lineWidth;
18+
19+ ///Color of the background of the circle , default = transparent
20+ final Color fillColor;
21+
22+ ///First color applied to the complete circle
23+ final Color backgroundColor;
24+
25+ Color get progressColor => _progressColor;
26+
27+ Color _progressColor;
28+
29+ ///true if you want the circle to have animation
30+ final bool animation;
31+
32+ ///duration of the animation in milliseconds, It only applies if animation attribute is true
33+ final int animationDuration;
34+
35+ ///widget at the top of the circle
36+ final Widget header;
37+
38+ ///widget at the bottom of the circle
39+ final Widget footer;
40+
41+ ///widget inside the circle
42+ final Widget center;
43+
44+ final LinearGradient linearGradient;
45+
46+ ///The kind of finish to place on the end of lines drawn, values supported: butt, round, square
47+ final CircularStrokeCap circularStrokeCap;
48+
49+ ///the angle which the circle will start the progress (in degrees, eg: 0.0, 45.0, 90.0)
50+ final double startAngle;
51+
52+ /// set true if you want to animate the linear from the last percent value you set
53+ final bool animateFromLastPercent;
54+
55+ /// set false if you don't want to preserve the state of the widget
56+ final bool addAutomaticKeepAlive;
57+
58+ /// set the arc type
59+ final ArcType arcType;
60+
61+ /// set a circular background color when use the arcType property
62+ final Color arcBackgroundColor;
63+
64+ /// set true when you want to display the progress in reverse mode
65+ final bool reverse;
66+
67+ /// Creates a mask filter that takes the progress shape being drawn and blurs it.
68+ final MaskFilter maskFilter;
69+
70+ CircularPercentIndicator (
71+ {Key key,
72+ this .percent = 0.0 ,
73+ this .lineWidth = 5.0 ,
74+ this .startAngle = 0.0 ,
75+ @required this .radius,
76+ this .fillColor = Colors .transparent,
77+ this .backgroundColor = const Color (0xFFB8C7CB ),
78+ Color progressColor,
79+ this .linearGradient,
80+ this .animation = false ,
81+ this .animationDuration = 500 ,
82+ this .header,
83+ this .footer,
84+ this .center,
85+ this .addAutomaticKeepAlive = true ,
86+ this .circularStrokeCap,
87+ this .arcBackgroundColor,
88+ this .arcType,
89+ this .animateFromLastPercent = false ,
90+ this .reverse = false ,
91+ this .maskFilter})
92+ : super (key: key) {
93+ if (linearGradient != null && progressColor != null ) {
94+ throw ArgumentError (
95+ 'Cannot provide both linearGradient and progressColor' );
96+ }
97+ _progressColor = progressColor ?? Colors .red;
98+
99+ assert (startAngle >= 0.0 );
100+ if (percent < 0.0 || percent > 1.0 ) {
101+ throw Exception ("Percent value must be a double between 0.0 and 1.0" );
102+ }
103+
104+ if (arcType == null && arcBackgroundColor != null ) {
105+ throw ArgumentError ('arcType is required when you arcBackgroundColor' );
106+ }
107+ }
108+
109+ @override
110+ _CircularPercentIndicatorState createState () =>
111+ _CircularPercentIndicatorState ();
112+ }
113+
114+ class _CircularPercentIndicatorState extends State <CircularPercentIndicator >
115+ with SingleTickerProviderStateMixin , AutomaticKeepAliveClientMixin {
116+ AnimationController _animationController;
117+ Animation _animation;
118+ double _percent = 0.0 ;
119+
120+ @override
121+ void dispose () {
122+ if (_animationController != null ) {
123+ _animationController.dispose ();
124+ }
125+ super .dispose ();
126+ }
127+
128+ @override
129+ void initState () {
130+ if (widget.animation) {
131+ _animationController = AnimationController (
132+ vsync: this ,
133+ duration: Duration (milliseconds: widget.animationDuration));
134+ _animation =
135+ Tween (begin: 0.0 , end: widget.percent).animate (_animationController)
136+ ..addListener (() {
137+ setState (() {
138+ _percent = _animation.value;
139+ });
140+ });
141+ _animationController.forward ();
142+ } else {
143+ _updateProgress ();
144+ }
145+ super .initState ();
146+ }
147+
148+ @override
149+ void didUpdateWidget (CircularPercentIndicator oldWidget) {
150+ super .didUpdateWidget (oldWidget);
151+ if (oldWidget.percent != widget.percent ||
152+ oldWidget.startAngle != widget.startAngle) {
153+ if (_animationController != null ) {
154+ _animationController.duration =
155+ Duration (milliseconds: widget.animationDuration);
156+ _animation = Tween (
157+ begin: widget.animateFromLastPercent ? oldWidget.percent : 0.0 ,
158+ end: widget.percent)
159+ .animate (_animationController);
160+ _animationController.forward (from: 0.0 );
161+ } else {
162+ _updateProgress ();
163+ }
164+ }
165+ }
166+
167+ _updateProgress () {
168+ setState (() {
169+ _percent = widget.percent;
170+ });
171+ }
172+
173+ @override
174+ Widget build (BuildContext context) {
175+ super .build (context);
176+ var items = List <Widget >();
177+ if (widget.header != null ) {
178+ items.add (widget.header);
179+ }
180+ items.add (Container (
181+ height: widget.radius + widget.lineWidth,
182+ width: widget.radius,
183+ child: CustomPaint (
184+ painter: CirclePainter (
185+ progress: _percent * 360 ,
186+ progressColor: widget.progressColor,
187+ backgroundColor: widget.backgroundColor,
188+ startAngle: widget.startAngle,
189+ circularStrokeCap: widget.circularStrokeCap,
190+ radius: (widget.radius / 2 ) - widget.lineWidth / 2 ,
191+ lineWidth: widget.lineWidth,
192+ arcBackgroundColor: widget.arcBackgroundColor,
193+ arcType: widget.arcType,
194+ reverse: widget.reverse,
195+ linearGradient: widget.linearGradient,
196+ maskFilter: widget.maskFilter),
197+ child: (widget.center != null )
198+ ? Center (child: widget.center)
199+ : Container (),
200+ )));
201+
202+ if (widget.footer != null ) {
203+ items.add (widget.footer);
204+ }
205+
206+ return Material (
207+ color: widget.fillColor,
208+ child: Container (
209+ child: Column (
210+ mainAxisAlignment: MainAxisAlignment .center,
211+ mainAxisSize: MainAxisSize .min,
212+ children: items,
213+ )),
214+ );
215+ }
216+
217+ @override
218+ bool get wantKeepAlive => widget.addAutomaticKeepAlive;
219+ }
220+
221+ class CirclePainter extends CustomPainter {
222+ final Paint _paintBackground = Paint ();
223+ final Paint _paintLine = Paint ();
224+ final Paint _paintBackgroundStartAngle = Paint ();
225+ final double lineWidth;
226+ final double progress;
227+ final double radius;
228+ final Color progressColor;
229+ final Color backgroundColor;
230+ final CircularStrokeCap circularStrokeCap;
231+ final double startAngle;
232+ final LinearGradient linearGradient;
233+ final Color arcBackgroundColor;
234+ final ArcType arcType;
235+ final bool reverse;
236+ final MaskFilter maskFilter;
237+
238+ CirclePainter (
239+ {this .lineWidth,
240+ this .progress,
241+ @required this .radius,
242+ this .progressColor,
243+ this .backgroundColor,
244+ this .startAngle = 0.0 ,
245+ this .circularStrokeCap = CircularStrokeCap .round,
246+ this .linearGradient,
247+ this .reverse,
248+ this .arcBackgroundColor,
249+ this .arcType,
250+ this .maskFilter}) {
251+ _paintBackground.color = backgroundColor;
252+ _paintBackground.style = PaintingStyle .stroke;
253+ _paintBackground.strokeWidth = lineWidth;
254+
255+ if (arcBackgroundColor != null ) {
256+ _paintBackgroundStartAngle.color = arcBackgroundColor;
257+ _paintBackgroundStartAngle.style = PaintingStyle .stroke;
258+ _paintBackgroundStartAngle.strokeWidth = lineWidth;
259+ }
260+
261+ _paintLine.color = progressColor;
262+ _paintLine.style = PaintingStyle .stroke;
263+ _paintLine.strokeWidth = lineWidth;
264+ if (circularStrokeCap == CircularStrokeCap .round) {
265+ _paintLine.strokeCap = StrokeCap .round;
266+ } else if (circularStrokeCap == CircularStrokeCap .butt) {
267+ _paintLine.strokeCap = StrokeCap .butt;
268+ } else {
269+ _paintLine.strokeCap = StrokeCap .square;
270+ }
271+ }
272+
273+ @override
274+ void paint (Canvas canvas, Size size) {
275+ final center = Offset (size.width / 2 , size.height / 2 );
276+ canvas.drawCircle (center, radius, _paintBackground);
277+
278+ if (maskFilter != null ) {
279+ _paintLine.maskFilter = maskFilter;
280+ }
281+ if (linearGradient != null ) {
282+ /*
283+ _paintLine.shader = SweepGradient(
284+ center: FractionalOffset.center,
285+ startAngle: math.radians(-90.0 + startAngle),
286+ endAngle: math.radians(progress),
287+ //tileMode: TileMode.mirror,
288+ colors: linearGradient.colors)
289+ .createShader(
290+ Rect.fromCircle(
291+ center: center,
292+ radius: radius,
293+ ),
294+ );*/
295+ _paintLine.shader = linearGradient.createShader (
296+ Rect .fromCircle (
297+ center: center,
298+ radius: radius,
299+ ),
300+ );
301+ }
302+
303+ double fixedStartAngle = startAngle;
304+
305+ double startAngleFixedMargin = 1.0 ;
306+ if (arcType != null ) {
307+ if (arcType == ArcType .FULL ) {
308+ fixedStartAngle = 220 ;
309+ startAngleFixedMargin = 172 / fixedStartAngle;
310+ } else {
311+ fixedStartAngle = 270 ;
312+ startAngleFixedMargin = 135 / fixedStartAngle;
313+ }
314+ }
315+
316+ if (arcBackgroundColor != null ) {
317+ canvas.drawArc (
318+ Rect .fromCircle (center: center, radius: radius),
319+ math.radians (- 90.0 + fixedStartAngle),
320+ math.radians (360 * startAngleFixedMargin),
321+ false ,
322+ _paintBackgroundStartAngle,
323+ );
324+ }
325+
326+ if (reverse) {
327+ final start =
328+ math.radians (360 * startAngleFixedMargin - 90.0 + fixedStartAngle);
329+ final end = math.radians (- progress * startAngleFixedMargin);
330+ canvas.drawArc (
331+ Rect .fromCircle (
332+ center: center,
333+ radius: radius,
334+ ),
335+ start,
336+ end,
337+ false ,
338+ _paintLine,
339+ );
340+ } else {
341+ final start = math.radians (- 90.0 + fixedStartAngle);
342+ final end = math.radians (progress * startAngleFixedMargin);
343+ canvas.drawArc (
344+ Rect .fromCircle (
345+ center: center,
346+ radius: radius,
347+ ),
348+ start,
349+ end,
350+ false ,
351+ _paintLine,
352+ );
353+ }
354+ }
355+
356+ @override
357+ bool shouldRepaint (CustomPainter oldDelegate) {
358+ return true ;
359+ }
360+ }
0 commit comments