In this section, we will implement a circular background gradient progress bar with the following features:
Support for multiple background gradient colors.
Arbitrary arc angles; the progress bar does not need to form a complete circle.
Customizable thickness, rounded ends, and other styles.
As we can see, it's impossible to achieve such a progress bar using existing components alone, so we will implement it through custom drawing. The code is as follows:
import 'dart:math'; import 'package:flutter/material.dart'; class GradientCircularProgressIndicator extends StatelessWidget { const GradientCircularProgressIndicator({ Key? key, this.strokeWidth = 2.0, required this.radius, required this.colors, this.stops, this.strokeCapRound = false, this.backgroundColor = const Color(0xFFEEEEEE), this.totalAngle = 2 * pi, this.value, }) : super(key: key); /// Thickness final double strokeWidth; /// Radius of the circle final double radius; /// Whether the ends are rounded final bool strokeCapRound; /// Current progress, value range [0.0-1.0] final double value; /// Progress bar background color final Color backgroundColor; /// Total angle of the progress bar; 2*PI forms a complete circle, less than 2*PI forms an arc final double totalAngle; /// Gradient colors final List<Color> colors; /// Stops for the gradient, corresponding to the `colors` attribute final List<double> stops; @override Widget build(BuildContext context) { double _offset = .0; // If the ends are rounded, we need to adjust the starting position to avoid misalignment // The formula for this adjustment is derived from geometric principles. Feel free to explore why this is the case. if (strokeCapRound) { _offset = asin(strokeWidth / (radius * 2 - strokeWidth)); } var _colors = colors; if (_colors == null) { Color color = Theme.of(context).colorScheme.secondary; _colors = [color, color]; } return Transform.rotate( angle: -pi / 2.0 - _offset, child: CustomPaint( size: Size.fromRadius(radius), painter: _GradientCircularProgressPainter( strokeWidth: strokeWidth, strokeCapRound: strokeCapRound, backgroundColor: backgroundColor, value: value, total: totalAngle, radius: radius, colors: _colors, ) ), ); } } // Painter implementation class _GradientCircularProgressPainter extends CustomPainter { _GradientCircularProgressPainter({ this.strokeWidth = 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 strokeWidth; 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 = strokeWidth / 2.0; double _value = (value ?? .0); _value = _value.clamp(.0, 1.0) * total; double _start = .0; if (strokeCapRound) { _start = asin(strokeWidth/ (size.width - strokeWidth)); } Rect rect = Offset(_offset, _offset) & Size( size.width - strokeWidth, size.height - strokeWidth ); var paint = Paint() ..strokeCap = strokeCapRound ? StrokeCap.round : StrokeCap.butt ..style = PaintingStyle.stroke ..isAntiAlias = true ..strokeWidth = strokeWidth; // Draw the background if (backgroundColor != Colors.transparent) { paint.color = backgroundColor; canvas.drawArc( rect, _start, total, false, paint ); } // Draw the foreground with gradient if (_value > 0) { paint.shader = SweepGradient( startAngle: 0.0, endAngle: _value, colors: colors, stops: stops, ).createShader(rect); canvas.drawArc( rect, _start, _value, false, paint ); } } // Always return true for now; in practice, this should depend on whether the painter's properties have changed. @override bool shouldRepaint(CustomPainter oldDelegate) => true; }
Next, let's test it. To showcase the various appearances and uses of the GradientCircularProgressIndicator
, this example code is relatively long and includes animations. We recommend that readers run the example to see the actual effects. Below is a snapshot of one frame of the animation (Figure):
Example Code:
import 'dart:math'; import 'package:flutter/material.dart'; import '../widgets/index.dart'; class GradientCircularProgressRoute extends StatefulWidget { const GradientCircularProgressRoute({Key? key}) : super(key: key); @override GradientCircularProgressRouteState createState() { return GradientCircularProgressRouteState(); } } class GradientCircularProgressRouteState extends State<GradientCircularProgressRoute> with TickerProviderStateMixin { late AnimationController _animationController; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const 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, 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: const [Colors.blue, Colors.blue], radius: 50.0, strokeWidth: 3.0, value: _animationController.value, ), GradientCircularProgressIndicator( colors: const [Colors.red, Colors.orange], radius: 50.0, strokeWidth: 3.0, value: _animationController.value, ), GradientCircularProgressIndicator( colors: const [Colors.red, Colors.orange, Colors.red], radius: 50.0, strokeWidth: 5.0, value: _animationController.value, ), GradientCircularProgressIndicator( colors: const [Colors.teal, Colors.cyan], radius: 50.0, strokeWidth: 5.0, strokeCapRound: true, value: CurvedAnimation( parent: _animationController, curve: Curves.decelerate, ).value, ), TurnBox( turns: 1 / 8, child: GradientCircularProgressIndicator( colors: const [Colors.red, Colors.orange, Colors.red], radius: 50.0, strokeWidth: 5.0, strokeCapRound: true, backgroundColor: Colors.red.shade50, totalAngle: 1.5 * pi, value: CurvedAnimation( parent: _animationController, curve: Curves.ease, ).value, ), ), RotatedBox( quarterTurns: 1, child: GradientCircularProgressIndicator( colors: [ Colors.blue.shade700, Colors.blue.shade200 ], radius: 50.0, strokeWidth: 3.0, strokeCapRound: true, backgroundColor: Colors.transparent, value: _animationController.value, ), ), GradientCircularProgressIndicator( colors: [ Colors.red, Colors.amber, Colors.cyan, Colors.green.shade200, Colors.blue, Colors.red ], radius: 50.0, strokeWidth: 5.0, strokeCapRound: true, value: _animationController.value, ), ], ), GradientCircularProgressIndicator( colors: [Colors.blue.shade700, Colors.blue.shade200], radius: 100.0, strokeWidth: 20.0, value: _animationController.value, ), Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: GradientCircularProgressIndicator( colors: [Colors.blue.shade700, Colors.blue.shade300], radius: 100.0, strokeWidth: 20.0, value: _animationController.value, strokeCapRound: true, ), ), // Clip half-circle ClipRect( child: Align( alignment: Alignment.topCenter, heightFactor: .5, child: Padding( padding: const EdgeInsets.only(bottom: 8.0), child: SizedBox( child: TurnBox( turns: .75, child: GradientCircularProgressIndicator( colors: [Colors.teal, Colors.cyan.shade500], radius: 100.0, strokeWidth: 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.shade500], radius: 100.0, strokeWidth: 8.0, value: _animationController.value, totalAngle: pi, strokeCapRound: true, ), ), ), Padding( padding: const EdgeInsets.only(top: 10.0), child: Text( "${(_animationController.value * 100).toInt()}%", style: const TextStyle( fontSize: 25.0, color: Colors.blueGrey, ), ), ) ], ), ), ], ), ); }, ), ], ), ), ); } }
How cool is that! The GradientCircularProgressIndicator
has been added to the author's maintained Flukit component library. Readers can directly depend on the Flukit package if needed.