深入理解Flutter動(dòng)畫

本文翻譯自
原文地址:Animation deep dive
原作者:Filip Hracek

去年我錄制了Flutter動(dòng)畫系列的一集視頻視頻鏈接 罐脊,我想我也應(yīng)該給那些喜歡看文字而不是視頻的人發(fā)布相同的內(nèi)容侍咱。
Animation videos from the Flutter YouTube channel 在該系列的其他劇集中,我的同事們討論了在Flutter中制作動(dòng)畫的所有實(shí)用方法芋忿,但是在我的劇集中不是這樣子的。在這里尚骄,您將學(xué)習(xí)如何以可想象的最不務(wù)實(shí)的方式實(shí)現(xiàn)動(dòng)畫(但是潭流,此過程中您仍會(huì)學(xué)到一些東西)难菌。

讓我們以輕松而又簡(jiǎn)單的事情開始吧:

什么是運(yùn)動(dòng)
你看途乃,運(yùn)動(dòng)是一種幻想,看這里(Filip揮舞著他的手的視頻):

motion.gif

這是一個(gè)假象扔傅,你實(shí)際上看到的是很多快速連續(xù)顯示的靜止圖像耍共,電影就是這樣工作的。單個(gè)圖片在電影中稱之為幀(frame)--因?yàn)閿?shù)碼顯示屏的工作原理類似--因此在此處也稱之為幀猎塞。在電影中通常一秒顯示24幀试读,現(xiàn)代數(shù)碼設(shè)備每秒顯示60到120幀。

因此荠耽,如果運(yùn)動(dòng)只是假象的話钩骇,那么這些AnimationFooFooTransition到底在做什么,當(dāng)然由于每秒最多需要構(gòu)建120幀,UI不可能每次都會(huì)重新構(gòu)建倘屹。

或者银亲,可能?
實(shí)際上纽匙,F(xiàn)lutter中的動(dòng)畫只是在每一幀上重建部件樹的一部分的一種方法务蝠,沒有特殊情況,F(xiàn)lutter能足夠快的做到這一點(diǎn)烛缔。

讓我們看一下Flutter動(dòng)畫的構(gòu)建塊之一:AnimatedBuilder馏段,它繼承自AnimatedWidget,在AnimatedWidget中會(huì)創(chuàng)建_AnimatedState践瓷,在這個(gè)StateinitState()初始化方法中院喜,我們監(jiān)聽了Animation (或者在這稱之為Listenable),當(dāng)它的值發(fā)生變化時(shí)候晕翠,我們調(diào)用了...setState()喷舀。

AnimationSetState.gif

這個(gè)源碼調(diào)用截圖,證明了我段落內(nèi)闡述的事實(shí)淋肾,動(dòng)畫的構(gòu)造函數(shù)真的每一幀都會(huì)調(diào)用setState()方法元咙。

所以,在Flutter中動(dòng)畫只是一連串的快速的改變Widget的狀態(tài)巫员,每秒60到120次。

我能證明上述結(jié)論甲棍。這是一個(gè)從0到光速的‘動(dòng)畫化’的動(dòng)畫简识,盡管他只是每一幀改變了Text文字,從Flutter的角度來看感猛,他只是又一種動(dòng)畫!


02light.gif

讓我們使用Flutter的動(dòng)畫框架從基本原理出發(fā)構(gòu)建動(dòng)畫七扰。

通常我們將會(huì)使用TweenAnimationBuilder或者其他類似的的Widget,但是在這篇文章中陪白,我們先忽略這些颈走,轉(zhuǎn)而使用tickercontroller咱士、setState立由。

Ticker
先介紹一下Ticker是什么,99%的場(chǎng)景序厉,你并不會(huì)直接使用到ticker锐膜,但是我認(rèn)為介紹一下仍會(huì)是有幫助的--即使只是揭開它的神秘面紗。

// ticker 是一個(gè)每幀都會(huì)執(zhí)行某個(gè)函數(shù)的對(duì)象
// A ticker is an object that calls a function for every frame.

var ticker = Ticker((elapsed) => print('hello'));
ticker.start();

在這個(gè)例子中弛房,我們每幀都會(huì)打印“hello”道盏,雖然并沒有什么用。
并且,我們忘記了調(diào)用ticker.dispose()荷逞,因此我們的ticker將會(huì)一直運(yùn)行媒咳,直到我們殺掉APP。

這也是為什么Flutter給您提供SingleTickerProviderStateMixin种远,也就是你在之前的一些視頻中看到的名字很恰如其分的mixin涩澡。

這種mixin解決了管理ticker的麻煩,只需將它混入到你widget的state中院促,你的State就隱秘的具備了TickerProvider能力筏养。

class _MyWidgetState extends State<MyWidget> 
    with SingleTickerProviderStateMixin<MyWidget> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

這意味著Flutter框架可以向你的state尋求ticker能力,最重要的是AnimationController可以向state尋求ticker常拓。

class _MyWidgetState extends State<MyWidget>
    with SingleTickerProviderStateMixin<MyWidget> {
  AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
  }
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

AnimationController的初始化需要ticker參數(shù)渐溶,如果你使用了SingleTickerProviderStateMixin或者類似的TickerProviderStateMixin,你就可以將this傳參給AnimationController弄抬,那么就初始化完成了茎辐。

AnimationController
AnimationController通常用來播放暫停掂恕、翻轉(zhuǎn)拖陆、停止動(dòng)畫,和純粹的"tick"不同懊亡,AnimationController可以隨時(shí)告訴我們動(dòng)畫執(zhí)行到了哪個(gè)點(diǎn)依啰,例如我們的動(dòng)畫執(zhí)行到一半嗎,執(zhí)行到了99%店枣,還是已經(jīng)執(zhí)行完成了速警?

AnimationController.gif

通常你可以使用AnimationController,或者使用Curve對(duì)它進(jìn)行轉(zhuǎn)換鸯两,或者將它放置到Tween中闷旧,或者可以用在像FadeTransitionTweenAnimationBuilder這樣便捷使用的widget中钧唐。但是出于教學(xué)目的忙灼,以上這些我們都不使用,相反钝侠,我們直接調(diào)用setState该园。

setState
初始化AnimationController之后,我們通過它添加一個(gè)監(jiān)聽器帅韧,在監(jiān)聽器內(nèi)調(diào)用setState

class _MyWidgetState extends State<MyWidget>
    with SingleTickerProviderStateMixin<MyWidget> {
  AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
    _controller.addListener(_update);
  }
  void _update() {
    setState(() {
      // TODO
    });
  }
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

現(xiàn)在爬范,我們或許應(yīng)該設(shè)置state了,保持簡(jiǎn)單的integer類型弱匪,不要忘記在build方法中實(shí)際使用state青瀑,并且在我們的監(jiān)聽中根據(jù)controller的當(dāng)前值改變state璧亮。

class _MyWidgetState extends State<MyWidget>
    with SingleTickerProviderStateMixin<MyWidget> {
  AnimationController _controller;
  int i = 0;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
    _controller.addListener(_update);
  }
  void _update() {
    setState(() {
      i = (_controller.value * 299792458).round();
    });
  }
  @override
  Widget build(BuildContext context) {
    return Text('$i m/s');
  }
}

這段代碼根據(jù)動(dòng)畫的進(jìn)度,分配一個(gè)從0到光速的值斥难。

運(yùn)行動(dòng)畫
現(xiàn)在我們只需要告訴動(dòng)畫執(zhí)行應(yīng)該多長(zhǎng)時(shí)間枝嘶,然后開始動(dòng)畫

class _MyWidgetState extends State<MyWidget>
    with SingleTickerProviderStateMixin<MyWidget> {
  AnimationController _controller;
  int i = 0;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
    _controller.addListener(_update);
  }
  void _update() {
    setState(() {
      i = (_controller.value * 299792458).round();
    });
  }
  @override
  Widget build(BuildContext context) {
    return Text('$i m/s');
  }
}

這個(gè)widget動(dòng)畫一旦添加到屏幕上就立刻開始執(zhí)行,”動(dòng)畫“在一秒內(nèi)從0過渡到光速哑诊。


