Flutter series: Make a download button animation

Introduction

In the development process of the app, we often use some animation effects that indicate progress, such as a download button. We hope that the button can dynamically display the progress of the download, so as to give the user some intuitive impressions, so a download button in Flutter How should the animation be produced?

Let’s take a look.

Define download status

Before we actually develop the download button, we first define several download states. Because different download states result in different button displays, we use the following enumeration class to set the download state of the button:

enum DownloadStatus {
  notDownloaded,
  fetchingDownload,
  downloading,
  downloaded,
}

There are basically 4 states, namely, not downloading, preparing to download but not yet getting the download resource link, getting the download resource and being downloaded, and finally downloading completed.

Define the properties of DownloadButton

Here we need to customize a DownloadButton component. This component must be a StatelessWidget. All state information is passed in from the outside.

We need to specify the style of DownloadButton according to the download status, so we need a status attribute. There is also a download progress bar during the download process, so we need a downloadProgress attribute.

In addition, the onDownload event will be triggered when the download button is clicked, the onCancel event can be triggered during the download process, and the onOpen event can be triggered after the download is completed.

Finally, because it is an animation component, it also needs an animation duration attribute transitionDuration.

So our DownloadButton needs the following properties:

class DownloadButton extends StatelessWidget {
  ...
  const DownloadButton({
    super.key,
    required this.status,
    this.downloadProgress = 0.0,
    required this.onDownload,
    required this.onCancel,
    required this.onOpen,
    this.transitionDuration = const Duration(milliseconds: 500),
  });

Let the properties of DownloadButton change dynamically

As mentioned above, DownloadButton is a StatelessWidget, and all properties are passed in from the outside. However, for an animated DownloadButton, information such as status and downloadProgress will change dynamically. So how can the changed properties be passed to DownloadButton? What about redrawing components in ?

Because it involves complex state changes, the simple AnimatedWidget can no longer meet our needs. Here we need to use the AnimatedBuilder component in flutter.

AnimatedBuilder is a subclass of AnimatedWidget. It has two necessary parameters, animation and builder.

Where animation is a Listenable object, it can be Animation, ChangeNotifier or etc.

AnimatedBuilder will rebuild the components in the builder by monitoring animation changes. The buidler method can obtain the corresponding change attributes from animation.

In this way, we create a Listenable DownloadController object, and then encapsulate the DownloadButton with AnimatedBuilder, so that we can monitor the changes in downloadStatus and downloadProgress in real time.

As follows:

Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('下载按钮')),
      body: Center(
        child: SizedBox(
          width: 96,
          child: AnimatedBuilder(
            animation: _downloadController,
            builder: (context, child) {
              return DownloadButton(
                status: _downloadController.downloadStatus,
                downloadProgress: _downloadController.progress,
                onDownload: _downloadController.startDownload,
                onCancel: _downloadController.stopDownload,
                onOpen: _downloadController.openDownload,
              );
            },
          ),
        ),
      ),
    );
  }

Define downloadController

downloadController is a Listenable object. Here we let it implement the ChangeNotifier interface and define two methods to obtain download status and download progress. It also defines three click trigger events:

abstract class DownloadController implements ChangeNotifier  {
  DownloadStatus get downloadStatus;
  double get progress;

  void startDownload();
  void stopDownload();
  void openDownload();
}

Next we implement this abstract method:

class MyDownloadController extends DownloadController
    with ChangeNotifier {
  MyDownloadController({
    DownloadStatus downloadStatus = DownloadStatus.notDownloaded,
    double progress = 0.0,
    required VoidCallback onOpenDownload,
  })  : _downloadStatus = downloadStatus,
        _progress = progress,
        _onOpenDownload = onOpenDownload;

The two methods startDownload and stopDownload are related to the download status and download progress. Let’s look at stopDownload first:

  void stopDownload() {
    if (_isDownloading) {
      _isDownloading = false;
      _downloadStatus = DownloadStatus.notDownloaded;
      _progress = 0.0;
      notifyListeners();
    }
  }

You can see that this method finally needs to call notifyListeners to notify AnimatedBuilder to redraw the component.

The startDownload method is a little more complicated. We need to simulate changes in download status and progress, as shown below:

  Future<void> _doDownload() async {
    _isDownloading = true;
    _downloadStatus = DownloadStatus.fetchingDownload;
    notifyListeners();

    // fetch耗时1秒钟
    await Future<void>.delayed(const Duration(seconds: 1));

    if (!_isDownloading) {
      return;
    }

    // 转换到下载的状态
    _downloadStatus = DownloadStatus.downloading;
    notifyListeners();

    const downloadProgressStops = [0.0, 0.15, 0.45, 0.8, 1.0];
    for (final progress in downloadProgressStops) {
      await Future<void>.delayed(const Duration(seconds: 1));
      if (!_isDownloading) {
        return;
      }
      //更新progress
      _progress = progress;
      notifyListeners();
    }

    await Future<void>.delayed(const Duration(seconds: 1));
    if (!_isDownloading) {
      return;
    }
    //切换到下载完毕状态
    _downloadStatus = DownloadStatus.downloaded;
    _isDownloading = false;
    notifyListeners();
  }
}

Because downloading is a relatively long process, the asynchronous method is used here, and the notification is performed in the asynchronous method.

Details of defining DownloadButton

With status and progress that can change dynamically, we can build a specific page display in DownloadButton.

Before starting the download, we hope that the downloadButton will be a long button with the text on the button showing GET. During the download process, we hope it will be an animation similar to CircularProgressIndicator, which can change dynamically according to the download progress.

At the same time, during the download process, we hope to hide the previous long bar button. After the download is completed, the long bar button is displayed again. At this time, the text on the button displays OPEN.

Because the animation is relatively complex, we divide the animation component into two parts. The first part is to display and hide the long button. Here we use AnimatedOpacity to achieve the fade-in and fade-out effect of the text, and encapsulate AnimatedOpacity in AnimatedContainer to implement decoration. Animation effects:

  return AnimatedContainer(
      duration: transitionDuration,
      curve: Curves.ease,
      width: double.infinity,
      decoration: shape,
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 6),
        child: AnimatedOpacity(
          duration: transitionDuration,
          opacity: isDownloading || isFetching ? 0.0 : 1.0,
          curve: Curves.ease,
          child: Text(
            isDownloaded ? 'OPEN' : 'GET',
            textAlign: TextAlign.center,
            style: Theme.of(context).textTheme.button?.copyWith(
              fontWeight: FontWeight.bold,
              color: CupertinoColors.activeBlue,
            ),
          ),
        ),
      ),
    );

The implementation effect is as follows:

Next, deal with the CircularProgressIndicator part:

 Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: 1,
      child: TweenAnimationBuilder<double>(
        tween: Tween(begin: 0, end: downloadProgress),
        duration: const Duration(milliseconds: 200),
        builder: (context, progress, child) {
          return CircularProgressIndicator(
            backgroundColor: isDownloading
                ? CupertinoColors.lightBackgroundGray
                : Colors.white.withOpacity(0),
            valueColor: AlwaysStoppedAnimation(isFetching
                ? CupertinoColors.lightBackgroundGray
                : CupertinoColors.activeBlue),
            strokeWidth: 2,
            value: isFetching ? null : progress,
          );
        },
      ),
    );
  }

TweenAnimationBuilder is used here to implement the animation effect of CircularProgressIndicator according to different progress.

Because there is a stop function during the download process, we put another stop icon on the CircularProgressIndicator, and finally encapsulate this stack in AnimatedOpacity to achieve an overall fade-in and fade-out function:

         Positioned.fill(
            child: AnimatedOpacity(
              duration: transitionDuration,
              opacity: _isDownloading || _isFetching ? 1.0 : 0.0,
              curve: Curves.ease,
              child: Stack(
                alignment: Alignment.center,
                children: [
                  ProgressIndicatorWidget(
                    downloadProgress: downloadProgress,
                    isDownloading: _isDownloading,
                    isFetching: _isFetching,
                  ),
                  if (_isDownloading)
                    const Icon(
                      Icons.stop,
                      size: 14,
                      color: CupertinoColors.activeBlue,
                    ),
                ],
              ),
            ),

Summarize

In this way, our animated download button is completed, and the effect is as follows:

Examples in this article:https://github.com/ddean2009/learn-flutter.git

Guess you like

Origin blog.csdn.net/superfjj/article/details/130849498