到目前為止建立的動畫,只是單個方向,如果想建立往復式的動畫呢?像是自動地重複放大縮小?透過隱式動畫 Widget,也不是辦不到,就是比較麻煩。例如:
import 'package:flutter/material.dart';
void main() => runApp(
MaterialApp(
home: Caterpillar(),
)
);
class Caterpillar extends StatefulWidget {
@override
State<StatefulWidget> createState() => _CaterpillarState();
}
class _CaterpillarState extends State<Caterpillar> {
var zoom_in = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('openhome.cc'),
),
body: Center(
child: GestureDetector(
// 點一下播放動畫
onTap: () {
setState(() {
zoom_in = !zoom_in;
});
},
child: AnimatedContainer(
width: zoom_in ? 300 : 100,
child: Image.network('https://openhome.cc/Gossip/images/caterpillar.jpg'),
duration: Duration(seconds: 1),
// 在動畫播放完畢後重置狀態
onEnd: () {
setState(() {
zoom_in = !zoom_in;
});
},
),
)
),
);
}
}
在這個範例中,藉由 zoom_in
來改變 AnimatedContainer
的寬度,若在 AnimatedContainer
播放動畫結束後,改變 zoom_in
並重置狀態,那麼就會寬度就會相反,這時就會再度播放動畫,效果如下:
那麼…怎麼停止動畫呢?呃…或許再加個 isForward
的變數,判斷是否在 onEnd
時改變 zoom_in
之類的…不過…很麻煩…
如果你想要的動畫,不只是單向播放,而想要能控制正向、反向、停止、播放等細節時,可以改用隱式動畫 Widget,具體來說,這些 Widget 是 AnimatedWidget
的子類,在 API 文件中可以看到,Flutter 也內建了一些隱式動畫 Widget,例如這邊來使用 ScaleTransition
處理縮放:
import 'package:flutter/material.dart';
void main() => runApp(
MaterialApp(
home: Caterpillar(),
)
);
class Caterpillar extends StatefulWidget {
@override
State<StatefulWidget> createState() => _CaterpillarState();
}
class _CaterpillarState extends State<Caterpillar> with SingleTickerProviderStateMixin {
var beat = false;
AnimationController scaleController;
@override
void initState() {
super.initState();
// 建立 AnimationController
scaleController = AnimationController(
lowerBound: 0.75,
upperBound: 1.0,
duration: Duration(seconds: 1),
vsync: this
);
scaleController.stop();
}
// 記得釋放 `AnimationController`
@override
void dispose() {
scaleController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('openhome.cc'),
),
body: Center(
child: ScaleTransition(
child: Image.network('https://openhome.cc/Gossip/images/caterpillar.jpg'),
alignment: Alignment.center,
scale: scaleController,
)
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() {
beat = !beat;
if(beat) {
scaleController.repeat();
}
else {
scaleController.stop();
}
}),
tooltip: beat ? 'Stop' : 'Beat',
child: Icon(beat ? Icons.stop : Icons.favorite),
),
);
}
}
這邊有幾個重點,首先是 AnimationController
,其實在〈分頁工具列 TabBar〉有稍微提過,TabBar
的動畫在內部就是由它來控制,顯式動畫 Widget 之所以為顯式,就在於你必須顯式地處理 AnimationController
。
動畫相關的邏輯依 AnimationController
的 value
等資訊來產生,value
預設會是 0.0 到 1.0,對 ScaleTransition
來說,這就是它的縮放值,也就是兩者預設的組合下,圖片會從無到有地放大,你可以自行設定 lowerBound
與 upperBound
,作為 value
的變化範圍。
duration
就是動畫持續時間,接著是 vsync
,Flutter 基本上希望能在畫面顯示時,提供每秒 60 個畫框(frame)的更新,而 TickerProvider
可以提供 Ticker
實例,每次 Flutter 在更新畫框(Frame),會呼叫指定給 Ticker
的函式,這個函式中可以操作 AnimationController
,例如更新它的 value
,動畫相關的邏輯依 AnimationController
的 value
等資訊來產生動畫。
簡單來說,AnimationController
需要 TickerProvider
,這邊暫時不接觸 TickerProvider
等細節,只 mixin 了 SingleTickerProviderStateMixin
,如此之來,_CaterpillarState
就實作了 TickerProvider
,將這個實例(this
)指定給 AnimationController
的 vsync
就可以了。
最後記得,AnimationController
不會自動釋放,因此重新實作了 dispose
,在 _CaterpillarState
釋放資源時也釋放 AnimationController
。
ScaleTransition
的 scale
特性,需要 Animation<double>
,AnimationController
實作了 Animation<double>
,如前描述,這是為了取得它的 value
來作為縮放依據,alignment
指定了縮放的原點,這邊是以圖片中心縮放。
AnimationController
本身可以有 forward
、reverse
、stop
、repeat
等方法來操控 value
的計算,從而操作動畫的進行,這就是為什麼想要單向播放以外的動畫控制時,透過顯式控制比較方便的原因。
來看一下執行效果:
呃…是可以控制停止,看來也是不停地播放啦!不過好像不是往復式的?每次都是由小至大?說好的由小至大再由大至小呢? AnimationController
的 repeat
有個 reverse
可以用,預設是 false
,若設為 true
就是往復式的了:
scaleController.repeat(reverse: true);
效果如下: