Flutter 中動畫的使用
在學習了靜態頁面的搭建後,需要再加上一點動畫效果來提升使用者體驗。
動畫靜態的畫面根據事先定義好的規律,在一定時間內不斷微調,產生變化效果。而動畫實現由靜止到動態,主要是靠人眼的視覺殘留效應。所以,對動畫系統而言,為了實現動畫,它需要做三件事兒:
- 確定畫面變化的規律;
- 根據這個規律設定好動畫週期,然後啟動動畫
- 通過不斷獲取動畫當前值,重繪畫面。
在Flutter 中,通過Animation、AnimationController 與 Listener完完成這三件事。
Animation:根據預定規則,在動畫週期內不斷輸出動畫當前值。
AnimationController:用於控制 Animation,可以設定動畫時長、啟動、停止動畫。
Listener:用於動畫的監聽,根據回撥,獲取動畫的當前值,進行渲染。
class _AnimationDemo1State extends State with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
@override
void initState() {
super.initState();
// 建立 controller
controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 1000));
// 建立 animation
animation = Tween(begin: 50.0, end: 250.0).animate(controller)
..addListener(() {
// 更新狀態
setState(() {});
});
//在啟動動畫時,使用 repeat(reverse: true),讓動畫來回重複執行。
controller.repeat(reverse: true);
// 監聽動畫狀態。在動畫結束時,反向執行;在動畫反向執行完畢時,重新啟動執行。
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
//在動畫結束時,反向執行
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
//在動畫反向執行完畢時,重新啟動執行
controller.forward();
}
});
// 開始動畫
controller.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("基礎動畫"),
),
body: Center(
child: Container(
width: animation.value,
height: animation.value,
color: Colors.yellow,
),
),
);
}
@override
void dispose() {
// 停止動畫
controller.stop();
super.dispose();
}
}
複製程式碼
可以看到 animation只提供動畫資料,因此我們還需要監聽動畫執行進度,並在回撥中使用 setState 強制重新整理介面才能看到動畫效果。這些步驟都是固定的,有沒有什麼更簡單的方法嗎?
AnimatedWidget 與 AnimatedBuilder
通過這兩個widget 可以簡化我們的動畫步驟。 AnimatedWidget: 把對動畫的監聽放到 AnimatedWidget中,在裡面就可以拿到動畫的值,進行頁面的重繪。
// 通過 AnimatedWidget 來實現動畫
class AnimatedWidgetDemo extends AnimatedWidget {
AnimatedWidgetDemo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return Container(
width: animation.value,
height: animation.value,
color: Colors.red,
);
}
}
class AnimationDemo2 extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AnimationDemo2State();
}
}
class _AnimationDemo2State extends State with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
@override
void initState() {
super.initState();
// 建立 controller
controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 1000));
animation = Tween(begin: 50.0, end: 250.0).animate(controller);
//在啟動動畫時,使用 repeat(reverse: true),讓動畫來回重複執行。
controller.repeat(reverse: true);
// 監聽動畫狀態。在動畫結束時,反向執行;在動畫反向執行完畢時,重新啟動執行。
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
//在動畫結束時,反向執行
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
//在動畫反向執行完畢時,重新啟動執行
controller.forward();
}
});
// 開始動畫
controller.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("AnimatedWidgetDemo"),
),
body: Center(
child: AnimatedWidgetDemo(
animation: animation,
),
),
);
}
@override
void dispose() {
controller.stop();
super.dispose();
}
}
複製程式碼
AnimatedBuilder: 將動畫和渲染邏輯分開。
class _AnimationDemo3State extends State with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
@override
void initState() {
super.initState();
// 建立 controller
controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 1000));
animation = Tween(begin: 50.0, end: 250.0).animate(controller);
//在啟動動畫時,使用 repeat(reverse: true),讓動畫來回重複執行。
controller.repeat(reverse: true);
// 監聽動畫狀態。在動畫結束時,反向執行;在動畫反向執行完畢時,重新啟動執行。
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
//在動畫結束時,反向執行
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
//在動畫反向執行完畢時,重新啟動執行
controller.forward();
}
});
// 開始動畫
controller.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("AnimatedWidgetDemo"),
),
body: AnimatedBuilder(
animation: animation,
builder: (context, child) => Center(
child: Container(
width: animation.value,
height: animation.value,
color: Colors.deepPurple,
),
),
));
}
@override
void dispose() {
controller.stop();
super.dispose();
}
}
複製程式碼
可以看到 通過AnimatedBuilder,傳入 animation ,然後可以在裡面的widget 拿到 animation 的值,然後設定相應的屬性,更新UI。
通過上面幾種方式對比,僅僅是在動畫監聽 做了修改而已,從自己新增 listener 到繼承 AnimationWidget 傳入animation物件,在裡面可以使用到;在到 AnimationBuild ,直接傳入animation 物件,字面的子 widget 獲取到 animation的屬性後,更新UI,基本思路都是差不多。
hero 動畫
實現小圖到大圖頁面逐步放大的動畫切換效果,而當使用者關閉大圖時,也實現原路返回的動畫。這樣的跨頁面共享的控制元件動畫效果有一個專門的名詞,即“共享元素變換”(Shared Element Transition)。
Flutter 也有類似的概念,即 Hero 控制元件。通過 Hero,我們可以在兩個頁面的共享元素之間,做出流暢的頁面切換效果。
為了實現共享元素變換,我們需要將這兩個元件分別用 Hero 包裹,並同時為它們設定相同的 tag “hero”
class AnimationDemo4 extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("hero動畫"),
),
body: GestureDetector(
//手勢監聽點選
child: Hero(
tag: 'hero', //設定共享tag
child: Center(
child: Container(width: 80, height: 80, child: FlutterLogo()),
)),
onTap: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => Page2())); //點選後開啟第二個頁面
},
));
}
}
class Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二個頁面"),
),
body: Hero(
tag: 'hero', //設定共享tag
child: Center(
child: Container(width: 320, height: 320, child: FlutterLogo()),
)));
}
}
複製程式碼
總結
Flutter 中的動畫,是通過 Animation、AnimationController和Listener來完成的,通過Animation設定動畫的變化規律、AnimationController設定動畫時長、重複次數、開始、停止動畫等完成對動畫的管理,最後通過 Listener來完成動畫的監聽,改變widget 屬性,重新整理UI。Flutter 為我們提供更簡單方便的 AnimationWidget、AnimationBuild widget 來幫助我們把動畫和UI的更新邏輯分開,更好的完成複雜的動畫效果。最後Flutter 還提供了共享元素動畫 Hero。