Search code examples
flutterflutter-animationflutter-circularprogressindicator

Circular Progress Indicator Around a Button that Progresses On a Timer


What I have is a play button that plays back user recorded message. Once the user hits the play button it changes into a stop button that displays a circular progress indicator that progresses based on a percentage of the recorded message total time and the current track time.

What I have somewhat worked, but isn't exact enough, depending on how long the track is (4 seconds vs 6.5 seconds) the track will run a bit longer after the circular progress indicator is done or the track will end before the progress indicator is done.

I would also love the progression to be smooth and not jump in intervals. enter image description here

Here is the code for the stop button which takes a double totalTime which is the total time of the track playing and then starts a timer and AnimationController.

Also full transparency, the custom painter is something I found online and don't fully understand how it works, so even if there isn't an issue there if someone could break that down it would be seriously appreciated :D

import 'dart:async';
import 'dart:math';
import 'dart:ui';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_icons/flutter_icons.dart';

class StopButton extends StatefulWidget {
  @override
  _StopButtonState createState() => _StopButtonState();

  final double totalTime;
  final dynamic onClickFunction;
  StopButton(this.totalTime, this.onClickFunction);
}

class _StopButtonState extends State<StopButton> with TickerProviderStateMixin {
  double percentage = 0.0;
  double newPercentage = 0.0;
  AnimationController percentageAnimationController;
  Timer timer;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    setState(() {
      percentage = 0.0;
    });
    percentageAnimationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
          ..addListener(() {
            setState(() {
              percentage = lerpDouble(percentage, newPercentage,
                  percentageAnimationController.value);
            });
          });
    startTime();
  }

  @override
  void dispose() {
    // TODO: implement dispose
    timer.cancel();
    percentageAnimationController.dispose();
    super.dispose();
  }

  void startTime() {
    setState(() {
      percentage = newPercentage;
      newPercentage += 0.0;
      if (newPercentage > widget.totalTime) {
        percentage = 0.0;
        newPercentage = 0.0;
        timer.cancel();
      }
      percentageAnimationController.forward(from: 0.0);
    });
    timer = Timer.periodic(Duration(seconds: 1), (timer) {
      print(timer.tick);
      setState(() {
        percentage = newPercentage;
        newPercentage += 1.0;
        if (newPercentage > widget.totalTime) {
          percentage = 0.0;
          newPercentage = 0.0;
          timer.cancel();
        }
        percentageAnimationController.forward(from: 0.0);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: MyPainter(
        lineColor: Colors.transparent,
        completeColor: Color(0xFF133343),
        completePercent: percentage,
        width: 3.0,
        totalTime: widget.totalTime,
      ),
      child: Padding(
        padding: EdgeInsets.all(0.0),
        child: FloatingActionButton(
          onPressed: () async {
            await widget.onClickFunction();
          },
          backgroundColor: Colors.white,
          child: Icon(
            MaterialCommunityIcons.stop,
            color: Color(0xFF133343),
          ),
        ),
      ),
    );
  }
}

class MyPainter extends CustomPainter {
  Color lineColor;
  Color completeColor;
  double completePercent;
  double width;
  double totalTime;
  MyPainter(
      {this.lineColor,
      this.completeColor,
      this.completePercent,
      this.width,
      this.totalTime});
  @override
  void paint(Canvas canvas, Size size) {
    Paint line = Paint()
      ..color = lineColor
      ..strokeCap = StrokeCap.round
      ..style = PaintingStyle.stroke
      ..strokeWidth = width;
    Paint complete = Paint()
      ..color = completeColor
      ..strokeCap = StrokeCap.round
      ..style = PaintingStyle.stroke
      ..strokeWidth = width;
    Offset center = Offset(size.width / 2, size.height / 2);
    double radius = min(size.width / 2, size.height / 2);
    canvas.drawCircle(center, radius, line);
    double arcAngle = 2 * pi * (completePercent / totalTime);
    canvas.drawArc(Rect.fromCircle(center: center, radius: radius), -pi / 2,
        arcAngle, false, complete);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

Solution

  • you have add below custom CustomTimerPainter for create circular indicator

     class CustomTimerPainter extends CustomPainter {
      CustomTimerPainter({
        this.animation,
        this.backgroundColor,
        this.color,
      }) : super(repaint: animation);
    
      final Animation<double> animation;
      final Color backgroundColor, color;
    
      @override
      void paint(Canvas canvas, Size size) {
        Paint paint = Paint()
          ..color = backgroundColor
          ..strokeWidth = 6.0
          ..strokeCap = StrokeCap.butt
          ..style = PaintingStyle.stroke;
    
        canvas.drawCircle(size.center(Offset.zero), size.width / 2.0, paint);
        paint.color = color;
        double progress = (1.0 - animation.value) * 2 * math.pi;
        canvas.drawArc(Offset.zero & size, math.pi * 1.5, progress, false, paint);
      }
    
      @override
      bool shouldRepaint(CustomTimerPainter old) {
        return animation.value != old.animation.value ||
            color != old.color ||
            backgroundColor != old.backgroundColor;
      }
    }
    

    after adding indicator define controller

    AnimationController controller;
    
    @override
        void initState() {
          super.initState();
          controller = AnimationController(
          vsync: this,
          duration: Duration(seconds: 5),
         );
       }
    

    last step is add our custom painter

    floatingActionButton: Container(
        height: 60,
        width: 60,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: Colors.white
        ),
        child: GestureDetector(
          child: CustomPaint(
              painter: CustomTimerPainter(
                animation: controller,
                backgroundColor: Colors.white,
                color: themeData.indicatorColor,
              )),
          onTap: (){
            controller.reverse(
                from: controller.value == 0.0
                    ? 1.0
                    : controller.value);
          },
        ),
      ),