參考來(lái)源:https://flutterchina.club/animations/
動(dòng)畫類型
補(bǔ)間(Tween)動(dòng)畫
:在補(bǔ)間動(dòng)畫中,定義了開(kāi)始點(diǎn)和結(jié)束點(diǎn)、時(shí)間線以及定義轉(zhuǎn)換時(shí)間和速度的曲線,然后由框架自動(dòng)計(jì)算如何從開(kāi)始點(diǎn)過(guò)渡到結(jié)束點(diǎn)。基于物理的動(dòng)畫
:在基于物理的動(dòng)畫中,運(yùn)動(dòng)被模擬為與真實(shí)世界的行為相似。例如,當(dāng)你擲球時(shí),它在何處落地,取決于拋球速度有多快、球有多重、距離地面有多遠(yuǎn)。 類似地,將連接在彈簧上的球落下(并彈起)與連接到繩子上的球放下的方式也是不同。
常見(jiàn)的動(dòng)畫模式
- 動(dòng)畫列表或網(wǎng)格(AnimatedList 示例
- 共享元素轉(zhuǎn)換(Hero 動(dòng)畫
- 交錯(cuò)動(dòng)畫
基本的動(dòng)畫概念和類
Animation對(duì)象:
- Animation對(duì)象是Flutter動(dòng)畫庫(kù)中的一個(gè)核心類,它生成指導(dǎo)動(dòng)畫的值。
- Animation對(duì)象知道動(dòng)畫的當(dāng)前狀態(tài)(例如,它是開(kāi)始、停止還是向前或向后移動(dòng)),但它不知道屏幕上顯示的內(nèi)容。
- Flutter中的Animation對(duì)象是一個(gè)在一段時(shí)間內(nèi)依次生成一個(gè)區(qū)間之間值的類。Animation對(duì)象的輸出可以是線性的、曲線的、一個(gè)步進(jìn)函數(shù)或者任何其他可以設(shè)計(jì)的映射。 根據(jù)Animation對(duì)象的控制方式,動(dòng)畫可以反向運(yùn)行,甚至可以在中間切換方向。
- Animation還可以生成除double之外的其他類型值,如:Animation<Color> 或 Animation<Size>
- Animation對(duì)象有狀態(tài),可以通過(guò)訪問(wèn)其value屬性獲取動(dòng)畫的當(dāng)前值
- Animation對(duì)象本身和UI渲染沒(méi)有任何關(guān)系
Interval
在[begin]之前為0.0的曲線,然后根據(jù)[curve曲線]在[end]時(shí)從0.0到1.0,然后是1.0。
可以使用[Interval]來(lái)延遲動(dòng)畫。例如,使用[Interval]將[begin]設(shè)置為0.5,將[end]設(shè)置為1.0的[Interval]使用[Interval],這6秒的動(dòng)畫將本質(zhì)上變成三秒鐘后開(kāi)始的動(dòng)畫。
CurvedAnimation:將動(dòng)畫過(guò)程定義為一個(gè)非線性曲線,屬于Animation<double>類型
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeIn);
注: Curves 類類定義了許多常用的曲線,也可以創(chuàng)建自己的,例如:
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
AnimationController:
- AnimationController是一個(gè)特殊的Animation對(duì)象,在屏幕刷新的每一幀,就會(huì)生成一個(gè)新的值,默認(rèn)情況下,AnimationController在給定的時(shí)間段內(nèi)會(huì)線性的生成從0.0到1.0的數(shù)字
- 屬于Animation<double>類型
- 具有控制動(dòng)畫的方法,例如,.forward()方法可以啟動(dòng)動(dòng)畫
- 當(dāng)創(chuàng)建一個(gè)AnimationController時(shí),需要傳遞一個(gè)vsync參數(shù),存在vsync時(shí)會(huì)防止屏幕外動(dòng)畫(動(dòng)畫的UI不在當(dāng)前屏幕時(shí))消耗不必要的資源。
通過(guò)將SingleTickerProviderStateMixin添加到類定義中,可以將stateful對(duì)象作為vsync的值。如果要使用自定義的State對(duì)象作為vsync時(shí),請(qǐng)包含TickerProviderStateMixin。
// 下面代碼創(chuàng)建一個(gè)Animation對(duì)象,但不會(huì)啟動(dòng)它運(yùn)行:
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
Tween:
- 默認(rèn)情況下,AnimationController對(duì)象的范圍從0.0到1.0,使用Tween使動(dòng)畫可以生成不同的范圍或數(shù)據(jù)類型的值。
- Tween是一個(gè)無(wú)狀態(tài)(stateless)對(duì)象,需要begin和end值,Tween的唯一作用就是定義從輸入范圍到輸出范圍的映射。
- Tween繼承自Animatable<T>,而不是繼承自Animation<T>。Animatable與Animation相似,不是必須輸出double值。例如,ColorTween指定兩種顏色之間的過(guò)渡。
- evaluate(Animation<double> animation)方法將映射函數(shù)應(yīng)用于動(dòng)畫當(dāng)前值,Animation對(duì)象的當(dāng)前值可以通過(guò)value()方法取到。
- 要使用Tween對(duì)象,請(qǐng)調(diào)用其animate()方法,傳入一個(gè)控制器對(duì)象(Tween.animate),注意animate()返回的是一個(gè)Animation,而不是一個(gè)Animatable。
// 以下示例,Tween生成從-200.0到0.0的值
final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);
// ColorTween指定兩種顏色之間的過(guò)渡
final Tween colorTween =
new ColorTween(begin: Colors.transparent, end: Colors.black54);
// 以下代碼在500毫秒內(nèi)生成從0到255的整數(shù)值
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);
// 以下示例構(gòu)建了一個(gè)控制器、一條曲線和一個(gè)Tween:
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);
動(dòng)畫通知
- 一個(gè)Animation對(duì)象可以擁有Listeners和StatusListeners監(jiān)聽(tīng)器,可以用addListener()和addStatusListener()來(lái)添加
- 只要?jiǎng)赢嫷闹蛋l(fā)生變化,就會(huì)調(diào)用監(jiān)聽(tīng)器
- 動(dòng)畫開(kāi)始、結(jié)束、向前移動(dòng)或向后移動(dòng)(如AnimationStatus所定義)時(shí)會(huì)調(diào)用StatusListener
- 一個(gè)Listener最常見(jiàn)的行為是調(diào)用setState()來(lái)觸發(fā)UI重建。
動(dòng)畫示例
要使用Animation<>對(duì)象進(jìn)行渲染,請(qǐng)將Animation對(duì)象存儲(chǔ)為Widget的成員,然后使用其value值來(lái)決定如何繪制
考慮下面的應(yīng)用程序,它繪制Flutter logo時(shí)沒(méi)有動(dòng)畫:
import 'package:flutter/material.dart';
class LogoApp extends StatefulWidget {
_LogoAppState createState() => new _LogoAppState();
}
class _LogoAppState extends State<LogoApp> {
Widget build(BuildContext context) {
return new Center(
child: new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
height: 300.0,
width: 300.0,
child: new FlutterLogo(),
),
);
}
}
void main() {
runApp(new LogoApp());
}
修改以上代碼,通過(guò)一個(gè)逐漸放大的動(dòng)畫顯示logo。定義AnimationController時(shí),必須傳入一個(gè)vsync對(duì)象。
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
class LogoApp extends StatefulWidget {
_LogoAppState createState() => new _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
..addListener(() {
setState(() {
// the state that has changed here is the animation object’s value
});
});
controller.forward();
}
Widget build(BuildContext context) {
return new Center(
child: new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
height: animation.value,
width: animation.value,
child: new FlutterLogo(),
),
);
}
dispose() {
controller.dispose();
super.dispose();
}
}
void main() {
runApp(new LogoApp());
}
該addListener()函數(shù)調(diào)用了setState(),所以每次動(dòng)畫生成一個(gè)新的數(shù)字時(shí),當(dāng)前幀被標(biāo)記為臟(dirty),這會(huì)導(dǎo)致widget的build()方法再次被調(diào)用。 在build()中,改變container大小,因?yàn)樗母叨群蛯挾痊F(xiàn)在使用的是animation.value。動(dòng)畫完成時(shí)釋放控制器(調(diào)用dispose()方法)以防止內(nèi)存泄漏。
用AnimatedWidget簡(jiǎn)化
- 使用AnimatedWidget助手類(而不是addListener()和setState())來(lái)給widget添加動(dòng)畫
- 使用AnimatedWidget創(chuàng)建一個(gè)可重用動(dòng)畫的widget。要從widget中分離出動(dòng)畫過(guò)渡,請(qǐng)使用AnimatedBuilder。
- Flutter API提供的關(guān)于AnimatedWidget的示例包括:AnimatedBuilder、AnimatedModalBarrier、DecoratedBoxTransition、FadeTransition、PositionedTransition、RelativePositionedTransition、RotationTransition、ScaleTransition、SizeTransition、SlideTransition。
- AnimatedWidget類允許您從setState()調(diào)用中的動(dòng)畫代碼中分離出widget代碼。AnimatedWidget不需要維護(hù)一個(gè)State對(duì)象來(lái)保存動(dòng)畫。
在下面的重構(gòu)示例中,LogoApp現(xiàn)在繼承自AnimatedWidget而不是StatefulWidget。AnimatedWidget在繪制時(shí)使用動(dòng)畫的當(dāng)前值。LogoApp仍然管理著AnimationController和Tween。
// Demonstrate a simple animation with AnimatedWidget
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return new Center(
child: new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
height: animation.value,
width: animation.value,
child: new FlutterLogo(),
),
);
}
}
class LogoApp extends StatefulWidget {
_LogoAppState createState() => new _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
controller.forward();
}
Widget build(BuildContext context) {
return new AnimatedLogo(animation: animation);
}
dispose() {
controller.dispose();
super.dispose();
}
}
void main() {
runApp(new LogoApp());
}
LogoApp將Animation對(duì)象傳遞給基類并用animation.value設(shè)置容器的高度和寬度,因此它的工作原理與之前完全相同。
監(jiān)視動(dòng)畫的過(guò)程
- 使用addStatusListener來(lái)處理動(dòng)畫狀態(tài)更改的通知,例如啟動(dòng)、停止或反轉(zhuǎn)方向。
知道動(dòng)畫何時(shí)改變狀態(tài)通常很有用的,如完成、前進(jìn)或倒退。你可以通過(guò)addStatusListener()來(lái)得到這個(gè)通知。
// 以下代碼用來(lái)監(jiān)聽(tīng)動(dòng)態(tài)狀態(tài)更改并打印更新:
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
..addStatusListener((state) => print("$state"));
controller.forward();
}
//...
}
運(yùn)行此代碼將輸出以下內(nèi)容:
AnimationStatus.forward
AnimationStatus.completed
接下來(lái),使用addStatusListener()在開(kāi)始或結(jié)束時(shí)反轉(zhuǎn)動(dòng)畫。這產(chǎn)生了循環(huán)效果:
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
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();
}
//...
}
用AnimatedBuilder重構(gòu)
- AnimatedBuilder用于將widget與動(dòng)畫分離
- AnimatedBuilder了解如何渲染過(guò)渡.
- AnimatedBuilder 不知道如何渲染widget,也不知道如何管理Animation對(duì)象。
- 使用AnimatedBuilder將動(dòng)畫描述為另一個(gè)widget的build方法的一部分。如果你只是想用可復(fù)用的動(dòng)畫定義一個(gè)widget,請(qǐng)使用AnimatedWidget。
- Flutter API中AnimatedBuilder的示例包括: BottomSheet、ExpansionTile、 PopupMenu、ProgressIndicator、RefreshIndicator、Scaffold、SnackBar、TabBar、TextField。
- 與AnimatedWidget類似,AnimatedBuilder自動(dòng)監(jiān)聽(tīng)來(lái)自Animation對(duì)象的通知,并根據(jù)需要將該控件樹(shù)標(biāo)記為臟(dirty),因此不需要手動(dòng)調(diào)用addListener()。
從widget樹(shù)的底部開(kāi)始,渲染logo的代碼直接明了:
class LogoWidget extends StatelessWidget {
// Leave out the height and width so it fills the animating parent
build(BuildContext context) {
return new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
child: new FlutterLogo(),
);
}
}
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),
);
}
}
class LogoApp extends StatefulWidget {
_LogoAppState createState() => new _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
Animation animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeIn);
animation = new Tween(begin: 0.0, end: 300.0).animate(curve);
controller.forward();
}
Widget build(BuildContext context) {
return new GrowTransition(child: new LogoWidget(), animation: animation);
}
dispose() {
controller.dispose();
super.dispose();
}
}
void main() {
runApp(new LogoApp());
}
initState()方法創(chuàng)建一個(gè)AnimationController和一個(gè)Tween,然后通過(guò)animate()綁定它們。魔術(shù)發(fā)生在build()方法中,該方法返回一個(gè)帶有LogoWidget作為子對(duì)象的GrowTransition對(duì)象,以及一個(gè)用于驅(qū)動(dòng)過(guò)渡的動(dòng)畫對(duì)象。
并行動(dòng)畫
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
class AnimatedLogo extends AnimatedWidget {
// The Tweens are static because they don't change.
static final _opacityTween = new Tween<double>(begin: 0.1, end: 1.0);
static final _sizeTween = new Tween<double>(begin: 0.0, end: 300.0);
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return new Center(
child: new Opacity(
opacity: _opacityTween.evaluate(animation),
child: new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: new FlutterLogo(),
),
),
);
}
}
class LogoApp extends StatefulWidget {
_LogoAppState createState() => new _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
animation = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
controller.forward();
}
Widget build(BuildContext context) {
return new AnimatedLogo(animation: animation);
}
dispose() {
controller.dispose();
super.dispose();
}
}
void main() {
runApp(new LogoApp());
}
AnimatedWidget的構(gòu)造函數(shù)只接受一個(gè)動(dòng)畫對(duì)象。 為了解決這個(gè)問(wèn)題,該示例創(chuàng)建了自己的Tween對(duì)象并顯式計(jì)算了這些值。其build方法.evaluate()在父級(jí)的動(dòng)畫對(duì)象上調(diào)用Tween函數(shù)以計(jì)算所需的size和opacity值。