02light.gif

銷毀controller
不要忘記銷毀AnimationController群扶,否則你的APP將會(huì)有內(nèi)存泄漏。

class _MyWidgetState extends State<MyWidget>
    with SingleTickerProviderStateMixin<MyWidget> {
  AnimationController _controller;
  int i = 0;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this, 
      duration: const Duration(seconds: 1),
    );
    _controller.addListener(_update);
    _controller.forward();
  }
  void _update() {
    setState(() {
      i = (_controller.value * 299792458).round();
    });
  }
  @override
  Widget build(BuildContext context) {
    return Text('$i m/s');
  }
}

只使用內(nèi)置widget镀裤,可以嗎
如你所見竞阐,獨(dú)自完成所有操作并不理想,達(dá)到同樣的功能暑劝,使用TweenAnimationBuilder用了少的代碼骆莹,并且不用在AnimationController和調(diào)用setState之間費(fèi)神。

class MyPragmaticWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder(
      tween: IntTween(begin: 0, end: 299792458),
      duration: const Duration(seconds: 1),
      builder: (BuildContext context, int i, Widget child) {
        return Text('$i m/s');
      },
    );
  }
}

總結(jié)
我們了解了Ticker是什么担猛,了解了怎么給AnimationController添加監(jiān)聽幕垦,了解了從根本上講,動(dòng)畫就是快速連續(xù)的對(duì)widget進(jìn)行rebuild傅联,你可以在做在每一幀做你想做的任何事情先改。

系列文章:

視頻 對(duì)應(yīng)文章(英文原文) 對(duì)應(yīng)文章(中文翻譯)
如何在 Flutter 中選擇合適的動(dòng)畫 Widget?????在 Flutter中使用動(dòng)畫的正確選擇 How to Choose Which Flutter Animation Widget is Right for You? 【已翻譯】鏈接
隱式動(dòng)畫基礎(chǔ) Flutter animation basics with implicit animations 【已翻譯】鏈接
使用 TweenAnimationBuilder 創(chuàng)建獨(dú)特的隱式動(dòng)畫 Custom Implicit Animations in Flutter…with TweenAnimationBuilder 【已翻譯】鏈接
使用內(nèi)置顯式動(dòng)畫 Directional animations with built-in explicit animations 【已翻譯】鏈接
通過 AnimatedBuilder 和 AnimatedWidget 創(chuàng)建一個(gè)自定義動(dòng)畫 When should I useAnimatedBuilder or AnimatedWidget? 【已翻譯】鏈接
深入理解動(dòng)畫 Animation deep dive 【已翻譯】鏈接
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蒸走,隨后出現(xiàn)的幾起案子仇奶,更是在濱河造成了極大的恐慌,老刑警劉巖比驻,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件该溯,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡嫁艇,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門弦撩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來步咪,“玉大人,你說我怎么就攤上這事益楼』” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵感凤,是天一觀的道長(zhǎng)悯周。 經(jīng)常有香客問我,道長(zhǎng)陪竿,這世上最難降的妖魔是什么禽翼? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上闰挡,老公的妹妹穿的比我還像新娘锐墙。我一直安慰自己,他們只是感情好长酗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布溪北。 她就那樣靜靜地躺著,像睡著了一般夺脾。 火紅的嫁衣襯著肌膚如雪之拨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天咧叭,我揣著相機(jī)與錄音蚀乔,去河邊找鬼。 笑死佳簸,一個(gè)胖子當(dāng)著我的面吹牛乙墙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播生均,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼听想,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了马胧?” 一聲冷哼從身側(cè)響起汉买,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎佩脊,沒想到半個(gè)月后蛙粘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡威彰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年出牧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歇盼。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡舔痕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出豹缀,到底是詐尸還是另有隱情伯复,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布邢笙,位于F島的核電站啸如,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏氮惯。R本人自食惡果不足惜叮雳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一想暗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧债鸡,春花似錦江滨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至棺弊,卻和暖如春晶密,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背模她。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工稻艰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侈净。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓尊勿,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親畜侦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子元扔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容