## 動畫基本結構
我們通過實現一個圖片逐漸放大的示例來演示一下Flutter中動畫的基本結構:
```
class ScaleAnimationRoute extends StatefulWidget {
@override
_ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}
//需要繼承TickerProvider,如果有多個AnimationController,則應該使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin{
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//圖片寬高從0變到300
animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
..addListener(() {
setState(()=>{});
});
//啟動動畫(正向執行)
controller.forward();
}
@override
Widget build(BuildContext context) {
return new Center(
child: Image.asset("images/avatar.png",
width: animation.value,
height: animation.value
),
);
}
dispose() {
//路由銷毀時需要釋放動畫資源
controller.dispose();
super.dispose();
}
}
```
上面代碼中`addListener()`函數調用了`setState()`,所以每次動畫生成一個新的數字時,當前幀被標記為臟(dirty),這會導致widget的`build()`方法再次被調用,而在`build()`中,改變Image的寬高,因為它的高度和寬度現在使用的是`animation.value` ,所以就會逐漸放大。值得注意的是動畫完成時要釋放控制器(調用`dispose()`方法)以防止內存泄漏。
上面的例子中并沒有指定Curve,所以放大的過程是線性的(勻速),下面我們指定一個Curve,來實現一個類似于彈簧效果的動畫過程,我們只需要將`initState`中的代碼改為下面這樣即可:
```
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//使用彈性曲線
animation=CurvedAnimation(parent: controller, curve: Curves.bounceIn);
//圖片寬高從0變到300
animation = new Tween(begin: 0.0, end: 300.0).animate(animation)
..addListener(() {
setState(() {
});
});
//啟動動畫
controller.forward();
}
```
### 使用AnimatedWidget簡化
細心的讀者可能已經發現上面示例中通過`addListener()`和`setState()` 來更新UI這一步其實是通用的,如果每個動畫中都加這么一句是比較繁瑣的。AnimatedWidget類封裝了調用`setState()`的細節,并允許我們將Widget分離出來,重構后的代碼如下:
```
class AnimatedImage extends AnimatedWidget {
AnimatedImage({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return new Center(
child: Image.asset("images/avatar.png",
width: animation.value,
height: animation.value
),
);
}
}
class ScaleAnimationRoute extends StatefulWidget {
@override
_ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>
with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//圖片寬高從0變到300
animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
//啟動動畫
controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedImage(animation: animation,);
}
dispose() {
//路由銷毀時需要釋放動畫資源
controller.dispose();
super.dispose();
}
}
```
### 用AnimatedBuilder重構
用AnimatedWidget可以從動畫中分離出widget,而動畫的渲染過程(即設置寬高)仍然在AnimatedWidget中,假設如果我們再添加一個widget透明度變化的動畫,那么我們需要再實現一個AnimatedWidget,這樣不是很優雅,如果我們能把渲染過程也抽象出來,那就會好很多,而AnimatedBuilder正是將渲染邏輯分離出來, 上面的build方法中的代碼可以改為:
```
@override
Widget build(BuildContext context) {
//return AnimatedImage(animation: animation,);
return AnimatedBuilder(
animation: animation,
child: Image.asset("images/avatar.png"),
builder: (BuildContext ctx, Widget child) {
return new Center(
child: Container(
height: animation.value,
width: animation.value,
child: child,
),
);
},
);
}
```
上面的代碼中有一個迷惑的問題是,`child`看起來像被指定了兩次。但實際發生的事情是:將外部引用child傳遞給AnimatedBuilder后AnimatedBuilder再將其傳遞給匿名構造器, 然后將該對象用作其子對象。最終的結果是AnimatedBuilder返回的對象插入到Widget樹中。
也許你會說這和我們剛開始的示例差不了多少,其實它會帶來三個好處:
1. 不用顯式的去添加幀監聽器,然后再調用`setState()` 了,這個好處和AnimatedWidget是一樣的。
2. 動畫構建的范圍縮小了,如果沒有builder,setState()將會在父widget上下文調用,這將會導致父widget的build方法重新調用,而有了builder之后,只會導致動畫widget的build重新調用,這在復雜布局下性能會提高。
3. 通過AnimatedBuilder可以封裝常見的過渡效果來復用動畫。下面我們通過封裝一個GrowTransition來說明,它可以對子widget實現放大動畫:
```
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) {
return new Center(
child: new AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return new Container(
height: animation.value,
width: animation.value,
child: child
);
},
child: child
),
);
}
}
```
這樣,最初的示例就可以改為:
```
...
Widget build(BuildContext context) {
return GrowTransition(
child: Image.asset("images/avatar.png"),
animation: animation,
);
}
```
**Flutter中正是通過這種方式封裝了很多動畫,如:FadeTransition、ScaleTransition、SizeTransition、FractionalTranslation等,很多時候都可以復用這些預置的過渡類。**
## 動畫狀態監聽
上面說過,我們可以通過Animation的`addStatusListener()`方法來添加動畫狀態改變監聽器。Flutter中,有四種動畫狀態,在AnimationStatus枚舉類中定義,下面我們逐個說明:
枚舉值含義`dismissed`動畫在起始點停止`forward`動畫正在正向執行`reverse`動畫正在反向執行`completed`動畫在終點停止#### 示例
我們將上面圖片放大的示例改為先放大再縮小再放大……這樣的循環動畫。要實現這種效果,我們只需要監聽動畫狀態的改變即可,即:在動畫正向執行結束時反轉動畫,在動畫反向執行結束時再正向執行動畫。代碼如下:
```
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 1), vsync: this);
//圖片寬高從0變到300
animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
//動畫執行結束時反向執行動畫
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
//動畫恢復到初始狀態時執行動畫(正向)
controller.forward();
}
});
//啟動動畫(正向)
controller.forward();
}
```
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示