1.動畫介紹
Flutter中的動畫系統基于
Animation
對象的,它不是一個Widget,這是因為
Animation
對象本身和UI渲染沒有任何關系。Animation是一個抽象類,就相當于一個定時器,它用于儲存動畫的插值和狀态,并執行數值的變化。widget可以在
build
函數中讀取
Animation
對象的目前值, 并且可以監聽動畫的狀态改變。
2.在Flutter中有哪些類型的動畫?
在Flutter中動畫分為兩類:基于tween或基于實體的。
- 補間(Tween)動畫:在補間動畫中,定義了開始點和結束點、時間線以及定義轉換時間和速度的曲線。然後由架構計算如何從開始點過渡到結束點;
- 基于實體的動畫:在基于實體的動畫中,運動被模拟為與真實世界的行為相似。例如,當你擲球時,它在何處落地,取決于抛球速度有多快、球有多重、距離地面有多遠。 類似地,将連接配接在彈簧上的球落下(并彈起)與連接配接到繩子上的球放下的方式也是不同;
3.Flutter中常用的動畫的API
在Flutter中使用動畫有幾個常用的API:
- Animation:是Flutter動畫庫中的一個核心類,它生成指導動畫的值;
- AnimationController:Animation的一個子類,用來管理Animation;
- Tween:在正在執行動畫的對象所使用的資料範圍之間生成值。例如,Tween可生成從紅到藍之間的色值,或者從0到255;
- AnimatedBuilder:是拆分動畫的一個工具類,借助它我們可以将動畫和widget進行分離,而AnimatedWidget了解為Animation的助手,使用它可以簡化我們對動畫的使用;
- AnimatedBuilder與AnimatedWidget的最大差別是在于對動畫的拆分上AnimatedBuilder的模式可以将動畫邏輯和widget展示進行拆分,而AnimatedWidget是融合在一起的。
- Curve:Flutter中可以通過Curve(曲線)來描述動畫過程;
- Hero動畫:Hero動畫就是在路由切換時,有一個共享的Widget可以在新舊路由間切換,由于共享的Widget在新舊路由頁面上的位置、外觀可能有所差異,是以在路由切換時會逐漸過渡,這樣就會産生一個Hero動畫
- 組合動畫:有些時候我們可能會需要執行一個動畫序列執行一些複雜的動畫
- Ticker:的作用是添加螢幕重新整理回調,每次螢幕重新整理都會調用
TickerCallback;
Animation
在Flutter中,Animation對象本身和UI渲染沒有任何關系。Animation是一個抽象類,它擁有其目前值和狀态(完成或停止)。其中一個比較常用的Animation類是
Animation<double>
。
Flutter中的Animation對象是一個在一段時間内依次生成一個區間之間值的類。Animation對象的輸出可以是線性的、曲線的、一個步進函數或者任何其他可以設計的映射。 根據Animation對象的控制方式,動畫可以反向運作,甚至可以在中間切換方向。
- Animation還可以生成除double之外的其他類型值,如:
或Animation<Color>
;Animation<Size>
- Animation對象有狀态。可以通過通路其value屬性擷取動畫的目前值;
- Animation對象本身和UI渲染沒有任何關系;
AnimationController
AnimationController
是一個特殊的
Animation
對象,在螢幕重新整理的每一幀,就會生成一個新的值。預設情況下,
AnimationController
在給定的時間段内會線性的生成從0.0到1.0的數字。 例如,下面代碼建立一個Animation對象:
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
AnimationController派生自
Animation<double>
,是以可以在需要Animation對象的任何地方使用。 但是,
AnimationController
具有控制動畫的其他方法:
-
:啟動動畫;forward()
-
:倒放動畫;reverse({double from})
-
:重置動畫,将其設定到動畫的開始位置;reset()
-
:停止動畫;stop({ bool canceled = true })
當建立一個AnimationController時,需要傳遞一個vsync參數,存在vsync時會防止螢幕外動畫消耗不必要的資源,可以将stateful對象作為vsync的值。
注意: 在某些情況下,值(position,值動畫的目前值)可能會超出AnimationController的0.0-1.0的範圍。例如,fling()函數允許您提供速度(velocity)、力量(force)、position(通過Force對象)。位置(position)可以是任何東西,是以可以在0.0到1.0範圍之外。 CurvedAnimation生成的值也可以超出0.0到1.0的範圍。根據選擇的曲線,CurvedAnimation的輸出可以具有比輸入更大的範圍。例如,Curves.elasticIn等彈性曲線會生成大于或小于預設範圍的值。
Tween
預設情況下,
AnimationController
對象值為:double類型,範圍是0.0到1.0 。如果我們需要不同的範圍或不同的資料類型,則可以使用Tween來配置動畫以生成不同的範圍或資料類型的值。例如,以下示例,Tween生成從-200.0到0.0的值:
final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);
Tween是一個無狀态(stateless)對象,需要begin和end值。Tween的唯一職責就是定義從輸入範圍到輸出範圍的映射。輸入範圍通常為0.0到1.0,但這不是必須的。
Tween繼承自
Animatable<T>
,而不是繼承自
Animation<T>
。Animatable與Animation相似,不是必須輸出double值。例如,ColorTween指定兩種顔色之間的過渡。
final Tween colorTween =
new ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween對象不存儲任何狀态。相反,它提供了
evaluate(Animation<double> animation)
方法将映射函數應用于動畫目前值。 Animation對象的目前值可以通過
value()
方法取到。evaluate函數還執行一些其它處理,例如分别確定在動畫值為0.0和1.0時傳回開始和結束狀态。
Tween.animate
要使用Tween對象,可調用它的
animate()
方法,傳入一個控制器對象。例如,以下代碼在500毫秒内生成從0到255的整數值。
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);
注意
animate()
傳回的是一個Animation,而不是一個Animatable。
Curve
動畫過程預設是線性的(勻速),如果需要非線形的,比如:加速的或者先加速後減速等。Flutter中可以通過Curve(曲線)來描述動畫過程。以下示例建構了一個控制器、一條曲線和一個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);
Curves曲線 | 動畫過程 |
---|---|
linear | 勻速的 |
decelerate | 勻減速 |
ease | 開始加速,後面減速 |
easeIn | 開始慢,後面快 |
easeOut | 開始快,後面慢 |
easeInOut | 開始慢,然後加速,最後再減速 |
Hero動畫
Hero動畫就是在路由切換時,有一個共享的Widget可以在新舊路由間切換,由于共享的Widget在新舊路由頁面上的位置、外觀可能有所差異,是以在路由切換時會逐漸過渡,這樣就會産生一個Hero動畫。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("首頁"),
),
body: Route1()),
);
}
}
// 路由A
class Route1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.topCenter,
child: InkWell(
child: Hero(
tag: "avatar", //唯一标記,前後兩個路由頁Hero的tag必須相同
child: CircleAvatar(
backgroundImage: AssetImage(
"assets/banner.jpeg",
),
),
),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return Route2();
}));
},
),
);
}
}
class Route2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Hero(
tag: "avatar", //唯一标記,前後兩個路由頁Hero的tag必須相同
child: Image.asset("assets/banner.jpeg")),
);
}
}
組合動畫
有些時候我們可能會需要執行一個動畫序列執行一些複雜的動畫。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Route(),
);
}
}
class Route extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return RouteState();
}
}
class RouteState extends State<Route> with SingleTickerProviderStateMixin {
Animation<Color> color;
Animation<double> width;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
// 動畫的時長
duration: Duration(milliseconds: 2000),
// 提供 vsync 最簡單的方式,就是直接繼承 SingleTickerProviderStateMixin
vsync: this,
);
//高度動畫
width = Tween<double>(
begin: 100.0,
end: 300.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
//間隔,前60%的動畫時間 1200ms執行高度變化
0.0, 0.6,
),
),
);
color = ColorTween(
begin: Colors.green,
end: Colors.red,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.6, 1.0, //高度變化完成後 800ms 執行顔色編碼
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("首頁"),
),
body: InkWell(
///1、不用顯式的去添加幀監聽器,再調用setState()
///2、縮小動畫建構的範圍,如果沒有builder,setState()将會在父widget上下文調用,導緻父widget的build方法重新調用,現在隻會導緻動畫widget的build重新調用
child: AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Container(
color: color.value,
width: width.value,
height: 100.0,
);
}),
onTap: () {
controller.forward().whenCompleteOrCancel(() => controller.reverse());
},
),
);
}
}
Ticker
Ticker的作用是添加螢幕重新整理回調,每次螢幕重新整理都會調用
TickerCallback
。使用Ticker來驅動動畫會防止螢幕外動畫(動畫的UI不在目前螢幕時,如鎖屏時)消耗不必要的資源。因為Flutter中螢幕重新整理時會通知Ticker,鎖屏後螢幕會停止重新整理,是以Ticker就不會再觸發。最簡單的做法為将
SingleTickerProviderStateMixin
添加到State的定義中。
4.實作一個從小放大的動畫
在下面的執行個體中我們為一個logo添加了一個從小放大的動畫:
import 'package:flutter/animation.dart';
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;
AnimationStatus animationState;
double animationValue;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
// #docregion addListener
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
// #enddocregion addListener
setState(() {
animationValue = animation.value;
});
// #docregion addListener
})
..addStatusListener((AnimationStatus state) {
setState(() {
animationState = state;
});
});
// #enddocregion addListener
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 50),
child: Column(
children: <Widget>[
GestureDetector(
onTap: () {
controller.reset();
controller.forward();
},
child: Text('Start', textDirection: TextDirection.ltr),
),
Text('State:' + animationState.toString(),
textDirection: TextDirection.ltr),
Text('Value:' + animationValue.toString(),
textDirection: TextDirection.ltr),
Container(
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
],
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
注意,在上述代碼中要實作這個動畫的關鍵一步是在的回調中添加
addListener()
的調用這樣才能觸發頁面重新渲染,動畫才能有效,另外也可以通過AnimatedWidget來實作,在下文中會講到。
setState
5.為動畫添加監聽器
有時我們需要知道動畫執行的進度和狀态,在Flutter中我們可以通過Animation的
addListener
與
addStatusListener
方法為動畫添加監聽器:
-
:動畫的值發生變化時被調用;addListener
-
:動畫狀态發生變化時被調用;addStatusListener
枚舉值 | 含義 |
---|---|
| 動畫在起始點停止 |
| 動畫正在正向執行 |
| 動畫正在反向執行 |
| 動畫在終點停止 |
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
// #enddocregion print-state
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
})
// #docregion print-state
..addStatusListener((state) => print('$state'));
..addListener(() {
// #enddocregion addListener
setState(() {
// The state that has changed here is the animation object’s value.
});
// #docregion addListener
});
controller.forward();
}
6.用AnimatedWidget與AnimatedBuilder簡化代碼
什麼是AnimatedWidget?
我們可以将
AnimatedWidget
了解為Animation的助手,使用它可以簡化我們對動畫的使用,在實作一個從小放大的動畫的學習中我們不難發現,在不使用
AnimatedWidget
的情況下需要手動調用動畫的
addListener()
并在回調中添加
setState
才能看到動畫效果,
AnimatedWidget
将為我們簡化這一操作。
在下面的重構示例中,LogoApp現在繼承自
AnimatedWidget
而不是
StatefulWidget
。
AnimatedWidget
在繪制時使用動畫的目前值。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());
}
什麼是AnimatedBuilder?
AnimatedBuilder
是用于建構動畫的通用widget,AnimatedBuilder對于希望将動畫作為更大建構函數的一部分包含在内的更複雜的widget時非常有用,其實你可以這樣了解:AnimatedBuilder是拆分動畫的一個工具類,借助它我們可以将動畫和widget進行分離:
在上面的執行個體中我們的代碼存在的一個問題: 更改動畫需要更改顯示logo的widget。更好的解決方案是将職責分離:
- 顯示logo
- 定義Animation對象
- 渲染過渡效果
接下來我們就借助
AnimatedBuilder
類來完成此分離。
AnimatedBuilder
是渲染樹中的一個獨立的類, 與
AnimatedWidget
類似,
AnimatedBuilder
自動監聽來自Animation對象的通知,不需要手動調用
addListener()
。
我們根據下圖的 widget 樹來建立我們的代碼:
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
// #docregion LogoWidget
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(),
);
}
// #enddocregion LogoWidget
// #docregion GrowTransition
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) => Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
height: animation.value,
width: animation.value,
child: child,
),
child: child),
);
}
// #enddocregion GrowTransition
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
// #docregion print-state
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();
}
// #enddocregion print-state
@override
Widget build(BuildContext context) => GrowTransition(
child: LogoWidget(),
animation: animation,
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
// #docregion print-state
}
參考:https://mp.weixin.qq.com/s/IGHmnOS7E3oZjfq9PDpKzA