我們知道當(dāng)你乘坐飛機(jī)飛行的時(shí)候你有很多選擇,這里我想表達(dá)的是在Flutter中選擇動(dòng)畫(huà),首先感謝你選擇使用AnimatedBuilder
和 AnimatedWidget
,等等茧泪,什么,還沒(méi)有使用聋袋?Flutter有很多不同的動(dòng)畫(huà)widget队伟,但是與商業(yè)航空公司不一樣的是,flutter中的每種類型的widget都有自己的適用場(chǎng)景幽勒。當(dāng)然嗜侮,你可以使用兩種不同的方式來(lái)完成一樣的動(dòng)畫(huà),但是使用適當(dāng)?shù)腶nimation widget來(lái)完成這項(xiàng)工作啥容,將會(huì)更加輕松锈颗。
這篇文章介紹了和其他動(dòng)畫(huà)widget對(duì)比,你為什么可能需要使用AnimatedBuilder
和 AnimatedWidget
咪惠,以及如何使用它們宜猜,假設(shè)你想向你的APP中添加動(dòng)畫(huà)。本文是該系列文章的一部分硝逢,逐步介紹了可能希望使用的各種類型的動(dòng)畫(huà)widget。你想要特定動(dòng)畫(huà)重復(fù)執(zhí)行幾次绅喉,或者想要暫停渠鸽、開(kāi)始以響應(yīng)某些事件,比如手指點(diǎn)擊柴罐,由于您的動(dòng)畫(huà)需要重復(fù)或停止徽缚、開(kāi)始,因此你將需要使用顯式動(dòng)畫(huà)革屠。
順便說(shuō)一下凿试,F(xiàn)lutter有兩大類型動(dòng)畫(huà):顯式和隱式排宰。對(duì)于顯式動(dòng)畫(huà),你需要一個(gè)animation controller那婉,對(duì)于隱式動(dòng)畫(huà)則不需要板甘。在上篇關(guān)于使用內(nèi)置顯示動(dòng)畫(huà)的文章,我們介紹了animation controller详炬,假如你想要了解更多關(guān)于此的內(nèi)容盐类,請(qǐng)先查看那篇文章。
到此呛谜,如果你確定使用顯式動(dòng)畫(huà)在跳,有很多顯式動(dòng)畫(huà)供您選擇,這些類通常命名為FooTransition
隐岛,Foo
是您想要設(shè)置的動(dòng)畫(huà)的屬性名稱猫妙,我建議先了解一下是否可以使用其中的一個(gè)widget來(lái)實(shí)現(xiàn)你的需求,然后再深入了解AnimatedBuilder
和 AnimatedWidget
聚凹。有很多效果很棒的widget供您選擇割坠,包括旋轉(zhuǎn)、位移元践、對(duì)齊韭脊、淡入淡出、文本樣式等单旁,另外你可以組合這些Widget沪羔,這樣就可以同時(shí)進(jìn)行旋轉(zhuǎn)和淡入淡出效果。但是象浑,如果這些內(nèi)置的Widget不能滿足你的需求蔫饰,那么就是時(shí)機(jī)使用AnimatedBuilder
和 AnimatedWidget
了。
這是用于了解使用哪種動(dòng)畫(huà)的流程圖愉豺,本文重點(diǎn)介紹底部的兩個(gè)藍(lán)色部分篓吁,AnimatedBuilder and AnimatedWidget。
特別的例子
為了使以上內(nèi)容更加具體蚪拦,讓我們來(lái)看一個(gè)具體的場(chǎng)景:我想編寫(xiě)一個(gè)帶有外星飛船的APP杖剪,這個(gè)飛船有一個(gè)光柱動(dòng)畫(huà)。
我繪制了一個(gè)漸變色的飛船光束驰贷,漸變色從正中心向外逐步黃色變?yōu)橥该魇⒑伲缓螅沂褂寐窂讲眉簦╬ath clipper)從該漸變創(chuàng)建了一個(gè)光束的形狀括袒。
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;
}
我想要?jiǎng)?chuàng)建一個(gè)光束降落
的動(dòng)畫(huà)次兆,從該漸變的中心開(kāi)始,并使其重復(fù)锹锰。這意味著我需要?jiǎng)?chuàng)建顯式動(dòng)畫(huà)芥炭,不幸的是漓库,沒(méi)有內(nèi)置的顯式動(dòng)畫(huà)來(lái)為漏斗形漸變?cè)O(shè)置動(dòng)畫(huà),但是你知道我們有...AnimatedBuilder
和 AnimatedWidget
可以解決這個(gè)問(wèn)題园蝠!
AnimatedBuilder
為了制作光束動(dòng)畫(huà)渺蒿,我將把這段漸變代碼包裹在AnimatedBuilder
widget中。當(dāng)AnimatedBuilder
被調(diào)用的時(shí)候砰琢,包含在builder函數(shù)中漸變代碼也將被調(diào)用蘸嘶。
接下來(lái)我需要添加一個(gè)controller來(lái)驅(qū)動(dòng)動(dòng)畫(huà),controller將會(huì)提供AnimatedBuilder
用來(lái)逐幀繪制所需要的值陪汽。如你在之前的文章里看到的训唱,我混入(mix in)了SingleTickerProviderStateMixin
類,并在initState
而不是build
方法中初始化了controller實(shí)例對(duì)象挚冤,因?yàn)槲也幌攵啻蝿?chuàng)建controller--我想要它為動(dòng)畫(huà)的每一幀提供新的值况增!因?yàn)槲以?code>initState中創(chuàng)建了一個(gè)新的對(duì)象,所以我也添加了一個(gè)dispose
方法训挡,用來(lái)告知Flutter澳骤,當(dāng)不再有父節(jié)點(diǎn)widget顯示在屏幕上的時(shí)候,可以銷毀controller澜薄。
然后为肮,我將controller傳遞給AnimatedBuilder
,動(dòng)畫(huà)按照預(yù)期運(yùn)行啦肤京!
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 參數(shù)來(lái)進(jìn)行性能優(yōu)化,我們?cè)?code>AnimatedBuilder中也可以這樣做忘分∑逭恚基本上,如果我們?cè)趧?dòng)畫(huà)中有從來(lái)沒(méi)改變過(guò)的對(duì)象妒峦,則可以提前構(gòu)建他們重斑,然后將它傳遞到AnimatedBuilder
中。
在這個(gè)例子中肯骇,有一種更好的實(shí)現(xiàn)方式來(lái)做同樣的事情:給BeamClipper
設(shè)置一個(gè)const
構(gòu)造函數(shù)窥浪,并且僅僅設(shè)置了const
。這樣只需要少量的代碼笛丙,這個(gè)對(duì)象將會(huì)在編譯期創(chuàng)建漾脂,使構(gòu)建更快速。當(dāng)然若债,有時(shí)你會(huì)編寫(xiě)一些沒(méi)有const構(gòu)造函數(shù)的代碼,這種情況對(duì)與使用可選child參數(shù)來(lái)說(shuō)是個(gè)很好的應(yīng)用場(chǎng)景拆融。
AnimatedWidget
到此蠢琳,我們創(chuàng)建了自己的動(dòng)畫(huà)啊终,但是包含AnimatedBuilder
的構(gòu)建函數(shù)代碼量有點(diǎn)大,假如你的構(gòu)建方法開(kāi)始變的有點(diǎn)難以閱讀傲须,是時(shí)候重構(gòu)代碼了蓝牲。
你可以將AnimatedBuilder
代碼提取到單獨(dú)的Widget中,但是這樣的話泰讽,你的構(gòu)建方法中將會(huì)嵌套另一個(gè)構(gòu)建方法例衍,看起來(lái)有點(diǎn)丑陋。取而代之的是已卸,你可以通過(guò)繼承自AnimatedWidget
創(chuàng)建一個(gè)新的Widget來(lái)完成相同的動(dòng)畫(huà)佛玄。我將我的Widget命名為BeamTransition
,與FooTransition
顯示動(dòng)畫(huà)的命名習(xí)慣一致累澡。我將animation controller傳遞給BeamTransition
梦抢,并重用了AnimatedBuilder
構(gòu)造函數(shù)的主體代碼。
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參數(shù)到我的widget中,以便進(jìn)行性能優(yōu)化蕊梧,因?yàn)樗梢蕴崆岸皇敲看芜M(jìn)行動(dòng)畫(huà)時(shí)進(jìn)行構(gòu)建霞赫。順帶提醒一下,在此例子中肥矢,將BeamClipper
采用const
構(gòu)造聲明是最好的方式端衰。
那么,我到底該用哪個(gè)吶橄抹?
我們剛剛看到了靴迫,當(dāng)你無(wú)法找到內(nèi)置顯式動(dòng)畫(huà)想要實(shí)現(xiàn)你想要的效果時(shí),AnimatedBuilder
和 AnimatedWidget
都可以用來(lái)實(shí)現(xiàn)相同效果的顯式動(dòng)畫(huà)楼誓,那么玉锌,你該用哪一個(gè)吶?這是一個(gè)個(gè)人偏好問(wèn)題疟羹,一般來(lái)說(shuō)我建議制作獨(dú)立的widget主守,每個(gè)widget負(fù)責(zé)單獨(dú)的功能--在這個(gè)例子中是動(dòng)畫(huà)。
絕大多數(shù)時(shí)榄融,我都贊成使用AnimatedWidget
参淫,但是如果你創(chuàng)建animation controller的父節(jié)點(diǎn)Widget非常簡(jiǎn)單,那么為你的動(dòng)畫(huà)創(chuàng)建一個(gè)獨(dú)立的Widget可能會(huì)引入太多額外的代碼愧杯,這種情況涎才,AnimatedBuilder
是你的首選。
這里有這篇文章的視頻版本,如果你更喜歡視頻耍铜,點(diǎn)擊觀看邑闺。
系列文章: