Flutter動畫中用到的基本概念
Flutter動畫中有4個比較重要的角色:Animation、Controller、Curve、Tween,先來了解一下這四個角色
1.1 Animation
Animation是Flutter動畫庫中的核心類,用於插入指導動畫的值
Animation對象知道動畫當前的狀態(比如開始還是停止),可以使用addListener和addStatusListener監聽動畫狀態改變。
animation.addListener((){
//調用setState來刷新界面
setState(() {
});
});
animation.addStatusListener((status){
debugPrint('status $status');
switch (status){
//動畫一開始就停止了
case AnimationStatus.dismissed:
break;
//動畫從頭到尾都在播放
case AnimationStatus.forward:
break;
//動畫從結束到開始倒着播放
case AnimationStatus.reverse:
break;
//動畫播放完停止
case AnimationStatus.completed:
break;
}
});
- addListener: 每一幀都會調用,調用之後一般使用setState來刷新界面
- addStatusListener:監聽動畫當前的狀態 如動畫開始、結束、正向或反向
在Flutter中,Animation對象本身和UI渲染沒有任何關係。Animation是一個抽象類,它擁有其當前值和狀態(完成或停止)。其中一個比較常用的Animation類是Animation<double>
,還可以生成除double之外的其他類型值,如:Animation<Color>
或Animation<Size>
。
1.2 AnimationController
用來管理Animation,它繼承自Animation,是個特殊的Animation,屏幕每刷新一幀,它都會生成一個新值,需要一個vsync參數,vsync的存在可以防止後臺動畫消耗不必要的資源。
vsync的值怎麼獲得,可以讓stateful對象擴展使用TickerProviderStateMixin比如:
class AnimationDemoHome extends StatefulWidget {
@override
_AnimationDemoHomeState createState() => _AnimationDemoHomeState();
}
class _AnimationDemoHomeState extends State<AnimationDemoHome> with TickerProviderStateMixin{...}
AnimationController在默認情況下,在給定的時間段內,AnimationController會生成0.0到1.0的數字。
它可以控制動畫,比如使用.forward()
方法可以啓動一個動畫,.stop()
可以結束一個動畫,.reverse()
啓動反向動畫。
AnimationController({
double value,
this.duration,
this.reverseDuration,
this.debugLabel,
this.lowerBound = 0.0,
this.upperBound = 1.0,
this.animationBehavior = AnimationBehavior.normal,
@required TickerProvider vsync,
})
看一下AnimationController的構造方法,有一個必須的參數TickerProvider,就是前面給定的TickerProviderStateMixin
在StatefulWidget中創建一個AnimationController對象
animationController = AnimationController(
// lowerBound: 32.0,
// upperBound: 100.0,
duration: Duration(milliseconds: 2000),
vsync: this
);
1.3 CurvedAnimation
定義動畫曲線,運動過程,比如勻速,先加速在減速等等
CurvedAnimation({
@required this.parent,
@required this.curve,
this.reverseCurve,
})
它有兩個必要的參數parent和curve。parent就是前面的AnimationController對象,curve就是動畫運行的曲線,相當於Android屬性動畫中的插值器curve都有哪些取值呢
curve曲線 | 動畫過程 |
---|---|
linear | 勻速的 |
decelerate | 勻減速 |
ease | 先加速後減速 |
easeIn | 開始慢後面快 |
easeOut | 開始快後面慢 |
easeInOut | 先慢在快在慢 |
上面是常用的一些曲線,還有很多中曲線運動的方式可以去curve.dart源碼中去看,源碼註釋中有mp4的鏈接,可以清楚的看到動畫運動的視頻。
abstract class Curve {
const Curve();
double transform(double t) {
assert(t >= 0.0 && t <= 1.0);
if (t == 0.0 || t == 1.0) {
return t;
}
return transformInternal(t);
}
@protected
double transformInternal(double t) {
throw UnimplementedError();
}
...
}
如果系統提供的運動曲線仍然無法滿足我們的需求,那就可以繼承Curve來自己實現一個。上面的代碼可以看到Curve是一個抽象類,繼承它並重寫transform方法即可。比如我們可以自己在裏面實現一個sin或者cos函數的曲線。例如
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
創建一個CurvedAnimation對象
CurvedAnimation curvedAnimation =
CurvedAnimation(parent: animationController,curve: Curves.bounceOut);
1.4 Tween:
給動畫對象插入一個範圍值
默認情況下,AnimationController對象的範圍從0.0到1.0,如果我們想要更大的範圍,就需要使用到Tween了。比如
Tween tween = Tween(begin: 32.0,end: 100.0);
class Tween<T extends dynamic> extends Animatable<T>
Tween繼承自Animatable,接收一個begin和一個end值,Tween的職責就是定義從輸入範圍到輸出範圍的映射。所以這兩個值必須能進行加減乘的運算。
要使用Tween對象,調用其animate()方法,傳入一個控制器對象,返回一個Animation對象。例如,
Animation animation = Tween(begin: 32.0,end: 100.0).animate(curvedAnimation);
Animation animationColor = ColorTween(begin: Colors.red,end: Colors.green).animate(curvedAnimation);
動畫的使用
2.1 Animation動畫
動畫的四個角色都瞭解了,下面開始使用這些角色來構建一個動畫,動畫效果如下圖
有一個心形的button,點擊的時候放大並且顏色漸變,在點擊的時候原路返回
class AnimateDemo1 extends StatefulWidget {
@override
_AnimateDemo1State createState() => _AnimateDemo1State();
}
class _AnimateDemo1State extends State<AnimateDemo1> with SingleTickerProviderStateMixin{
AnimationController animationController;
Animation animationSize;
Animation animationColor;
CurvedAnimation curvedAnimation;
//Tween sizeTween;
//Tween colorTween;
@override
void initState() {
super.initState();
animationController = AnimationController(
duration: Duration(milliseconds: 1000),
vsync: this
);
//設置插值器 這裏使用一個默認的插值器bounceInOut
curvedAnimation = CurvedAnimation(parent: animationController,curve: Curves.bounceOut);
animationSize = Tween(begin: 32.0,end: 100.0).animate(curvedAnimation);
animationColor = ColorTween(begin: Colors.red,end: Colors.green).animate(curvedAnimation);
animationController.addListener((){
//刷新界面
setState(() {});
});
}
@override
void dispose() {
super.dispose();
animationController.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: IconButton(
icon: Icon(Icons.favorite),
iconSize: animationSize.value,
color: animationColor.value,
//iconSize: sizeTween.evaluate(curvedAnimation),
//color: colorTween.evaluate(curvedAnimation),
onPressed: (){
switch(animationController.status){
case AnimationStatus.completed:
animationController.reverse();
break;
default:
animationController.forward();
}
},
),
);
}
}
- 通過animation.value可以拿到動畫當前的值,然後賦值給當前需要動畫的控件的相關屬性即可
- 需要在addListener中調用setState來刷新界面,否則沒效果
- 需要注意 animationController需要在dispose()頁面銷燬的時候釋動畫資源。
- 如果沒有調用Tween的animate方法來構建一個Animation,可以在使用的地方使用如上面代碼中
sizeTween.evaluate(curvedAnimation)
的方式來獲取當前值。
2.2使用AnimatedWidget
2.1中每次寫動畫都需要在addListener中設置setState來更新UI,有點麻煩,系統給提供了一個AnimatedWidget,它內部封裝了addListener和setState的邏輯,我們只需要傳給它AnimationController和Animation就行了。
而且我們可以自定義一個Widget繼承它,讓動畫跟原來的視圖代碼分離
class AnimationDemo2 extends StatefulWidget {
@override
_AnimationDemo2State createState() => _AnimationDemo2State();
}
class _AnimationDemo2State extends State<AnimationDemo2> with SingleTickerProviderStateMixin{
AnimationController animationController;
Animation animationSize;
Animation animationColor;
CurvedAnimation curvedAnimation;
@override
void initState() {
super.initState();
animationController = AnimationController(
duration: Duration(milliseconds: 1000),
vsync: this
);
//設置插值器 這裏使用一個默認的插值器bounceInOut
curvedAnimation = CurvedAnimation(parent: animationController,curve: Curves.bounceOut);
animationSize = Tween(begin: 32.0,end: 100.0).animate(curvedAnimation);
animationColor = ColorTween(begin: Colors.red,end: Colors.green).animate(curvedAnimation);
}
@override
void dispose() {
super.dispose();
animationController.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimationHeart(
animations: [
animationSize,animationColor
],
controller: animationController,
),
);
}
}
//動畫代碼抽離
class AnimationHeart extends AnimatedWidget{
AnimationController controller;
List animations;
AnimationHeart({ this.animations,
this.controller,}):super(listenable:controller);
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(Icons.favorite),
iconSize: animations[0].value,
color: animations[1].value,
onPressed: (){
switch(controller.status){
case AnimationStatus.completed:
controller.reverse();
break;
default:
controller.forward();
}
},
);
}
}
自定義一個AnimationHeart繼承自AnimatedWidget,在構造方法中將AnimationController和Animation傳過來。其餘的跟2.1中一樣,最終效果也一樣。
2.3使用AnimatedBuilder
Flutter中還可以使用AnimatedBuilder來構建一個動畫
class AnimateDemo3 extends StatefulWidget {
@override
_AnimateDemo3State createState() => _AnimateDemo3State();
}
class _AnimateDemo3State extends State<AnimateDemo3> with SingleTickerProviderStateMixin{
AnimationController animationController;
Animation animationSize;
Animation animationColor;
CurvedAnimation curvedAnimation;
@override
void initState() {
super.initState();
animationController = AnimationController(
duration: Duration(milliseconds: 1000),
vsync: this
);
//設置插值器 這裏使用一個默認的插值器bounceInOut
curvedAnimation = CurvedAnimation(parent: animationController,curve: Curves.bounceOut);
animationSize = Tween(begin: 32.0,end: 100.0).animate(curvedAnimation);
animationColor = ColorTween(begin: Colors.red,end: Colors.green).animate(curvedAnimation);
}
@override
void dispose() {
super.dispose();
animationController.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
builder: (context,child){
return Center(
child: IconButton(
icon: Icon(Icons.favorite),
iconSize: animationSize.value,
color: animationColor.value,
onPressed: (){
switch(animationController.status){
case AnimationStatus.completed:
animationController.reverse();
break;
default:
animationController.forward();
}
},
),
);
},
);
}
}
實例化四個動畫元素的代碼跟前面還是一樣,主要是在build代碼塊中使用AnimatedBuilder構建,傳入animation對象。看起來比2.2中的方式也沒有簡單多少,不過看一下它的構造方法,系統還給提供了一個可選的參數child,讓它天然就支持封裝。
const AnimatedBuilder({
Key key,
@required Listenable animation,
@required this.builder,
this.child,
})
- 必需要一個Listenable,Animation就是Listenable
- 必需要一個builder,前面的代碼中知道builder中需要傳一個context和一個child
- 可以傳一個child。傳入的這個child最終會傳入到builder中
上面的例子中我們是直接在builder中創建了一個控件,既然child可以傳進來,那麼我們可以把一個類型的動畫封裝一下比如縮放動畫,漸變動畫等,以後只要把需要此動畫的小部件傳進來,這個小部件就有這個動畫了。
比如下面定義一個可以縮放的小部件。
class ScaleAnimate extends StatelessWidget {
final Animation animation;
final Widget child;
ScaleAnimate({@required this.animation,@required this.child});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (context,child){
return SizedBox(
width: animation.value,
height: animation.value,
child: child,
);
},
child: child,
);
}
}
Hero動畫
Hero動畫很簡單不過在平時的項目中也經常用到,主要用在路由頁面之間切換。比如一個頭像點擊看大圖,或者新聞列表頁面,點擊看詳情,這種共享式的無縫切換。
動畫效果如下圖
class AnimateDemo4 extends StatefulWidget {
@override
_AnimateDemo4State createState() => _AnimateDemo4State();
}
class _AnimateDemo4State extends State<AnimateDemo4> {
@override
Widget build(BuildContext context) {
return Center(
child: InkWell(
child: Hero(
tag: "avator",
child: ClipOval(
child: Image.network('http://ww1.sinaimg.cn/large/0065oQSqly1fsfq1k9cb5j30sg0y7q61.jpg',width: 100,),
),
),
onTap: (){
Navigator.of(context).push(MaterialPageRoute(builder: (context){
return Scaffold(
body: Center(
child: Hero(
tag: "avator",
child: Image.network('http://ww1.sinaimg.cn/large/0065oQSqly1fsfq1k9cb5j30sg0y7q61.jpg'),
),
),
);
}));
},
),
);
}
}
- 當前頁面的圓形小圖和詳情頁面的大圖都使用Hero包裹。
- 必須使用相同的tag,Flutter Framework通過tag來確定他們之間的關係。
交織動畫
有時候我們需要實現一組複雜的動畫,比如在0.1-0.2秒縮放,從0.2-0.4秒顏色漸變,從0.4-0.8秒左右移動,這時候使用交織動畫可以方便的完成,使用交織動畫需要注意下面幾點
- 需要使用多個Animation對象
- 一個AnimationController控制所有的動畫對象
- 給每一個動畫對象指定時間間隔(Interval)
class AnimateDemo5 extends StatefulWidget {
@override
_AnimateDemo5State createState() => _AnimateDemo5State();
}
class _AnimateDemo5State extends State<AnimateDemo5> with TickerProviderStateMixin{
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this
);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: <Widget>[
SizedBox(height: 30,),
Center(
child: StaggerAnimation(controller: _controller,),
),
SizedBox(height: 30,),
RaisedButton(
child: Text("點擊開始"),
onPressed: () {
_play();
},
textColor: Theme.of(context).primaryColor,
splashColor: Colors.grey[400],
)
],
),
);
}
void _play() async{
//先正向執行動畫
await _controller.forward().orCancel;
//再反向執行動畫
await _controller.reverse().orCancel;
}
}
class StaggerAnimation extends StatelessWidget {
final AnimationController controller;
Animation<double> width,height;
Animation<EdgeInsets> padding;
Animation<Color> color;
Animation<BorderRadius> borderRadius;
StaggerAnimation({Key key,this.controller}): super(key:key){
height = Tween<double>(
begin: 0,
end: 200)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.0,0.4,curve: Curves.ease)));
width = Tween<double>(
begin: 50,
end: 200)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.0,0.4,curve: Curves.ease)));
padding = Tween<EdgeInsets>(
begin:EdgeInsets.only(left: .0),
end:EdgeInsets.only(left: 100.0),
).animate(CurvedAnimation(parent: controller, curve: Interval(0.6, 1.0, curve: Curves.ease)),);
color = ColorTween(
begin:Colors.green ,
end:Colors.red,
).animate(CurvedAnimation(parent: controller, curve: Interval(0.0, 0.4, curve: Curves.ease,)));
borderRadius = BorderRadiusTween(
begin: BorderRadius.circular(3),
end: BorderRadius.circular(35),
).animate(CurvedAnimation(parent: controller, curve: Interval(0.4, 0.6,curve: Curves.ease,),));
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context,child){
return Container(
alignment: Alignment.bottomCenter,
padding:padding.value ,
child: Container(
width: width.value,
height: height.value,
decoration: BoxDecoration(
color: color.value,
border: Border.all(color: Colors.blue,width: 3),
borderRadius:borderRadius.value
),
),
);
},
);
}
}
- StaggerAnimation中定義了5個動畫,寬,高,顏色,左邊距,圓角
- 使用Interval來定義某個動畫執行的時機
- 最後異步啓動動畫。