

flutter系列之:做一个下载按钮的动画
source link: http://www.flydean.com/32-flutter-ui-animate-download-button/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我们在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,
),
],
),
),
这样,我们一个动画的下载按钮就制作完成了,效果如下:

Recommend
-
59
like_button 做这个按钮的起因是,产品突然说要改一下点赞效果,像下面这样。 但是我心中的审美不是这样的,不可以,坚决不行。 想起来群花拉面写过一个用Flutter实现一个仿Twitter的点赞效果. 看了下代码, 站在大佬们的肩膀 jd-alex
-
15
需具备的条件 若要顺利阅读本篇文章, 需要你具备如下条件: 本篇文章的环境: 环境版本Windows10Android Studio3.5Flutter1.19.0-2.0.pre一定要注意环境的差异, 考虑不兼容的可能性; 并且具备以上条件. 否则阅读本篇...
-
8
在实际项目开发中, 我们在进行页面跳转时, 偶尔会跳到不可返回的页面(比如退出登录后). 本篇文章就来记录下 Flutter 如何隐藏/移除导航栏的默认返回按钮. 有图有真相, 先来看下实际效果: 移除导航栏的默认返回按钮适用于:
-
6
https://medium.com/flutterdev...Flutter 允许您使用 FloatingActi...
-
7
Flutter 自定义单选按钮h...
-
4
flutter中有默认的Route组件,叫做MaterialPageRoute,一般情况下我们在flutter中进行跳转的话,只需要向Navigator中传入一个MaterialPageRoute就可以了。 但是MaterialPageRoute太普通了,如果我们想要做点不同的跳转特效应该如何处理呢? 一起来看看...
-
8
flutter中有默认的Route组件,叫做MaterialPageRoute,一般情况下我们在flutter中进行跳转的话,只需要向Navigator中传入一个MaterialPageRoute就可以了。 但是MaterialPageRoute太普通了,如果我们想要做点不同的跳转特效应该如何处理呢? 一起来看看...
-
6
什么是动画呢?动画实际上就是不同的图片连续起来形成的。flutter为我们提供了一个AnimationController来对动画进行详尽的控制,不过直接是用AnimationController是比较复杂的,如果只是对一个widget的属性进行修改,可以做成动画吗? 答案是肯定的,一起来看...
-
10
之前我们提到了flutter提供了比较简单好用的AnimatedContainer和SlideTransition来进行一些简单的动画效果,但是要完全实现自定义的复杂的动画效果,还是要使用AnimationController。 今天我们来尝试使用AnimationController来实现一个拖拽图片,然后返回原点...
-
6
之前我们提到了flutter提供了比较简单好用的AnimatedContainer和SlideTransition来进行一些简单的动画效果,但是要完全实现自定义的复杂的动画效果,还是要使用AnimationController。 今天我们来尝试使用AnimationController来实现一个拖拽图片,然后返回原点...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK