Shutter circle ellipse gradient countdown progress bar

Time:2022-6-6

At present, the following effects are achieved, supporting solid color and gradient

Shutter circle ellipse gradient countdown progress bar

use

class _CircleProgressPageState extends State<CircleProgressPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text(
        "Cycle progress",
        style: TextStyle(),
      )),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircleProgressIndicatorWidget(
              width: 100,
              height: 100,
              value: 1,
              gradient: SweepGradient(colors: [Colors.red, Colors.yellow, Colors.red,], stops: [0.3, 0.5, 1]),
              strokeWidth: 10,
              totalDuration: Duration(
                seconds: 10,
              ),
              child: (context, c) => Center(
                child: Text(
                  "${((1 - c.value) * 100).toInt()}%",
                  style: TextStyle(
                    
                  ),
                ),
              ),
            ),
            SizedBox(
              width: 100,
              height: 150,
              child: CircleProgressIndicatorWidget(
                value: 0.8,
                color: Colors.red,
                strokeWidth: 10,
                totalDuration: Duration(
                  seconds: 10,
                ),
                child: (context, c) => Center(
                  child: Text(
                    "${((1 - c.value) * 100).toInt()}%",
                    style: TextStyle(
                      
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Width and height support internal assignment or external restriction,
Value is the progress value, and the range is [0,1],
Only one color and gradient can be selected,
Child will call back a function to display the current UI. The returned value is the value of the animationcontroller, which needs to be reversed

There’s nothing else to say. Just tell me some small problems I need to solve when I write

problem

  1. rotate
    The default drawarc starts from the clockwise direction of the X axis. At first, I wanted to use -pi/2 negative angle, but I found that the shader gradient cannot customize the angle. It also starts from the X axis, and then I thought of rotating the layer and turning the X axis to the Y axis. However, at this time, I should pay attention to
.. Translate (centerpoint.x, centerpoint.y) // because rotate takes (0,0) as the anchor point
      .. Rotate (-pi / 2) // rotate 90 degrees so that the starting angle is the Y axis
      ..translate(-centerPoint.y, -centerPoint.x)

X and y are the opposite at the time of recovery
2. end fillet
I set it up for the progress bar at first..strokeCap = StrokeCap.round;However, the 0,1 coincidence position of the gradient color is very abrupt. For a time, I wanted to give up the origin of the gradient. Later, I decided to insert the same color as the head into the tail of the gradient array to increase the appearance. This requires the user’s own attention
3. arc rect
The center point of the width of the ring is just at the boundary of size. If readers want to completely limit the outer radius of this radius to fit the width and height of rect, they need to translate the center store, and then reduce the width of strokewidth / 2. In fact, this is very simple. I won’t write it, and the rest will be posted directly

Source code

import 'dart:math';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter_psd/common/utils/psdllog.dart';

typedef CircleProgressIndicatorChildBuilder = Widget Function(
    BuildContext context, AnimationController animationController);

class CircleProgressIndicatorWidget extends StatefulWidget {
  ///Progress value 0~1
  final double value;

  ///Solid)
  final Color? color;

  ///Gradient
  final Gradient? gradient;

  ///Line width
  final double? strokeWidth;

  ///Child controls
  final CircleProgressIndicatorChildBuilder? child;

  ///Total time required to complete a turn
  final Duration totalDuration;

  ///Width
  final double? width;

  ///High
  final double? height;

  CircleProgressIndicatorWidget({
    Key? key,
    required this.value,
    required this.totalDuration,
    this.color,
    this.gradient,
    this.strokeWidth,
    this.child,
    this.width,
    this.height,
  }): assert ((color! = null & & gradient! = null) = = false, "error: solid color, there can only be one gradient"),
        super(key: key);

  @override
  _CircleProgressIndicatorWidgetState createState() =>
      _CircleProgressIndicatorWidgetState();
}

class _CircleProgressIndicatorWidgetState
    extends State<CircleProgressIndicatorWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController animationController;

  double get _animateValue =>
      animationController.upperBound - animationController.value;

  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      vsync: this,
      lowerBound: 0,
      upperBound: widget.value,
      duration: widget.totalDuration * widget.value,
    )
      ..addListener(
        () {
          setState(() {});
        },
      )
      ..repeat();
  }

  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
    Psdllog ('$runtimetype released ');
  }

  @override
  Widget build(BuildContext context) {
    if (_animateValue == animationController.upperBound) {
      //To the highest point
      return Container();
    }
    return AnimatedBuilder(
      animation: animationController,
      builder: (BuildContext context, Widget? child) {
        return Container(
          width: widget.width ?? double.infinity,
          height: widget.height ?? double.infinity,
          color: Colors.amberAccent.withOpacity(0.3),
          child: CustomPaint(
            painter: _CircleProgressPainter(
              value: _animateValue,
              color: widget.color,
              gradient: widget.gradient,
              strokeWidth: widget.strokeWidth ?? 10,
            ),
            child: widget.child != null
                ? widget.child!(context, animationController)
                : null,
          ),
        );
      },
    );
  }
}

class _CircleProgressPainter extends CustomPainter {
  late Paint _paint;

  /// 0-1
  double value;
  Color? color;
  Gradient? gradient;
  double strokeWidth;

  _CircleProgressPainter({
    required this.value,
    required this.strokeWidth,
    this.color,
    this.gradient,
  }) : super();
  @override
  void paint(Canvas canvas, Size size) {
    var centerPoint = Point(size.width / 2, size.height / 2);

    _paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round;
    if (color != null) {
      _paint.color = this.color!;
      // _ paint. strokeCap = StrokeCap. round; //  The end point becomes round, and the effect is not good when the color gradient is set, so this effect is only available when the end point is set to solid color
    }
    if (gradient != null) {
      _paint.shader = ui.Gradient.sweep(Offset(centerPoint.y, centerPoint.x),
          gradient!.colors, gradient!.stops);
      // _paint.strokeCap = StrokeCap.round;
    }
    canvas
      .. Translate (centerpoint.x, centerpoint.y) // because rotate takes (0,0) as the anchor point
      .. Rotate (-pi / 2) // rotate 90 degrees so that the starting angle is the Y axis
      ..translate(-centerPoint.y, -centerPoint.x)
      ..drawArc(Rect.fromLTWH(0, 0, size.height, size.width), 0, 2 * pi * value,
          false, _paint);
  }

  @override
  bool shouldRepaint(covariant _CircleProgressPainter oldDelegate) {
    return value != oldDelegate.value;
  }
}

Ps: because this widget is still relatively simple, it took a little time to throw out all the parameters and formally make it into a component. Later, I don’t know what to explore. Maybe it’s a layer or an overlay. Let’s set up a flag and write another one in two days, taking advantage of the lack of life at the beginning of the year

Update 2022-02-10 14:41:32

Add a parameter to determine whether it is clockwise. The effect can be made up by your own brain. By default, the Y central axis is (0, size.height), and a little translation is required

if (clockwise == true) {
      //Clockwise countdown
      var transform = Matrix4.identity()..rotateY(pi);
      canvas
        .. Translate (centerpoint.x, centerpoint.y) // because rotate takes (0,0) as the anchor point
        .. Transform (transform.storage) // rotate 180 degrees
        ..translate(-centerPoint.x, -centerPoint.y);
    }