Preston Schwartz
Preston Schwartz

Reputation: 49

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;
  }
}

Upvotes: 1

Views: 3361

Answers (1)

Chirag Kalathiya
Chirag Kalathiya

Reputation: 414

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);
      },
    ),
  ),

Upvotes: 7

Related Questions