Flutter 中的 Animations(三)

官方文檔

如果沒有看過之前兩節(jié)的,建議先看看前兩節(jié)的內(nèi)容

上一節(jié)中我們主要做的事情如下:
  • AnimationaddListener(...) 中調(diào)用 setState(...) 來給 widget 添加動畫
  • 使用 AnimatedWidget 的方法來給 widget 添加動畫

上一節(jié)中我們也通過案例的形式展現(xiàn)出以上兩種方式的區(qū)別,得出的結(jié)論就是如下:
使用 addListener(...)setState(...)Widget 添加動畫的時候挺邀,會導(dǎo)致其他的 Widget 也跟著重繪蛉拙,而使用 AnimatedWidget 的方式給 Widget 添加動畫的時候只會重繪當(dāng)前的 Widget

這是為什么呢 岛请?压怠?掐场?

我們先來看看動畫的執(zhí)行流程躯保。

我們在使用動畫的時候會初始化一個 AnimationController

AnimationController controller = AnimationController(vsync: this, duration: Duration(milliseconds: 300));

可以看出旋膳,初始化 AnimationController 需要傳遞兩個參數(shù),一個是 vsync 一個是 duration 途事,這個 duration 很容易理解验懊,就是動畫執(zhí)行完畢需要的時長擅羞,那 vsync 是什么啊义图?上述代碼中我們傳入了 this 祟滴,那是因?yàn)槲覀冊?Statewith(混入,第一節(jié) 有講)SingleTickerProviderStateMixin歌溉,下面我們看看初始化 AnimationController 時都干了什么事垄懂,源碼如下:

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

我們抽重點(diǎn)出來看

_ticker = vsync.createTicker(_tick);

這里調(diào)用了 vsynccreateTicker(...) 方法創(chuàng)建了一個 Ticker, 而我們在初始化 AnimationController 的時候 vsync 參數(shù)傳的是 SingleTickerProviderStateMixin痛垛,SingleTickerProviderStateMixin#createTicker(...) 的源碼如下:

Ticker createTicker(TickerCallback onTick) {

    ...
    
    _ticker = new Ticker(onTick, debugLabel: 'created by $this');
    
    ...
    
    return _ticker;
  }

這個 Ticker 是個什么東西呢草慧?看看它的注釋吧!以下為 Ticker 類的部分注釋

Calls its callback once per animation frame.

When created, a ticker is initially disabled. Call [start] to enable the ticker.

Creates a ticker that will call the provided callback once per frame while running.

  • 第一句的大概意思就是 Ticker 類在每一幀動畫中都會調(diào)用他自己的回調(diào)一次
  • 第二句的大概意思就是 當(dāng) Ticker 被創(chuàng)建之后匙头,默認(rèn)是 不可用(disabled)的漫谷,可以調(diào)用 start 方法來使 Ticker 變得可用(enable)
  • 第三句的大概意思就是 創(chuàng)建一個 Ticker,他將在運(yùn)行時的每一幀都會調(diào)用一次所提供的回調(diào)

創(chuàng)建 Ticker 到這兒就完了蹂析。
如何讓Ticker可用呢舔示,也就是如何調(diào)用它的 start 方法呢?

我們在啟動動畫的時候會調(diào)用 AnimationController#forward() 电抚, 在 forward() 方法中就間接的會調(diào)用到 Ticker#start()惕稻, Ticker#start() 最后會調(diào)用到 scheduleFrame() ,而這個方法里面調(diào)用了 ui.window.scheduleFrame(); 蝙叛,這個 scheduleFrame() 是一個 Native 方法俺祠,如下:

void scheduleFrame() native 'Window_scheduleFrame';

上面的方法應(yīng)該就是調(diào)度屏幕刷新的(如有錯誤,請指出借帘,謝謝)蜘渣。

到這里,Ticker 也就 start 了肺然,然后 Ticker 就會在動畫的每一幀去調(diào)一次自己的回調(diào)蔫缸,這個回調(diào)
是在 AnimationController 中的構(gòu)造方法中創(chuàng)建 Ticker 的時候傳入的 _ticker = vsync.createTicker(_tick);

我們來看看 _tick 部分源碼:

void _tick(Duration elapsed) {
    
    ...
    
    notifyListeners();
    _checkStatusChanged();
  }

這里面調(diào)用了 notifyListeners()_checkStatusChanged(),我們先來看看 notifyListeners()际起,部分源碼如下:

void notifyListeners() {
    final List<VoidCallback> localListeners = new List<VoidCallback>.from(_listeners);
    for (VoidCallback listener in localListeners) {
      try {
        if (_listeners.contains(listener))
          // 執(zhí)行回調(diào)
          listener();
      } catch (exception, stack) {
        ...
      }
    }
  }

可以看出拾碌,notifyListeners() 中有一個 VoidCallback 的集合,然后遍歷這個集合加叁,并且執(zhí)行集合中所有的回調(diào)倦沧。

通常我們會使用 .addListener((){...}) 給動畫添加監(jiān)聽,我們在調(diào)用addListener((){...})之后會執(zhí)行如下源碼

void addListener(VoidCallback listener) {
    didRegisterListener();
    _listeners.add(listener);
  }

將我們傳入的回調(diào)方法添加到_listeners集合中它匕,也就是上面 notifyListeners() 方法中遍歷的集合。

