Flutter框架分析(五)-- 動(dòng)畫

例子

所謂動(dòng)畫其實(shí)就是一系列連續(xù)變化的圖片在極短的時(shí)間逐幀顯示,在人眼看來(lái)就是動(dòng)畫了。這里我們舉一個(gè)簡(jiǎn)單的例子先說(shuō)明一下在Flutter中怎么運(yùn)行一個(gè)動(dòng)畫:

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: LogoAnim()));
}

class LogoAnim extends StatefulWidget {
  _LogoAnimState createState() => _LogoAnimState();
}

class _LogoAnimState extends State<LogoAnim> 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)
      ..addListener(() {
        setState(() {
        });
      });
    controller.forward(from: 0);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }

  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

這個(gè)動(dòng)畫是在手機(jī)屏幕上由小到大漸變的顯示一個(gè)Flutter標(biāo)志永乌。從上述代碼中我們可以看到在Flutter中實(shí)現(xiàn)一個(gè)動(dòng)畫要做這么幾件事吨述。

  1. 首先施加動(dòng)畫的Widget是個(gè)StatefulWidget。其State要混入(mixin) SingleTickerProviderStateMixin歧寺。
  2. 在initState()里要加入和動(dòng)畫相關(guān)的初始化燥狰,這里我們實(shí)例化了兩個(gè)類AnimationController和Animation棘脐。實(shí)例化AnimationController的時(shí)候我們傳入了兩個(gè)參數(shù),一個(gè)是動(dòng)畫的時(shí)長(zhǎng)龙致,另一個(gè)是State自己蛀缝,這里其實(shí)是利用到了混入的SingleTickerProviderStateMixin。實(shí)例化另一個(gè)Animation的時(shí)候目代,我們首先實(shí)例化的是一個(gè)Tween屈梁。這個(gè)類其實(shí)代表了從最小值到最大值的一個(gè)線性變化。所以實(shí)例化的時(shí)候要傳入開始和結(jié)束值榛了。然后調(diào)用animate()并傳入之前的controller在讶。這個(gè)調(diào)用會(huì)返回我們需要的Animation實(shí)例。顯然我們需要知道動(dòng)畫的屬性變化的時(shí)候的消息霜大,所以這里會(huì)通過(guò)..addListener()給Animation實(shí)例注冊(cè)回調(diào)构哺。這個(gè)回調(diào)只做一件事,那就是調(diào)用setState()來(lái)更新UI战坤。最后就是調(diào)用controller.forward()來(lái)啟動(dòng)動(dòng)畫曙强。
  3. 注意在build()函數(shù)里我們構(gòu)建widget的時(shí)候用到了animation.value。所以這里的鏈條就是動(dòng)畫在收到回調(diào)后會(huì)調(diào)用setState()途茫,而從我們上篇文章知道setState之后在渲染流水線的構(gòu)建階段會(huì)走到build()來(lái)重建Widget旗扑。重建的時(shí)候就用到了發(fā)生變化以后的animation.value。這個(gè)一幀一幀的循環(huán)慈省,我們的動(dòng)畫就動(dòng)起來(lái)了臀防。
  4. 最后在dispose()的時(shí)候要記得調(diào)用controller.dispose()釋放資源。

接下來(lái)我們就深入Flutter源碼來(lái)看一下動(dòng)畫是如何運(yùn)行的边败。

分析

首先我們來(lái)看一下混入到State中的SingleTickerProviderStateMixin袱衷。

mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
  Ticker _ticker;

  @override
  Ticker createTicker(TickerCallback onTick) {
    _ticker = Ticker(onTick, debugLabel: 'created by $this');
    return _ticker;
  }

  @override
  void didChangeDependencies() {
    if (_ticker != null)
      _ticker.muted = !TickerMode.of(context);
    super.didChangeDependencies();
  }
}

這個(gè)混入其實(shí)就做了一件事,實(shí)現(xiàn)createTicker()來(lái)實(shí)例化一個(gè)Ticker類笑窜。在另一個(gè)函數(shù)didChangeDependencies()里致燥,有這樣一行代碼_ticker.muted = !TickerMode.of(context);。這行代碼的意思是在這個(gè)帶有動(dòng)畫的State的在element tree中的依賴發(fā)生變化的時(shí)候是否mute自己的_ticker排截。一個(gè)場(chǎng)景就是當(dāng)前頁(yè)的動(dòng)畫還在播放的時(shí)候嫌蚤,用戶導(dǎo)航到另外一個(gè)頁(yè)面,當(dāng)前頁(yè)的動(dòng)畫就沒(méi)有必要再播放了断傲,反之在頁(yè)面切換回來(lái)的時(shí)候動(dòng)畫有可能還要繼續(xù)播放脱吱,控制的地方就在這里,注意TickerMode.of(context)這種方式认罩,我們?cè)贔lutter框架中很多地方都會(huì)見到箱蝠,基本上就是從element tree的祖先里找到對(duì)應(yīng)那個(gè)InheritedWidget的方式。

Ticker顧名思義,就是給動(dòng)畫提供vsync信號(hào)的吧宦搬。我們來(lái)看下源碼一探究竟牙瓢。

class Ticker {

  TickerFuture _future;
  
  bool get muted => _muted;
  bool _muted = false;
  set muted(bool value) {
    if (value == muted)
      return;
    _muted = value;
    if (value) {
      unscheduleTick();
    } else if (shouldScheduleTick) {
      scheduleTick();
    }
  }

  bool get isTicking {
    if (_future == null)
      return false;
    if (muted)
      return false;
    if (SchedulerBinding.instance.framesEnabled)
      return true;
    if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.idle)
      return true; 
    return false;
  }

  bool get isActive => _future != null;

  Duration _startTime;

  TickerFuture start() {
    _future = TickerFuture._();
    if (shouldScheduleTick) {
      scheduleTick();
    }
    if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
        SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
      _startTime = SchedulerBinding.instance.currentFrameTimeStamp;
    return _future;
  }
  
  void stop({ bool canceled = false }) {
    if (!isActive)
      return;

    final TickerFuture localFuture = _future;
    _future = null;
    _startTime = null;

    unscheduleTick();
    if (canceled) {
      localFuture._cancel(this);
    } else {
      localFuture._complete();
    }
  }


  final TickerCallback _onTick;

  int _animationId;

  @protected
  bool get scheduled => _animationId != null;

  @protected
  bool get shouldScheduleTick => !muted && isActive && !scheduled;

  void _tick(Duration timeStamp) {
    _animationId = null;

    _startTime ??= timeStamp;
    _onTick(timeStamp - _startTime);
    
    if (shouldScheduleTick)
      scheduleTick(rescheduling: true);
  }

  @protected
  void scheduleTick({ bool rescheduling = false }) {
    _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
  }

  @protected
  void unscheduleTick() {
    if (scheduled) {
      SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId);
      _animationId = null;
    }
    
  }
}

可以看到Ticker主要在做的有點(diǎn)像控制一個(gè)計(jì)時(shí)器,有start()和stop()和mute间校。還記錄當(dāng)前自己的狀態(tài)isTicking矾克。我們需要關(guān)注的的是scheduleTick()這個(gè)函數(shù):

@protected
  void scheduleTick({ bool rescheduling = false }) {
    _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
  }

你看,這里就跑到了我們之前文章說(shuō)的SchedulerBinding里面去了憔足。這里調(diào)度的時(shí)候會(huì)傳入Ticker的回調(diào)函數(shù)_tick胁附。

  int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
    scheduleFrame();
    _nextFrameCallbackId += 1;
    _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
    return _nextFrameCallbackId;
  }

在調(diào)度一幀的時(shí)候Ticker的回調(diào)函數(shù)_tick被加入了transientCallbacks。從之前對(duì)渲染流水線的分析四瘫,我們知道transientCallbacks會(huì)在vsync信號(hào)到來(lái)以后window的onBeginFrame回調(diào)里被執(zhí)行一次。也就是說(shuō)此時(shí)就進(jìn)入到渲染流水線的動(dòng)畫Animate階段了欲逃。

接著我們就看下Ticker的回調(diào)函數(shù)_tick做了什么:

  void _tick(Duration timeStamp) {
    _animationId = null;

    _startTime ??= timeStamp;
    _onTick(timeStamp - _startTime);

    if (shouldScheduleTick)
      scheduleTick(rescheduling: true);
  }

這里的_onTick是在實(shí)例化Ticker時(shí)候傳入的找蜜。_onTick被調(diào)用之后,Ticker如果發(fā)現(xiàn)自己的任務(wù)還沒(méi)有完成稳析,還要接著跳動(dòng)洗做,那就再來(lái)調(diào)度新一幀。所以你看動(dòng)畫的動(dòng)力其實(shí)還是來(lái)自vsync信號(hào)的彰居。

