Cuenta atrás circular en flutter

En esta ocasión he creado un control en flutter para mostrar una cuenta atrás, con un circulo de progreso que, en el centro, tiene un texto que expresa el tiempo restante en horas, minutos y segundos, tal y como se muestra a continuación.

Diseñando el control

Para crearlo, he utilizado un StatefulWidget. Para definir su aspecto, he utilizado dos CircularProgressIndicator superpuestos con un Stack. El que queda detrás es el circulo gris que hace las veces de fondo. Y el que queda delante es el circulo azul que marca el progreso de la cuenta atrás.

Además, en el interior de esos CircularProgressIndicator he añadido un Text que muestra el tiempo restante en formato hh:mm:ss.

@override
Widget build(BuildContext context) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    crossAxisAlignment: CrossAxisAlignment.center,
    children: [
      Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Stack(
            children: [
              const SizedBox(
                height: widget.diameter,
                width: widget.diameter,
                child: CircularProgressIndicator(
                  value: 1,
                  color: Colors.black12,
                ),
              ),
              SizedBox(
                  height: widget.diameter,
                  width: widget.diameter,
                  child: Center(
                      child: Text(
                          "${_currentDuration.inHours.toString().padLeft(2, '0')}:${_currentDuration.inMinutes.remainder(60).toString().padLeft(2, '0')}:${(_currentDuration.inSeconds.remainder(60).toString().padLeft(2, '0'))}"))),
              SizedBox(
                height: widget.diameter,
                width: widget.diameter,
                child: CircularProgressIndicator(
                  value: _countdownPercentage,
                ),
              ),
            ],
          ),
        ],
      ),
    ],
  );
}

Añadiendo movimiento

Tal y como se muestra en el gif anterior, el control tiene una animación para modificar el progreso y el texto que marcan el tiempo restante. El control tiene cuatro métodos que permiten interactuar con esa animación para iniciarla, pausarla, reanudarla y pararla.

Asimismo, siempre que se lanza alguna de las acciones especificadas, se modifica el estado del control. Los posibles estados se han detallado en el siguiente enumerado.

enum CountdownStatus { stoped, playing, paused }
late AnimationController? _countdownController;
late Animation<double> _countdownAnimation;
late double _countdownPercentage;
late Duration _currentDuration;
late Duration _startDuration;
late CountdownStatus _countdownStatus;

@override
void initState() {
  super.initState();
  _countdownPercentage = 0;
  _currentDuration = Duration.zero;
  _startDuration = Duration.zero;
  _countdownStatus = CountdownStatus.stoped;
  widget.controller._state = this;
  _countdownController = null;
}

@override
void dispose() {
  _countdownController?.dispose();
  super.dispose();
}

startCountdown(Duration duration) {
  if (_countdownController != null) {
    return;
  }

  _startDuration = duration;
  _countdownController = AnimationController(duration: _startDuration, vsync: this);
  _countdownAnimation = Tween<double>(begin: duration.inSeconds.toDouble(), end: 0).animate(_countdownController!)
    ..addListener(() {
      setState(() {
        _countdownPercentage = _countdownController!.value;
        _currentDuration = Duration(seconds: _countdownAnimation.value.round());
      });
    });
  widget.controller._state = this;
  _countdownController!.forward();
  _countdownStatus = CountdownStatus.playing;
}

pauseCountdown() {
  if (_countdownController != null) {
    _countdownController!.stop();
    _countdownStatus = CountdownStatus.paused;
  }
}

stopCountdown() {
  if (_countdownController != null) {
    _countdownController!.stop();
    _countdownController!.dispose();
    _countdownController = null;
    _countdownStatus = CountdownStatus.stoped;

    setState(() {
      _countdownPercentage = 0;
      _currentDuration = _startDuration;
    });
  }
}

resumeCountdown() {
  if (_countdownController != null) {
    _countdownController!.forward();
    _countdownStatus = CountdownStatus.playing;
  }
}

Manejando el control

Al ser un control que queremos utilizar desde cualquier widget, es necesario añadir funcionalidad para conseguirlo. En este caso, he añadido un controlador que encapsula las funcionalidades que podemos lanzar sobre nuestro control

class CountdownController {
  _CountdownControlState? _state;

  void playCountdown(Duration duration) {
    _state?.startCountdown(duration);
  }

  void pauseCountdown() {
    _state?.pauseCountdown();
  }

  void resumeCountdown() {
    _state?.resumeCountdown();
  }

  void stopCountdown() {
    _state?.stopCountdown();
  }

  CountdownStatus getStatus() {
    return _state?._countdownStatus ?? CountdownStatus.stoped;
  }

  void dispose() {
    _state?.dispose();
  }
}

Con todo lo anterior, podemos crear el control indicándole el controlador y el diámetro. Gracias al controlador, podemos saber el estado del control y lanzar las distintas funcionalidades que nos ofrece.

late CountdownController _countdownController;
...

@override
void initState() {
  super.initState();
  ...
  _countdownController = CountdownController();
}

@override
void dispose() {
  _countdownController.dispose();
  ...
  super.dispose();
}

@override
Widget build(BuildContext context) {
  return 
    ...
    CountdownControl(controller: _countdownController, diameter: 300)
    ...
}

Al igual que en otras ocasiones, el código de este ejemplo está disponible en mi repositorio de Github https://github.com/jorgediegocrespo/FlutterCountdown

Foto de Lukas Blazek en Unsplash

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.