作者能力有限, 如果您在閱讀過程中發現任何錯誤, 還請您務必聯繫本人,指出錯誤, 避免後來讀者再學習錯誤的知識.謝謝!
參考: https://flutter.dev/docs/development/ui/animations/tutorial
基本概念
Animation: Flutter 提供的核心動畫庫, 使用指定的值來構建動畫.
Animation 實例: 它只知道動畫當前的狀態(state)(比如,動態是否啓動,結束,或者正在執行), 但是不知道當前屏幕顯示的內容.
CurveAnimation: 用於定義一個非線性曲線.
Tween: 補間動畫,用於在動畫狀態之間插入補充的值。 比如,它可以用與在從紅色變成綠色的動畫中插入中間值.
Listensers 和 StatusListeners: 用於監聽動畫狀態(state)的改變.
下面簡要介紹一下常用的 Animation.
Animation<double>
Animation 的實例會在給定的兩個值之間生成連續的中間值。 生成數據的間隔由 duration 決定. 生成連續值的方式可以有以下幾種: 線性,曲線,階躍,或者其他方法.
Animation 實例是有狀態的, 可以通過 .value 字段獲取它當前的狀態值.
Animtation 實例無法知道 build 函數的狀態和其他繪製狀態.
CurvedAnimation
CurvedAnimation 用於定義一個非線性動畫.
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
Curves 類中定義了許多常用的曲線
CurvedAnimation 和 AnimationController 類型都是 Animation 的子類型. 因此你可以將他們賦值給 Animation.
AnimationController
AnimationController 是一個特殊的 Animation, 用戶生成新的值在硬件準備好之後. 默認情況下, 它會已特定間隔生成一系列間與 0.0 - 1.0 的值.
例如:
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
正如前文所說,AnimationController 是 Animation 的子類,相比於基類,它提供了一些額外的方法來控制動畫:
比如,你可以使用 forward()
來開始一段動畫.
生成數據的頻率由屏幕刷新次數決定. 通常情況下是每分鐘 60 次.
每當新數據生成時,每個 Animation 實例的 listenser 會被調用.
當創建一個動畫對象時,需要傳入一個 vsync
參數. 使用 vsync
可以防止屏幕外的動畫消耗不必要的資源。
Tween
通常情況下, AnimationController 實例的值範圍爲 0.0 - 1.0. 當你需要使用其他範圍的值時, 你可以通過使用 Tween 來配置動態來生成不同範圍的值.
比如: 當你想指定值的範圍爲 -200 - 0.0
tween = Tween<double>(begin: -200, end: 0);
Tween 是一個無狀態的對象(stateless), 它僅僅知道 begin 和 end. 它的任務通常是用來將一個值範圍映射爲另外一個值範圍. 被映射的值範圍通常都是 0.0 - 1.0.
Tween 是 Animatable 的子類,注意不是 Animation. Animatable 不需要生成的 double 值的輸出.
比如: ColroTween 用於生成在兩個 Color 之間的動畫.
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween 實例不保存任何狀態,但是它提供了 evalute(Animation animation)
方法. 這個方法可以用於映射當前動畫值. 當前動畫值可以通過 Animation 實例的 value 字符訪問.
Tween.animate
可以通過將 controller 對象傳給 Tween.animate
方法來使用一個 Tween.
比如: 如下代碼在 500 ms 中生成 0 - 255 之間的值.
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
// 注意,animate 返回一個 Animation 對象,而不是 Animatable 對象.
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
Animation Notification
Animation 實例可以由它的 Listenser 和 StatusListener,分別通過 addListener()
和 addStatusListener()
方法添加.
Listenser 將會被回調當動畫值改變的時候. 最常見的做法是在 Listener 中調用 setState()
來重繪頁面.
StatusListener 將會被調用當動畫開始, 結束,前進,後退的時候.
動畫實例
Rendering animations
一個顯示動畫 Flutter Logo 的實例
原始代碼,無動畫
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 300,
width: 300,
child: FlutterLogo(),
),
);
}
}
接下來,修改代碼,讓這個 logo 從沒有到佔滿屏幕
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
setState(() {});
});
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
我們使用 addListener 調用 setState
每次 Animation 生成一個新的值之後會調用我們添加的 listener, 通過調用 setState 將當前幀標記爲 dirty, 之後 Flutter 就會重繪整個幀.
在 build 方法中, 將 container 的 height 和 width 指定爲 animation 當前的值,這樣每次動畫值更新後,container 的大小就會跟着變化.
在 initState 中初始話 controller, 在 dispose 中釋放 controller, 以避免內存泄漏.
效果如下:
Simplifying with AnimatedWidget
這裏我們使用 AnimatedWidget 來簡化上一步的代碼, 將動畫代碼從 widget 的構建代碼中分離出來。
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
}
Monitoring the progress of the animation
本實例將使用 addStatusListener 來監聽動畫的狀態, 實現一個呼吸動畫.
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
})
..addStatusListener((status) => print('$status'));
controller.forward();
}
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
}
主要添加了 18-36 行的代碼.
效果如下:
Refactoring with AnimatedBuilder
上面代碼中存在一個問題, 那就是動畫改變時會同時修改包含它的頁面.
一個更好的解決方案是將繪製動畫的邏輯由另外一個類型負責.
我們使用 AnimatedBuilder 類來完成該功能. 就像 AnimatedWidget 一樣, AnimatedBuilder 會自動監聽動畫的狀態, 標記當前 widget
爲 dirty 在適當的時候, 然後重繪動畫. 我們不再需要調用 addListener.
爲此,我們就構建如下的 widget tree:
LogoWidget 的實現非常簡單, 僅僅顯示一個 Flutter Logo.
我們在 GrowTransition 的 build 方法中構建中間的 widgets.
完整代碼如下:
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) {
return GrowTransition(
child: LogoWidget(),
animation: animation,
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class LogoWidget extends StatelessWidget {
// Leave out the height and width so it fills the animating parent
Widget build(BuildContext context) => Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: FlutterLogo(),
);
}
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) => Center(
/*
* One tricky point in the code below is that the child looks like it’s specified twice.
* What’s happening is that the outer reference of child is passed to AnimatedBuilder,
* which passes it to the anonymous closure, which then uses that object as its child.
* The net result is that the AnimatedBuilder is inserted in between the two widgets in the render tree.
*/
child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
height: animation.value,
width: animation.value,
child: child,
),
child: child),
);
}
Simultaneous animations
這一節,我們在 “monitoring the progress of the animation” 小節的基礎上構建一個同時變化的動畫.
我們的 Logo 動畫將在改變大小的同時改變自身的透明度.
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
controller.forward();
}
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class AnimatedLogo extends AnimatedWidget {
// Make the Tweens static because they don't change.
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity (
opacity: _opacityTween.evaluate(animation), // 通過evaluate 獲取 Tween 當前的值
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: FlutterLogo(),
),
),
);
}
}
效果如下:
END!