我們在app的開發過程中經常會用到一些表示進度類的動畫效果,比如一個下載按鈕,我們希望按鈕能夠動態顯示下載的進度,這樣可以給使用者一些直觀的印象,那麼在flutter中一個下載按鈕的動畫應該如何製作呢?
一起來看看吧。
我們在真正開發下載按鈕之前,首先定義幾個下載的狀態,因為不同的下載狀態導致的按鈕展示樣子也是不一樣的,我們用下面的一個列舉類來設定按鈕的下載狀態:
enum DownloadStatus {
notDownloaded,
fetchingDownload,
downloading,
downloaded,
}
基本上有4個狀態,分別是沒有下載,準備下載但是還沒有獲取到下載的資源連結,獲取到下載資源正在下載中,最後是下載完畢。
這裡我們需要自定義一個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是一個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是一個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是一個長條形的按鈕,按鈕上的文字顯示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