简介
咱们在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