关于flutter:flutter系列之做一个下载按钮的动画

34次阅读

共计 5541 个字符,预计需要花费 14 分钟才能阅读完成。

简介

咱们在 app 的开发过程中常常会用到一些示意进度类的动画成果,比方一个下载按钮,咱们心愿按钮可能动态显示下载的进度,这样能够给用户一些直观的印象,那么在 flutter 中一个下载按钮的动画应该如何制作呢?

一起来看看吧。

定义下载的状态

咱们在真正开发下载按钮之前,首先定义几个下载的状态,因为不同的下载状态导致的按钮展现样子也是不一样的,咱们用上面的一个枚举类来设置按钮的下载状态:

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

基本上有 4 个状态,别离是没有下载,筹备下载然而还没有获取到下载的资源链接,获取到下载资源正在下载中,最初是下载结束。

定义 DownloadButton 的属性

这里咱们须要自定义一个 DownloadButton 组件,这个组件必定是一个 StatelessWidget,所有的状态信息都是由内部传入的。

咱们须要依据下载状态来指定 DownloadButton 的款式,所以须要一个 status 属性。下载过程中还有一个下载的进度条,所以咱们须要一个 downloadProgress 属性。

另外在点击下载按钮的时候会触发 onDownload 事件,下载过程中能够触发 onCancel 事件,下载结束之后能够登程 onOpen 事件。

最初因为是一个动画组件,所以还须要一个动画的持续时间属性 transitionDuration。

所以咱们的 DownloadButton 须要上面一些属性:

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

让 DownloadButton 的属性能够动态变化

下面提到了 DownloadButton 是一个 StatelessWidget,所有的属性都是由内部传入的,然而对于一个动画的 DownloadButton 来说,status,downloadProgress 这些信息都是会动态变化的,那么怎么能力让变动的属性传到 DownloadButton 中进行组件的重绘呢?

因为波及到简单的状态变动,所以简略的 AnimatedWidget 曾经满足不了咱们的需要了,这里就须要用到 flutter 中的 AnimatedBuilder 组件了。

AnimatedBuilder 是 AnimatedWidget 的子类,它有两个必须的参数,别离是 animation 和 builder。

其中 animation 是一个 Listenable 对象,它能够是 Animation,ChangeNotifier 或者等。

AnimatedBuilder 会通过监听 animation 的变动状况,来从新构建 builder 中的组件。buidler 办法能够从 animation 中获取对应的变动属性。

这样咱们创立一个 Listenable 的 DownloadController 对象,而后把 DownloadButton 用 AnimatedBuilder 封装起来,就能够实时监测到 downloadStatus 和 downloadProgress 的变动了。

如下所示:

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

定义 downloadController

downloadController 是一个 Listenable 对象, 这里咱们让他实现 ChangeNotifier 接口, 并且定义了两个获取下载状态和下载进度的办法,同时也定义了三个点击触发事件:

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

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

接下来咱们来实现这个形象办法:

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

startDownload,stopDownload 这两个办法是跟下载状态和下载进度相干的,先看下 stopDownload:

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

能够看到这个办法最初须要调用 notifyListeners 来告诉 AnimatedBuilder 来进行组件的重绘。

startDownload 办法会简单一点,咱们须要模仿下载状态的变动和进度的变动,如下所示:

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

因为下载是一个比拟长的过程,所以这里用的是异步办法,在异步办法中进行告诉。

定义 DownloadButton 的细节

有了能够动态变化的状态和进度之后,咱们就能够在 DownloadButton 中构建具体的页面展现了。

在未开始下载之前,咱们心愿 downloadButton 是一个长条形的按钮,按钮上的文字显示 GET, 下载过程中心愿是一个相似 CircularProgressIndicator 的动画,能够依据下载进度来动态变化。

同时,在下载过程中,咱们心愿可能暗藏之前的长条形按钮。下载结束之后,再次展现长条形按钮, 这时候按钮上的文字显示为 OPEN。

因为动画比较复杂,所以咱们将动画组件分成两局部,第一局部就是展现和暗藏长条形的按钮,这里咱们应用 AnimatedOpacity 来实现文字的淡入淡出的成果,并将 AnimatedOpacity 封装在 AnimatedContainer 中,实现 decoration 的动画成果:

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

实现成果如下所示:

接下来再解决 CircularProgressIndicator 的局部:

 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 来实现 CircularProgressIndicator 依据不同 progress 的动画成果。

因为在下载过程中,还有进行的性能,所以咱们在 CircularProgressIndicator 上再放一个 stop icon,最初将这个 stack 封装在 AnimatedOpacity 中,实现整体的一个淡入淡出性能:

         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,
                    ),
                ],
              ),
            ),

总结

这样,咱们一个动画的下载按钮就制作实现了,成果如下:

本文的例子:https://github.com/ddean2009/learn-flutter.git

正文完
 0