我們知道當你乘坐飛機飛行的時候你有很多選擇,這里我想表達的是在Flutter中選擇動畫,首先感謝你選擇使用AnimatedBuilder
和 AnimatedWidget
,等等,什么,還沒有使用?Flutter有很多不同的動畫widget,但是與商業航空公司不一樣的是,flutter中的每種類型的widget都有自己的適用場景。當然,你可以使用兩種不同的方式來完成一樣的動畫,但是使用適當的animation widget來完成這項工作,將會更加輕松。
這篇文章介紹了和其他動畫widget對比,你為什么可能需要使用AnimatedBuilder
和 AnimatedWidget
,以及如何使用它們,假設你想向你的APP中添加動畫。本文是該系列文章的一部分,逐步介紹了可能希望使用的各種類型的動畫widget。你想要特定動畫重復執行幾次,或者想要暫停、開始以響應某些事件,比如手指點擊,由于您的動畫需要重復或停止、開始,因此你將需要使用顯式動畫。
順便說一下,Flutter有兩大類型動畫:顯式和隱式。對于顯式動畫,你需要一個animation controller,對于隱式動畫則不需要。在上篇關于使用內置顯示動畫的文章,我們介紹了animation controller,假如你想要了解更多關于此的內容,請先查看那篇文章。
到此,如果你確定使用顯式動畫,有很多顯式動畫供您選擇,這些類通常命名為FooTransition
,Foo
是您想要設置的動畫的屬性名稱,我建議先了解一下是否可以使用其中的一個widget來實現你的需求,然后再深入了解AnimatedBuilder
和 AnimatedWidget
。有很多效果很棒的widget供您選擇,包括旋轉、位移、對齊、淡入淡出、文本樣式等,另外你可以組合這些Widget,這樣就可以同時進行旋轉和淡入淡出效果。但是,如果這些內置的Widget不能滿足你的需求,那么就是時機使用AnimatedBuilder
和 AnimatedWidget
了。
這是用于了解使用哪種動畫的流程圖,本文重點介紹底部的兩個藍色部分,AnimatedBuilder and AnimatedWidget。
特別的例子
為了使以上內容更加具體,讓我們來看一個具體的場景:我想編寫一個帶有外星飛船的APP,這個飛船有一個光柱動畫。
我繪制了一個漸變色的飛船光束,漸變色從正中心向外逐步黃色變為透明,然后,我使用路徑裁剪(path clipper)從該漸變創建了一個光束的形狀。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: MyHomePage(),
));
}
}
class MyHomePage extends StatelessWidget {
final Image starsBackground = Image.asset(
'assets/milky-way.jpg',
);
final Image ufo = Image.asset('assets/ufo.png');
@override
Widget build(BuildContext context) {
return Stack(
alignment: AlignmentDirectional.center,
children: <Widget>[
starsBackground,
ClipPath(
clipper: const BeamClipper(),
child: Container(
height: 1000,
decoration: BoxDecoration(
gradient: RadialGradient(
radius: 1.5,
colors: [
Colors.yellow,
Colors.transparent,
],
),
),
),
),
ufo,
],
);
}
}
class BeamClipper extends CustomClipper<Path> {
const BeamClipper();
@override
getClip(Size size) {
return Path()
..lineTo(size.width / 2, size.height / 2)
..lineTo(size.width, size.height)
..lineTo(0, size.height)
..lineTo(size.width / 2, size.height / 2)
..close();
}
/// Return false always because we always clip the same area.
@override
bool shouldReclip(CustomClipper oldClipper) => false;
}
我想要創建一個光束降落
的動畫,從該漸變的中心開始,并使其重復。這意味著我需要創建顯式動畫,不幸的是,沒有內置的顯式動畫來為漏斗形漸變設置動畫,但是你知道我們有...AnimatedBuilder
和 AnimatedWidget
可以解決這個問題!
AnimatedBuilder
為了制作光束動畫,我將把這段漸變代碼包裹在AnimatedBuilder
widget中。當AnimatedBuilder
被調用的時候,包含在builder函數中漸變代碼也將被調用。
接下來我需要添加一個controller來驅動動畫,controller將會提供AnimatedBuilder
用來逐幀繪制所需要的值。如你在之前的文章里看到的,我混入(mix in)了SingleTickerProviderStateMixin
類,并在initState
而不是build
方法中初始化了controller實例對象,因為我不想多次創建controller--我想要它為動畫的每一幀提供新的值!因為我在initState
中創建了一個新的對象,所以我也添加了一個dispose
方法,用來告知Flutter,當不再有父節點widget顯示在屏幕上的時候,可以銷毀controller。
然后,我將controller傳遞給AnimatedBuilder
,動畫按照預期運行啦!
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
final Image starsBackground = Image.asset(
'assets/milky-way.jpg',
);
final Image ufo = Image.asset('assets/ufo.png');
AnimationController _animation;
@override
void initState() {
super.initState();
_animation = AnimationController(
duration: const Duration(seconds: 5),
vsync: this,
)..repeat();
}
@override
Widget build(BuildContext context) {
return Stack(
alignment: AlignmentDirectional.center,
children: <Widget>[
starsBackground,
AnimatedBuilder(
animation: _animation,
builder: (_, __) {
return ClipPath(
clipper: const BeamClipper(),
child: Container(
height: 1000,
decoration: BoxDecoration(
gradient: RadialGradient(
radius: 1.5,
colors: [
Colors.yellow,
Colors.transparent,
],
stops: [0, _animation.value],
),
),
),
);
},
),
ufo,
],
);
}
@override
void dispose() {
_animation.dispose();
super.dispose();
}
}
class BeamClipper extends CustomClipper<Path> {
const BeamClipper();
@override
getClip(Size size) {
return Path()
..lineTo(size.width / 2, size.height / 2)
..lineTo(size.width, size.height)
..lineTo(0, size.height)
..lineTo(size.width / 2, size.height / 2)
..close();
}
/// Return false always because we always clip the same area.
@override
bool shouldReclip(CustomClipper oldClipper) => false;
}
你可能還記得在TweenAnimationBuilder一文中,我們提到使用child 參數來進行性能優化,我們在AnimatedBuilder
中也可以這樣做。基本上,如果我們在動畫中有從來沒改變過的對象,則可以提前構建他們,然后將它傳遞到AnimatedBuilder
中。
在這個例子中,有一種更好的實現方式來做同樣的事情:給BeamClipper
設置一個const
構造函數,并且僅僅設置了const
。這樣只需要少量的代碼,這個對象將會在編譯期創建,使構建更快速。當然,有時你會編寫一些沒有const構造函數的代碼,這種情況對與使用可選child參數來說是個很好的應用場景。
AnimatedWidget
到此,我們創建了自己的動畫,但是包含AnimatedBuilder
的構建函數代碼量有點大,假如你的構建方法開始變的有點難以閱讀,是時候重構代碼了。
你可以將AnimatedBuilder
代碼提取到單獨的Widget中,但是這樣的話,你的構建方法中將會嵌套另一個構建方法,看起來有點丑陋。取而代之的是,你可以通過繼承自AnimatedWidget
創建一個新的Widget來完成相同的動畫。我將我的Widget命名為BeamTransition
,與FooTransition
顯示動畫的命名習慣一致。我將animation controller傳遞給BeamTransition
,并重用了AnimatedBuilder
構造函數的主體代碼。
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
final Image starsBackground = Image.asset(
'assets/milky-way.jpg',
);
final Image ufo = Image.asset('assets/ufo.png');
AnimationController _animation;
@override
void initState() {
super.initState();
_animation = AnimationController(
duration: const Duration(seconds: 5),
vsync: this,
)..repeat();
}
@override
Widget build(BuildContext context) {
return Stack(
alignment: AlignmentDirectional.center,
children: <Widget>[
starsBackground,
BeamTransition(animation: _animation),
ufo,
],
);
}
@override
void dispose() {
_animation.dispose();
super.dispose();
}
}
class BeamTransition extends AnimatedWidget {
BeamTransition({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return ClipPath(
clipper: const BeamClipper(),
child: Container(
height: 1000,
decoration: BoxDecoration(
gradient: RadialGradient(
radius: 1.5,
colors: [
Colors.yellow,
Colors.transparent,
],
stops: [0, animation.value],
),
),
),
);
}
}
就像AnimatedBuilder
一樣,如果可能的話,我將添加child參數到我的widget中,以便進行性能優化,因為它可以提前而不是每次進行動畫時進行構建。順帶提醒一下,在此例子中,將BeamClipper
采用const
構造聲明是最好的方式。
那么,我到底該用哪個吶?
我們剛剛看到了,當你無法找到內置顯式動畫想要實現你想要的效果時,AnimatedBuilder
和 AnimatedWidget
都可以用來實現相同效果的顯式動畫,那么,你該用哪一個吶?這是一個個人偏好問題,一般來說我建議制作獨立的widget,每個widget負責單獨的功能--在這個例子中是動畫。
絕大多數時,我都贊成使用AnimatedWidget
,但是如果你創建animation controller的父節點Widget非常簡單,那么為你的動畫創建一個獨立的Widget可能會引入太多額外的代碼,這種情況,AnimatedBuilder
是你的首選。
這里有這篇文章的視頻版本,如果你更喜歡視頻,點擊觀看。
系列文章: