写点什么

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

作者:程序那些事
  • 2023-05-24
    广东
  • 本文字数:4193 字

    阅读完需:约 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

发布于: 2023-05-24阅读数: 28
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020-06-07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论 (1 条评论)

发布
用户头像
很久没用Flutter了,推荐一下
-----来自版主
2023-05-26 08:52 · 广东
回复
没有更多了
flutter系列之:做一个下载按钮的动画_flutter_程序那些事_InfoQ写作社区