這里我們來小結(jié)一下動畫的執(zhí)行流程

  • 創(chuàng)建動畫
    • 創(chuàng)建 Ticker窖认,并 傳入回調(diào)豫柬,回調(diào)里面會執(zhí)行 notifyListeners() 方法告希,此方法中會去遍歷_listeners集合,并執(zhí)行集合中的每一個回調(diào)方法
  • 給動畫添加監(jiān)聽
    • 這里監(jiān)聽時傳入的回調(diào)會被添加到 _listeners 集合中去
  • 啟動動畫
    • 會調(diào)用 ticker.srtart() 來啟動 Ticker烧给,然后 Ticker 在動畫的每一幀都會 執(zhí)行回調(diào)燕偶,也就是我們在第一步中傳入的

接下來就是我們在 addListener((){...}) 中干的事情了,通常會這樣

_controller.addListener(() {
      setState(() {

      });
    });

也就是我們會在傳入的回調(diào)方法中去執(zhí)行 setState((){}) 方法础嫡,我們可以看看文檔:

setState(VoidCallback fn) 部分文檔如下:

Notify the framework that the internal state of this object has changed.

Calling [setState] notifies the framework that the internal state of this
object has changed in a way that might impact the user interface in this
subtree, which causes the framework to schedule a [build] for this [State]
object.

大概意思就是:通知 framework 當(dāng)前對象的內(nèi)部狀態(tài)發(fā)生改變指么,這個操作會導(dǎo)致再次調(diào)用當(dāng)前 State 對象的 build 方法,相當(dāng)于重繪當(dāng)前對象榴鼎。

結(jié)合之前的分析伯诬,整個動畫過程就是,當(dāng)動畫啟動之后巫财,會不停的去執(zhí)行addListener((){...})中的回調(diào)盗似,而我們在監(jiān)聽回調(diào)中又調(diào)用了setState(...)方法,所以這就導(dǎo)致當(dāng)前對象不停的重繪平项,也就出現(xiàn)了屏幕上的動畫的效果赫舒。


回到之前的問題:
使用 addListener(...) 和 AnimatedWidget 為什么會出現(xiàn)那種區(qū)別呢?

其實(shí)我們來看下 AnimatedWidget 的源碼立馬就明白了

abstract class AnimatedWidget extends StatefulWidget {
  const AnimatedWidget({
    Key key,
    @required this.listenable
  }) : assert(listenable != null),
       super(key: key);
       
  final Listenable listenable;


  @protected
  Widget build(BuildContext context);

  @override
  _AnimatedState createState() => new _AnimatedState();
  
  ...
  
}

我們可以看到 AnimatedWidget 繼承自 StatefulWidget闽瓢, 而 StateFulWidget 在初始化的時候會去創(chuàng)建一個 State 對象來管理自身的狀態(tài)接癌,也就是回去執(zhí)行 createState() 方法。

其實(shí)到這里已經(jīng)可以解釋兩種動畫方式存在的區(qū)別扣讼,因?yàn)?AnimatedWidget 內(nèi)部也有一個 State 對象來管理這自身的狀態(tài)扔涧,而我們之前通過查看文檔也知道一個 State 對象只會維護(hù)當(dāng)前對象的狀態(tài),所以即使重繪届谈,也只會導(dǎo)致當(dāng)前 State 對象的重繪枯夜,而不會導(dǎo)致其他 State 對象的重繪。

我們來繼續(xù)看看 AnimatedWidget 中的 createState() 干了什么事艰山,它里面調(diào)用了_AnimatedState() 方法湖雹,部分源碼如下:

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }

...

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }

  @override
  Widget build(BuildContext context) => widget.build(context);

明白了吧!J锇帷摔吏!

_AnimatedStateinitState 中給 widgetlistenable 添加了監(jiān)聽,這里的listenable 就是我們在初始化一個 AnimatedWidget 是傳入的 Animation 對象纵装。

addListener 中傳入的回調(diào)是 _handleChange()征讲, 在 _handleChange() 中同樣調(diào)用了 setState(...)來觸發(fā)當(dāng)前對象重繪。

好了橡娄, 整個過程就是這樣了诗箍。如果有什么錯誤,歡迎大家指出挽唉,謝謝B俗妗?昀恰!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末匠童,一起剝皮案震驚了整個濱河市埂材,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌汤求,老刑警劉巖俏险,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扬绪,居然都是意外死亡竖独,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進(jìn)店門勒奇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來预鬓,“玉大人,你說我怎么就攤上這事赊颠「穸” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵竣蹦,是天一觀的道長顶猜。 經(jīng)常有香客問我,道長痘括,這世上最難降的妖魔是什么长窄? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮纲菌,結(jié)果婚禮上挠日,老公的妹妹穿的比我還像新娘。我一直安慰自己翰舌,他們只是感情好嚣潜,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著椅贱,像睡著了一般懂算。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上庇麦,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天计技,我揣著相機(jī)與錄音,去河邊找鬼山橄。 笑死垮媒,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播涣澡,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼贱呐,長吁一口氣:“原來是場噩夢啊……” “哼丧诺!你這毒婦竟也來了入桂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤驳阎,失蹤者是張志新(化名)和其女友劉穎抗愁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呵晚,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜘腌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了饵隙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撮珠。...
    茶點(diǎn)故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖金矛,靈堂內(nèi)的尸體忽然破棺而出芯急,到底是詐尸還是另有隱情,我是刑警寧澤驶俊,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布娶耍,位于F島的核電站,受9級特大地震影響饼酿,放射性物質(zhì)發(fā)生泄漏榕酒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一故俐、第九天 我趴在偏房一處隱蔽的房頂上張望想鹰。 院中可真熱鬧,春花似錦药版、人聲如沸辑舷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惩妇。三九已至,卻和暖如春筐乳,著一層夾襖步出監(jiān)牢的瞬間歌殃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工蝙云, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留氓皱,地道東北人。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像波材,于是被迫代替她去往敵國和親股淡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評論 2 354

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