那么這個(gè)_onTick又是啥樣的呢诚纸?這個(gè)函數(shù)是在實(shí)例化Ticker的時(shí)候傳入的。而從上述分析我們又知道陈惰,Ticker的實(shí)例化是在調(diào)用TickerProvider.createTicker()的時(shí)候完成的畦徘。誰(shuí)來(lái)調(diào)用這個(gè)函數(shù)呢?是AnimationController抬闯。

  AnimationController({
    double value,
    this.duration,
    this.debugLabel,
    this.lowerBound = 0.0,
    this.upperBound = 1.0,
    this.animationBehavior = AnimationBehavior.normal,
    @required TickerProvider vsync,
  }) : _direction = _AnimationDirection.forward {
    _ticker = vsync.createTicker(_tick);
    _internalSetValue(value ?? lowerBound);
  }

可見在其構(gòu)造函數(shù)里就調(diào)用createTicker()了井辆,傳入的參數(shù)是_ticker。 接著看_ticker溶握。

  void _tick(Duration elapsed) {
    _lastElapsedDuration = elapsed;
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
    if (_simulation.isDone(elapsedInSeconds)) {
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
      stop(canceled: false);
    }
    notifyListeners();
    _checkStatusChanged();
  }

這個(gè)回調(diào)里做這幾件事杯缺,根據(jù)vsync到來(lái)以后的時(shí)間戳來(lái)計(jì)算更新一下新的值,這里計(jì)算用的是個(gè)_simulation睡榆。為啥叫這名萍肆?因?yàn)檫@是用來(lái)模擬一個(gè)物體在外力作用下在不同的時(shí)間點(diǎn)的運(yùn)動(dòng)狀態(tài)的變化,這也算是動(dòng)畫的本質(zhì)吧胀屿。

算出來(lái)新的值以后就調(diào)用notifyListeners()來(lái)通知各位觀察者塘揣。還記的在開始的例子里我們實(shí)例化animation以后會(huì)通過(guò)..addListener()添加的回調(diào)嗎?在這里這個(gè)回調(diào)就會(huì)被調(diào)用宿崭,也就是setState()會(huì)被調(diào)用了勿负。接下來(lái)就是渲染流水線的構(gòu)建(build)階段了。

看到這里你可能會(huì)有疑問(wèn),事情都讓AnimationController做了奴愉,那那個(gè)例子里的Tween是用來(lái)干啥的琅摩?

從AnimationController的構(gòu)造函數(shù)里我們可以看出來(lái),它只管[0.0, 1.0]之間的模擬锭硼,也就是說(shuō)不管動(dòng)畫怎么動(dòng)房资,它任何時(shí)候只輸出0.0到1.0之間的值,但是我們的動(dòng)畫有旋轉(zhuǎn)角度檀头,顏色漸變轰异,圖形變化以及更復(fù)雜的組合,顯然我們得想辦法把0.0到1.0之間的值轉(zhuǎn)換為我們需要的角度暑始,位置搭独,顏色,透明度等等廊镜,這個(gè)轉(zhuǎn)化就是由各種Animation來(lái)完成的牙肝,像例子里說(shuō)的Tween,它的任務(wù)在動(dòng)畫期間把值從0漸變到300嗤朴。怎么做呢配椭?在實(shí)例化Tween以后我們會(huì)調(diào)用animate(),傳入AnimationController實(shí)例雹姊。

  Animation<T> animate(Animation<double> parent) {
    return _AnimatedEvaluation<T>(parent, this);
  }

你看股缸,入?yún)⑹莻€(gè)Animation<double>,這里也就是AnimationController吱雏。出參則是個(gè)Animation<T>敦姻。這樣就完成了從[0.0, 1.0]到任意類型的變化。

具體怎么變呢歧杏?這個(gè)變化其實(shí)是在用到這個(gè)值得時(shí)候發(fā)生的替劈,上面的例子里在State.build()函數(shù)里構(gòu)造widget的時(shí)候會(huì)調(diào)用到animation.value這個(gè)getter。這其實(shí)調(diào)用的是_AnimatedEvaluation.value得滤。

 @override
  T get value => _evaluatable.evaluate(parent);

_evaluatable就是Tween了陨献,parent就是AnimationController了。所以呢懂更,這個(gè)轉(zhuǎn)換是Tween自己完成的眨业,也是,只有它自己知道需要什么樣的輸出沮协。

T evaluate(Animation<double> animation) => transform(animation.value);

又到了transform()里了

@override
  T transform(double t) {
    if (t == 0.0)
      return begin;
    if (t == 1.0)
      return end;
    return lerp(t);
  }

