Flutter框架分析(五)-- 動畫

ad6623發表於2019-05-12

Flutter框架分析分析系列文章:

《Flutter框架分析(一)-- 總覽和Window》

《Flutter框架分析(二)-- 初始化》

《Flutter框架分析(三)-- Widget,Element和RenderObject》

《Flutter框架分析(四)-- Flutter框架的執行》

《Flutter框架分析(五)-- 動畫》

《Flutter框架分析(六)-- 佈局》

《Flutter框架分析(七)-- 繪製》

前言

前四篇文章介紹了Flutter框架的全貌,相信大家對Flutter框架有了個整體的瞭解。這一系列文章始終是圍繞著渲染流水線的的執行的各個階段加以說明。我們知道在Vsync訊號到來以後首先執行的是動畫(Animate)階段。而這個階段是在從engine回撥windowonBeginFrame函式開始執行的。那麼這篇文章我們就來介紹一下Flutter框架的動畫基本原理。

例子

所謂動畫其實就是一系列連續變化的圖片在極短的時間逐幀顯示,在人眼看來就是動畫了。這裡我們舉一個簡單的例子先說明一下在Flutter中怎麼執行一個動畫:

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();
  }
}

複製程式碼

這個動畫是在手機螢幕上由小到大漸變的顯示一個Flutter標誌。從上述程式碼中我們可以看到在Flutter中實現一個動畫要做這麼幾件事。

  1. 首先施加動畫的Widget是個StatefulWidget。其State要混入(mixin) SingleTickerProviderStateMixin
  2. initState()裡要加入和動畫相關的初始化,這裡我們例項化了兩個類AnimationControllerAnimation。例項化AnimationController的時候我們傳入了兩個引數,一個是動畫的時長,另一個是State自己,這裡其實是利用到了混入的SingleTickerProviderStateMixin。例項化另一個Animation的時候,我們首先例項化的是一個Tween。這個類其實代表了從最小值到最大值的一個線性變化。所以例項化的時候要傳入開始和結束值。然後呼叫animate()並傳入之前的controller。這個呼叫會返回我們需要的Animation例項。顯然我們需要知道動畫的屬性變化的時候的訊息,所以這裡會通過..addListener()Animation例項註冊回撥。這個回撥只做一件事,那就是呼叫setState()來更新UI。最後就是呼叫controller.forward()來啟動動畫。
  3. 注意在build()函式裡我們構建widget的時候用到了animation.value。所以這裡的鏈條就是動畫在收到回撥後會呼叫setState(),而從我們上篇文章知道setState之後在渲染流水線的構建階段會走到build()來重建Widget。重建的時候就用到了發生變化以後的animation.value。這個一幀一幀的迴圈,我們的動畫就動起來了。
  4. 最後在dispose()的時候要記得呼叫controller.dispose()釋放資源。

接下來我們就深入Flutter原始碼來看一下動畫是如何執行的。

分析

首先我們來看一下混入到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();
  }
}
複製程式碼

這個混入其實就做了一件事,實現createTicker()來例項化一個Ticker類。在另一個函式didChangeDependencies()裡,有這樣一行程式碼_ticker.muted = !TickerMode.of(context);。這行程式碼的意思是在這個帶有動畫的State的在element tree中的依賴發生變化的時候是否mute自己的_ticker。一個場景就是當前頁的動畫還在播放的時候,使用者導航到另外一個頁面,當前頁的動畫就沒有必要再播放了,反之在頁面切換回來的時候動畫有可能還要繼續播放,控制的地方就在這裡,注意TickerMode.of(context)這種方式,我們在Flutter框架中很多地方都會見到,基本上就是從element tree的祖先裡找到對應那個InheritedWidget的方式。

Ticker顧名思義,就是給動畫提供vsync訊號的吧。我們來看下原始碼一探究竟。

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主要在做的有點像控制一個計時器,有start()stop()mute。還記錄當前自己的狀態isTicking。我們需要關注的的是scheduleTick()這個函式:

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

你看,這裡就跑到了我們之前文章說的SchedulerBinding裡面去了。這裡排程的時候會傳入Ticker的回撥函式_tick

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

在排程一幀的時候Ticker的回撥函式_tick被加入了transientCallbacks。從之前對渲染流水線的分析,我們知道transientCallbacks會在vsync訊號到來以後windowonBeginFrame回撥裡被執行一次。也就是說此時就進入到渲染流水線的動畫Animate階段了。

接著我們就看下Ticker的回撥函式_tick做了什麼:

  void _tick(Duration timeStamp) {
    _animationId = null;

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

    if (shouldScheduleTick)
      scheduleTick(rescheduling: true);
  }
複製程式碼

這裡的_onTick是在例項化Ticker時候傳入的。_onTick被呼叫之後,Ticker如果發現自己的任務還沒有完成,還要接著跳動,那就再來排程新一幀。所以你看動畫的動力其實還是來自vsync訊號的。

那麼這個_onTick又是啥樣的呢?這個函式是在例項化Ticker的時候傳入的。而從上述分析我們又知道,Ticker的例項化是在呼叫TickerProvider.createTicker()的時候完成的。誰來呼叫這個函式呢?是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);
  }
複製程式碼

可見在其建構函式裡就呼叫createTicker()了,傳入的引數是_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();
  }
複製程式碼

這個回撥裡做這幾件事,根據vsync到來以後的時間戳來計算更新一下新的值,這裡計算用的是個_simulation。為啥叫這名?因為這是用來模擬一個物體在外力作用下在不同的時間點的運動狀態的變化,這也算是動畫的本質吧。

算出來新的值以後就呼叫notifyListeners()來通知各位觀察者。還記的在開始的例子裡我們例項化animation以後會通過..addListener()新增的回撥嗎?在這裡這個回撥就會被呼叫,也就是setState()會被呼叫了。接下來就是渲染流水線的構建(build)階段了。

看到這裡你可能會有疑問,事情都讓AnimationController做了,那那個例子裡的Tween是用來幹啥的?

AnimationController的建構函式裡我們可以看出來,它只管[0.0, 1.0]之間的模擬,也就是說不管動畫怎麼動,它任何時候只輸出0.0到1.0之間的值,但是我們的動畫有旋轉角度,顏色漸變,圖形變化以及更復雜的組合,顯然我們得想辦法把0.0到1.0之間的值轉換為我們需要的角度,位置,顏色,透明度等等,這個轉化就是由各種Animation來完成的,像例子裡說的Tween,它的任務在動畫期間把值從0漸變到300。怎麼做呢?在例項化Tween以後我們會呼叫animate(),傳入AnimationController例項。

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

複製程式碼

你看,入參是個Animation<double>,這裡也就是AnimationController。出參則是個Animation<T>。這樣就完成了從[0.0, 1.0]到任意型別的變化。

具體怎麼變呢?這個變化其實是在用到這個值得時候發生的,上面的例子裡在State.build()函式裡構造widget的時候會呼叫到animation.value這個getter。這其實呼叫的是_AnimatedEvaluation.value

 @override
  T get value => _evaluatable.evaluate(parent);
複製程式碼

_evaluatable就是Tween了,parent就是AnimationController了。所以呢,這個轉換是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);
  }
複製程式碼

看到範圍限制了嗎?真正的轉換又是在lerp()裡完成的。

@protected
  T lerp(double t) {
    return begin + (end - begin) * t;
  }
複製程式碼

很簡單的線性插值。

所裡你要理解Flutter中的Tween動畫是幹什麼的只要把握住它在自己的transform()函式中做了什麼事情就知道了,從上可知Tween其實就是在做線性插值的動畫而已。Tween是線性插值的,那如果我想搞非線性插值的動畫呢?那就用CurvedAnimation。Flutter裡有一大票各種各樣的線性插值動畫和非線性插值的動畫,你甚至可以自己定義自己的非線性動畫,只要重寫變換函式就行了:

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

好了,關於Flutter框架裡的動畫就先分析到這裡。

總結

本篇文章是Flutter框架分析系列文章的第五篇,本系列文章主要是以Flutter的渲染流水線為線索來分析其執行的。本篇主要針對的是渲染流水線的動畫階段,從底層的角度對Flutter的動畫機制做了一個簡要的分析。期望大家對Flutter的動畫有一個基礎的認識。在此之上的各種眼花繚亂的動畫相關widgets都是在此基礎上衍生出來的,所謂道生一,一生二,二生三,三生萬物。掌握了道,就不會被萬物所迷惑。

相關文章