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

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

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

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

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

《Flutter框架分析(四)-- Flutter框架的運行》佈局

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

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

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

前言

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

例子

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

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都是在此基礎上衍生出來的,所謂道生一,一輩子二,二生三,三生萬物。掌握了道,就不會被萬物所迷惑。

相關文章
相關標籤/搜索