看到范圍限制了嗎龄捡?真正的轉(zhuǎn)換又是在lerp()里完成的。

@protected
  T lerp(double t) {
    return begin + (end - begin) * t;
  }

很簡(jiǎn)單的線性插值慷暂。
所里你要理解Flutter中的Tween動(dòng)畫是干什么的只要把握住它在自己的transform()函數(shù)中做了什么事情就知道了聘殖,從上可知Tween其實(shí)就是在做線性插值的動(dòng)畫而已晨雳。Tween是線性插值的,那如果我想搞非線性插值的動(dòng)畫呢奸腺?那就用CurvedAnimation餐禁。Flutter里有一大票各種各樣的線性插值動(dòng)畫和非線性插值的動(dòng)畫,你甚至可以自己定義自己的非線性動(dòng)畫突照,只要重寫變換函數(shù)就行了:

import 'dart:math';
class ShakeCurve extends Curve {
  @override
  double transform(double t) => sin(t * pi * 2);
}

好了帮非,關(guān)于Flutter框架里的動(dòng)畫就先分析到這里。

創(chuàng)作不易喜歡的話記得點(diǎn)擊+關(guān)注哦

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末讹蘑,一起剝皮案震驚了整個(gè)濱河市末盔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌座慰,老刑警劉巖陨舱,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異版仔,居然都是意外死亡游盲,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門邦尊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)背桐,“玉大人优烧,你說(shuō)我怎么就攤上這事蝉揍。” “怎么了畦娄?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵又沾,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我熙卡,道長(zhǎng)杖刷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任驳癌,我火速辦了婚禮滑燃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘颓鲜。我一直安慰自己表窘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布甜滨。 她就那樣靜靜地躺著乐严,像睡著了一般。 火紅的嫁衣襯著肌膚如雪衣摩。 梳的紋絲不亂的頭發(fā)上昂验,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼既琴。 笑死占婉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的呛梆。 我是一名探鬼主播锐涯,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼填物!你這毒婦竟也來(lái)了纹腌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤滞磺,失蹤者是張志新(化名)和其女友劉穎升薯,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體击困,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涎劈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阅茶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蛛枚。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖脸哀,靈堂內(nèi)的尸體忽然破棺而出蹦浦,到底是詐尸還是另有隱情,我是刑警寧澤撞蜂,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布盲镶,位于F島的核電站,受9級(jí)特大地震影響蝌诡,放射性物質(zhì)發(fā)生泄漏溉贿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一浦旱、第九天 我趴在偏房一處隱蔽的房頂上張望宇色。 院中可真熱鬧,春花似錦颁湖、人聲如沸宣蠕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)植影。三九已至,卻和暖如春涎永,著一層夾襖步出監(jiān)牢的瞬間思币,已是汗流浹背鹿响。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谷饿,地道東北人惶我。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像博投,于是被迫代替她去往敵國(guó)和親绸贡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

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

  • 本文通過(guò)代碼層面去分析Flutter動(dòng)畫的實(shí)現(xiàn)過(guò)程毅哗,介紹了Flutter中的Animation庫(kù)以及Physics...
    Q吹個(gè)大氣球Q閱讀 3,474評(píng)論 2 12
  • 1 背景 不能只分析源碼呀听怕,分析的同時(shí)也要整理歸納基礎(chǔ)知識(shí),剛好有人微博私信讓全面說(shuō)說(shuō)Android的動(dòng)畫虑绵,所以今...
    未聞椛洺閱讀 2,711評(píng)論 0 10
  • 該文已授權(quán)公眾號(hào) 「碼個(gè)蛋」尿瞭,轉(zhuǎn)載請(qǐng)指明出處 在 Flutter 中,自帶手勢(shì)監(jiān)聽的目前為止好像只有按鈕部件和一些...
    Kuky_xs閱讀 1,709評(píng)論 2 3
  • 【Android 動(dòng)畫】 動(dòng)畫分類補(bǔ)間動(dòng)畫(Tween動(dòng)畫)幀動(dòng)畫(Frame 動(dòng)畫)屬性動(dòng)畫(Property ...
    Rtia閱讀 6,164評(píng)論 1 38
  • 動(dòng)畫實(shí)現(xiàn)的方式 Flutter中翅睛,我們可以簡(jiǎn)單的把調(diào)用this.setState()理解為渲染一幀声搁。那么只要我們不...
    shawn_yy閱讀 1,843評(píng)論 0 6