《Flutter攻略》之Animation運行原理

首先,如果你還不知道什么是Flutter的話,請看這里,簡單講就是Google自己的React Native。它使用的編程語言是Dart,如果不知道什么是Dart的話請看這里。有人可能會問這兩個東西聽都沒聽過,學了有用嗎?我的答案是“俺也不知道”。
廢話都多說,下面開始學習Flutter的Animation。

Example

我們先來看下最后的運行結果:

基本邏輯就是點擊按鈕,執行一個Animation,然后我們的自定義View就會根據這個Animation不斷變化的值來繪制一個圓形。接下來就看看代碼是怎么實現的?

class _AnimationPainter extends CustomPainter{

  Animation<double> _repaint ;
  _AnimationPainter({
    Animation<double> repaint
  }):_repaint = repaint,super(repaint:repaint);

  @override
  void paint(Canvas canvas, Size size){
    final Paint paint = new Paint()
      ..color = Colors.red[500].withOpacity(0.25)
      ..strokeWidth = 4.0
      ..style = PaintingStyle.stroke;
    canvas.drawCircle(new Point(size.width/2, size.height/2), _repaint.value, paint);
  }

  @override
  bool shouldRepaint(_AnimationPainter oldDelegate){
    return oldDelegate._repaint != _repaint;
  }
}

首先我們先來自定義一個Painter,這個Painter繼承自CustomPainter,構造方法中接收一個Animation對象。在它的paint()方法中使用_repaint.value作為圓形的半徑進行繪圖。

接下來我們再看主布局:

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Flutter Demo')
      ),
      body: new Center(
        child: new CustomPaint(
          key:new GlobalKey(),
          foregroundPainter: new _AnimationPainter(
            repaint:_animation
          )
        )
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _startAnimation,
        tooltip: 'startAnimation',
        child: new Icon(Icons.add)
      )
    );
  }

可以看到我們使用了一個CustomPaint,在Flutter中這算是一個自定義布局。接著給它構造方法中的foregroundPainter參數傳遞一個我們之前定義的_AnimationPainter
下面還定義了一個按鈕,點擊事件為_startAnimation

  void _startAnimation() {
    _animation.forward(from:0.0);
  }

_startAnimation很簡單,就是用來開始一個動畫的。

OK,到這里布局就講完了,這時候我們還缺啥?對了,我們還缺一個Animation對象,下面就定義一個:

  AnimationController _animation = new AnimationController(
    duration: new Duration(seconds: 3),
    lowerBound: 0.0,
    upperBound: 500.0
  );

大家可能會好奇AnimationController又是個啥玩意?它其實是Animation的一個子類,可以用來控制Animation行為。
到這里所有相關代碼都交代完了,編譯運行就能出結果。
完整代碼:

import 'package:flutter/material.dart';

void main() {
  runApp(
    new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue
      ),
      home: new FlutterDemo()
    )
  );
}

class FlutterDemo extends StatefulWidget {
  FlutterDemo({Key key}) : super(key: key);

  @override
  _FlutterDemoState createState() => new _FlutterDemoState();
}

class _FlutterDemoState extends State<FlutterDemo> {


  AnimationController _animation = new AnimationController(
    duration: new Duration(seconds: 3),
    lowerBound: 0.0,
    upperBound: 500.0
  );
  void _startAnimation() {
    _animation.forward(from:0.0);
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Flutter Demo')
      ),
      body: new Center(
        child: new CustomPaint(
          key:new GlobalKey(),
          foregroundPainter: new _AnimationPainter(
            repaint:_animation
          )
        )
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _startAnimation,
        tooltip: 'startAnimation',
        child: new Icon(Icons.add)
      )
    );
  }
}

class _AnimationPainter extends CustomPainter{

  Animation<double> _repaint ;
  _AnimationPainter({
    Animation<double> repaint
  }):_repaint = repaint,super(repaint:repaint);

  @override
  void paint(Canvas canvas, Size size){
    final Paint paint = new Paint()
      ..color = Colors.red[500].withOpacity(0.25)
      ..strokeWidth = 4.0
      ..style = PaintingStyle.stroke;
    canvas.drawCircle(new Point(size.width/2, size.height/2), _repaint.value, paint);
  }

  @override
  bool shouldRepaint(_AnimationPainter oldDelegate){
    return oldDelegate._repaint != _repaint;
  }
}

Animation原理

事物的運動狀態改變是需要外力的,Animation也一樣從靜止到運動(動畫執行階段),肯定有一股外力的存在,那這股力量就是啥?我們來一探究竟。

在Flutter中的一切都是由flutter engine來驅動的,看下圖:

由于flutter engine超出了我們討論范圍,我們這里只是假設它有一個叫做freshScreen()的方法,用于刷新屏幕。對了,這里的freshScreen就是我們Animation最原始的動力,接下來介紹一個叫做
Scheduler的類,如下圖:

它是一個單例,它內部有一個callback列表,用來存儲某些回調,而addFrameCallback方法就是往callback列表添加回調的。那這里的handleBeginFrame又是干么的呢?當flutter engine每調用一次freshScreen的時候,就回去調用Scheduler的handleBeginFrame方法,而在handleBeginFrame中會將callback列表中所有的回調調用一遍。并且會給每個回調傳遞一個當時的時間戳。到這里似乎還沒有Animation什么事,我們繼續往下看。

到這邊我們應該知道系統中有個叫Schedule的類,你只要調用它的addFrameCallback方法,向其中添加回調,那么這個回調就因為界面的刷新而一直被調用。那么又是誰來使用addFrameCallback呢?答案就是Ticker

Ticker是一個類似可控的計時器,調用它的start方法后,內部會調用它的_scheduleTick方法,其中會向Schedule中添加名為_tick的方法回調。而在_tick的方法回調中會調用到_onTick方法。這個_onTick方法是外部傳入的,可自定義其中的內容。并且每一次調用_onTick都會傳入一個時間參數,這個參數表示從調用start開始經歷的時間長度。

那么到這里我們知道了,我們不必直接跟Schedule打交道,有一個更好用的Ticker,我們只要給Ticker傳入一個回調,就能不斷的拿到一個△t,這個△t = now-timeStart。正是這個△t為我們推來了Animation的大門。

OK,到這里我們知道了Animation動畫執行的動力在哪了,接下來看看Animation內部是怎么利用Ticker實現的?

我們回到上面的小例子,拋開自定義的試圖,關鍵的代碼其實就兩行,如下:

AnimationController _animation = new AnimationController(
    duration: new Duration(seconds: 3),
    lowerBound: 0.0,
    upperBound: 500.0
  );

_animation.forward(from:0.0);

分別是初始化一個動畫和開始一個動畫。那就從這兩行入手,看看底下的源碼實現。
先看構造函數:

  AnimationController({
    double value,
    this.duration,
    this.debugLabel,
    this.lowerBound: 0.0,
    this.upperBound: 1.0
  }) {
    assert(upperBound >= lowerBound);
    _direction = _AnimationDirection.forward;
    _ticker = new Ticker(_tick);
    _internalSetValue(value ?? lowerBound);
  }

這里沒什么,只是初始化一些值,如動畫的值得上下邊界,執行時間和目前值。值得注意的是這里初始化了一個Ticker,我們看看Ticker的初始化都做了什么?

Ticker(TickerCallback onTick) : _onTick = onTick;

這里初始化了Ticker的_onTick方法,相當于傳入了一個回調用于Ticker來和Animation_controller交互。具體先不看傳入的這個_tick方法是啥,后面真正遇到了在做分析。

接著就是forward方法了,它的作用就是讓Animation的值從當前值變化到最大值。若不設置參數,即默認為下限值。

  Future<Null> forward({ double from }) {
    _direction = _AnimationDirection.forward;
    if (from != null)
      value = from;
    return animateTo(upperBound);
  }

OK,這是一個異步的方法,重點在animateTo這個方法,繼續:

Future<Null> animateTo(double target, { Duration duration, Curve curve: Curves.linear }) {
    Duration simulationDuration = duration;
    if (simulationDuration == null) {
      assert(this.duration != null);
      double range = upperBound - lowerBound;
      double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
      simulationDuration = this.duration * remainingFraction;
    }
    stop();
    if (simulationDuration == Duration.ZERO) {
      assert(value == target);
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
      _checkStatusChanged();
      return new Future<Null>.value();
    }
    assert(simulationDuration > Duration.ZERO);
    assert(!isAnimating);
    return _startSimulation(new _InterpolationSimulation(_value, target, simulationDuration, curve));
  }

雖然這里代碼有些多,但大部分是一些條件判斷和預處理,這里不需要理會,我們看關鍵的:

_startSimulation(new _InterpolationSimulation(_value, target, simulationDuration, curve));

這里引入了一個新的概念Simulation,它其實相當于一個時間t和坐標x及速度v的關系,它有一個x(t)方法,接受一個參數t及時間,返回t時間時x的值。在Animation中就是t時刻的值。而這個t就是Ticker產生的,我們接著往下看:

  Future<Null> _startSimulation(Simulation simulation) {
    assert(simulation != null);
    assert(!isAnimating);
    _simulation = simulation;
    _lastElapsedDuration = Duration.ZERO;
    _value = simulation.x(0.0).clamp(lowerBound, upperBound);
    Future<Null> result = _ticker.start();
    _status = (_direction == _AnimationDirection.forward) ?
      AnimationStatus.forward :
      AnimationStatus.reverse;
    _checkStatusChanged();
    return result;
  }

這里也是做了一些值得初始化,關鍵的一句是:

 _ticker.start();

這里啟動了Ticker,我們看看start里面又做了什么?

  Future<Null> start() {
    assert(!isTicking);
    assert(_startTime == null);
    _completer = new Completer<Null>();
    _scheduleTick();
    if (SchedulerBinding.instance.isProducingFrame)
      _startTime = SchedulerBinding.instance.currentFrameTimeStamp;
    return _completer.future;
  }

上面我們主要關注 _scheduleTick()這個方法,

  void _scheduleTick({ bool rescheduling: false }) {
    assert(isTicking);
    assert(_animationId == null);
    _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
  }

到這里小伙伴們應該已經看出來了,這里Ticker將自己的_tick方法回調注冊到了Scheduler中,一旦注冊完,隨著屏幕的刷新,_tick將會被不停的調用,

  void _tick(Duration timeStamp) {
    assert(isTicking);
    assert(_animationId != null);
    _animationId = null;

    if (_startTime == null)
      _startTime = timeStamp;

    _onTick(timeStamp - _startTime);

    // The onTick callback may have scheduled another tick already.
    if (isTicking && _animationId == null)
      _scheduleTick(rescheduling: true);
  }

_tick方法中主要做了對時間的處理,它會將從開始到當前的時間間隔傳給_onTick這個方法,而這個方法就是之前AnimationController傳遞進來的。及下面的_tick方法:

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

這里應該看的很明白了,AnimationController的_tick會被不停的調用,而AnimationController的值則是由_simulation來根據時間計算得來。接著再調用notifyListeners()和 _checkStatusChanged()通知監聽AnimatinController的對象。
Animation的運行原理差不多就是這些,有疑問或對此感興的可以在我主頁中找到我的微信二維碼,加我好友,我們慢慢聊。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容

  • 如果想讓事情變得順利,只有靠自己--夏爾·紀堯姆 上一章介紹了隱式動畫的概念。隱式動畫是在iOS平臺創建動態用戶界...
    夜空下最亮的亮點閱讀 1,973評論 0 1
  • >生活和藝術一樣,最美的永遠是曲線。--愛德華布爾沃-利頓 在第九章“圖層時間”中,我們討論了動畫時間和`CAMe...
    夜空下最亮的亮點閱讀 490評論 0 0
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,197評論 30 471
  • 1. 前言 作為Android程序員,或者是想要去模仿一些酷炫的效果,或者是為了實現視覺的變態需求,或者是壓抑不住...
    SparkInLee閱讀 19,862評論 7 53
  • 01 我如果可以討你的開心 我愿成為一朵花 讓你摘去我所有花瓣 只留下我注視你的目光 然后雨水蓄滿我的眼眶 三分之...
    守素閱讀 390評論 8 18