Flutter動畫原理

  • 概述

    作為前端開發(fā)技術(shù),動畫是一門前端語言所必須的,在Flutter中的動畫是如何使用的呢?它的設(shè)計(jì)原理又是什么呢?本文就從源碼的角度來分析一下Flutter動畫。

  • 從使用開始

    當(dāng)然,我們還是從使用API的入口開始,看一下動畫機(jī)制是怎么驅(qū)動的。下面是一個demo:

    //需要繼承TickerProvider,如果有多個AnimationController,則應(yīng)該使用TickerProviderStateMixin。
    class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>
        with SingleTickerProviderStateMixin {
      late Animation<double> animation;
      late AnimationController controller;
    
      initState() {
        super.initState();
        //step1.
        controller = AnimationController(
          duration: const Duration(seconds: 2),
          vsync: this,
        );
    
        //勻速
        //圖片寬高從0變到300
        //step2.
        final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut);
        //animation = Tween(begin: 0.0, end: 300.0).animate(controller)
        //Tween可以持有AnimationController也可以持有CurvedAnimation,他們都是Animation類型
        //animation = Tween(begin: 0.0, end: 300.0).animate(controller)
        animation = Tween(begin: 0.0, end: 300.0).animate(curve)
          ..addListener(() {
            setState(() => {});
          });
    
        //啟動動畫(正向執(zhí)行)
        //step3.
        controller.forward();
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Image.asset(
            "imgs/avatar.png",
            //Image的寬高被動畫的更新值驅(qū)動
            width: animation.value,
            height: animation.value,
          ),
        );
      }
    
      dispose() {
        //路由銷毀時需要釋放動畫資源
        controller.dispose();
        super.dispose();
      }
    }
    

    首先我們需要一個AnimationController,不管是什么類型的動畫一定得有這個,它的作用就是控制動畫的開始停止等(原理是被vsync屏幕刷新信號驅(qū)動來控制動畫值的更新,后面會分析到)。

    這里給出了兩個必須的參數(shù),一個是duration,表示動畫時長,這個值如果不傳會在執(zhí)行的時候報(bào)錯,另一個參數(shù)vsync是TickerProvider,它被required修飾,所以必傳,這里傳入的是this,這個this指向的并不是State而是SingleTickerProviderStateMixin,SingleTickerProviderStateMixin繼承自TickerProvider。

    step2部分的Tween其實(shí)你可以選擇去掉,但是需要在AnimationController構(gòu)造時指定lowerBound和upperBound來確定范圍,同時,你要用AnimationController調(diào)用addListener方法,并在回調(diào)中調(diào)用setState方法,否則動畫不會引起圖片大小的變化。

    Curve是用來描述速度變化快慢的,他最終想要驅(qū)動動畫值變化還是要調(diào)用AnimationController,它只不過是AnimationController的進(jìn)一步封裝,相當(dāng)于包裝模式;同理,在這個demo 中,Tween也是完成了對于Curve的封裝調(diào)用。

    最后執(zhí)行forward方法來開啟動畫。

    總之,在上面的這個demo中,AnimationController和其必須的構(gòu)造參數(shù)對象是必須要有的,中間的Tween就是指定圖片大小的上下限取值,可以用AnimationController全權(quán)受理。

    下面我們針對上面用到的這些類來逐個研究。

  • AnimationController

    上面我們說到,AnimationController是用來管理動畫的啟動停止等動作的,并且是通過vsync信號驅(qū)動的,我們從源碼角度來驗(yàn)證它。

    它的構(gòu)造方法中有這樣一段代碼:

    _ticker = vsync.createTicker(_tick);
    _internalSetValue(value ?? lowerBound);
    

    _internalSetValue方法邏輯比較簡單,我們先看 _internalSetValue方法:

    void _internalSetValue(double newValue) {
      _value = newValue.clamp(lowerBound, upperBound);
      if (_value == lowerBound) {
        _status = AnimationStatus.dismissed;
      } else if (_value == upperBound) {
        _status = AnimationStatus.completed;
      } else {
        _status = (_direction == _AnimationDirection.forward) ?
          AnimationStatus.forward :
          AnimationStatus.reverse;
      }
    }
    

    _value 是動畫即時值,未指定時是null,如果它小于lowerBound,clamp方法會把它置為lowerBound,同樣,大于upperBound會把它置為upperBound,這里會根據(jù)是它是等于lowerBound還是等于upperBound而給 _status賦值為AnimationStatus.dismissed或AnimationStatus.completed,前者表示在起點(diǎn),后者表示在終點(diǎn);如果是在規(guī)定區(qū)間內(nèi)的值則會根據(jù) _direction是否為 _AnimationDirection.forward來決定 _status是AnimationStatus.forward還是AnimationStatus.reverse。

    總之,_internalSetValue的作用就是初始化 _value和 _status。

    回過頭來,我們看到_ticker在這里是調(diào)用的vsync的createTicker方法創(chuàng)建的,假如我們State依賴的mixin是SingleTickerProviderStateMixin,我們?nèi)タ纯此倪@個方法:

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

    我只接截取了關(guān)鍵代碼,assert等容錯邏輯沒截取。

    可以看到,在這里創(chuàng)建了一個Ticker對象,他持有了一個函數(shù)對象onTick,這個onTick就是AnimationController構(gòu)造方法里傳入的 _tick函數(shù):

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

    可以看到這里會通過 _simulation產(chǎn)生新的 _value值,所以猜測,這里可能是屏幕刷新后會回調(diào)的地方,但是怎么驗(yàn)證呢?我們?nèi)フ艺宜窃趺春推聊凰⑿聶C(jī)制關(guān)聯(lián)上的。

    _simulation是什么呢?我們發(fā)現(xiàn)他沒有初始化值,又在什么時候賦值的呢?因?yàn)槠聊凰⑿乱恢痹谶M(jìn)行,動畫的回調(diào)想要被它影響一定會在某個時間點(diǎn)建立關(guān)聯(lián),那在什么時候呢?開啟動畫的時候就是最恰當(dāng)?shù)臅r候,所以我們直接從動畫開啟方法之一的forward方法找起:

    TickerFuture forward({ double? from }) {
      _direction = _AnimationDirection.forward;
      if (from != null)
        value = from;
      return _animateToInternal(upperBound);
    }
    

    可見它內(nèi)部調(diào)用了_animateToInternal方法,這個方法就是開啟動畫的方法,很明顯它是一個受保護(hù)的內(nèi)部方法,調(diào)用它的有四處,也是供外部調(diào)用的四個開啟方法:forward、reverse、animateTo、animateBack。

    _animateToInternal方法內(nèi)部主要是做一些對于當(dāng)前動畫的一些停止和對即將開始的新動畫的初始化工作,關(guān)鍵是它最后調(diào)用了 _startSimulation方法:

    return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));
    
    TickerFuture _startSimulation(Simulation simulation) {
      _simulation = simulation;
      _lastElapsedDuration = Duration.zero;
      _value = simulation.x(0.0).clamp(lowerBound, upperBound);
      final TickerFuture result = _ticker!.start();
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.forward :
        AnimationStatus.reverse;
      _checkStatusChanged();
      return result;
    }
    

    可以發(fā)現(xiàn),_startSimulation的代碼和 _tick中的代碼好相似啊,仔細(xì)看會發(fā)現(xiàn), _startSimulation方法中simulation的x方法的傳參固定是0.0,因?yàn)檫@是動畫開啟;又發(fā)現(xiàn)這里沒有調(diào)用stop方法,因?yàn)閯赢媱傞_啟不需要結(jié)束,結(jié)束的判斷就是要交給上面的 _tick方法。

    重要的是我們在這里找到了 _ticker的開啟方法:

    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!;
    }
    

    在這里會創(chuàng)建TickerFuture,isActive方法中會根據(jù) _future來判斷動畫是否正在運(yùn)行。然后調(diào)用scheduleTick方法:

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

    看到這里我們需要先明白一個原理,就是<font color=red>Flutter 應(yīng)用在啟動時都會綁定一個SchedulerBinding,通過SchedulerBinding可以給每一次屏幕刷新添加回調(diào),而Ticker就是通過SchedulerBinding來添加屏幕刷新回調(diào)的,這樣一來,每次屏幕刷新都會調(diào)用TickerCallback。使用Ticker(而不是Timer)來驅(qū)動動畫會防止屏幕外動畫(動畫的UI不在當(dāng)前屏幕時,如鎖屏?xí)r)消耗不必要的資源,因?yàn)镕lutter中屏幕刷新時會通知到綁定的SchedulerBinding,而Ticker是受SchedulerBinding驅(qū)動的,由于鎖屏后屏幕會停止刷新,所以Ticker就不會再觸發(fā)。</font>

    所以這里調(diào)用了SchedulerBinding.instance的scheduleFrameCallback方法來和 _tick回調(diào)函數(shù)建立關(guān)聯(lián):

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

    我們在handleBeginFrame方法中找到了使用 _transientCallbacks的地方:

    void handleBeginFrame(Duration? rawTimeStamp) {
          ...
        callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
          if (!_removedIds.contains(id))
            _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);
        });
          ...
    }
    

    這個方法會確保注冊在window上:

    @protected
    void ensureFrameCallbacksRegistered() {
      window.onBeginFrame ??= _handleBeginFrame;
      window.onDrawFrame ??= _handleDrawFrame;
    }
    

    到這里,我們就找出了動畫回調(diào)函數(shù)和屏幕刷新回調(diào)之間的關(guān)聯(lián)邏輯。

    而在Ticker的stop方法中我們會找到回調(diào)移除的邏輯:

    void stop({ bool canceled = false }) {
      if (!isActive)
        return;
    
      // We take the _future into a local variable so that isTicking is false
      // when we actually complete the future (isTicking uses _future to
      // determine its state).
      final TickerFuture localFuture = _future!;
      _future = null;
      _startTime = null;
      assert(!isActive);
      //移除動畫回調(diào)
      unscheduleTick();
      if (canceled) {
        localFuture._cancel(this);
      } else {
        localFuture._complete();
      }
    }
    
  • Simulation

    在上面的分析中有一個Simulation類,它有三個方法:

    /// The position of the object in the simulation at the given time.
    double x(double time);
    
    /// The velocity of the object in the simulation at the given time.
    double dx(double time);
    
    /// Whether the simulation is "done" at the given time.
    bool isDone(double time);
    

    根據(jù)注釋可知,可以通過x方法、dx方法、isDone方法分別可以得出當(dāng)前動畫的進(jìn)度、速度和是否已完成的標(biāo)志。我們上面?zhèn)魅氲氖撬膶?shí)現(xiàn)類 _InterpolationSimulation,它內(nèi)部持有的屬性有開始值、終點(diǎn)值和執(zhí)行時長,執(zhí)行時長保存在 _durationInSeconds屬性:

    _durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond;
    

    可以看到,這里會將設(shè)置的市場按照scale進(jìn)行壓縮,并最終取微秒單位,因?yàn)閯赢嬕笕庋劭床怀隹D,所以這里使用最細(xì)致的微秒單位。

    我們先看看它的x方法:

    @override
    double x(double timeInSeconds) {
      final double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0);
      if (t == 0.0)
        return _begin;
      else if (t == 1.0)
        return _end;
      else
        return _begin + (_end - _begin) * _curve.transform(t);
    }
    

    可見,這里會在初始值的基礎(chǔ)上加上需要前進(jìn)的值,正常的t是自上次屏幕刷新后消逝的時間,這里調(diào)用 _curve的transform方法也正是Curve的原理所在,它對進(jìn)度作了進(jìn)一步的處理。

    @override
    double dx(double timeInSeconds) {
      final double epsilon = tolerance.time;
      return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon);
    }
    

    同樣,dx方法中是關(guān)于速度的算法。

    isDone最簡單,只是通過是否超出_durationInSeconds來判斷動畫是否結(jié)束:

    @override
    bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
    

    <font color=red>記住這個Simulation所做的事情,在下面對于Curve和Tween的原理分析中,你會看到似曾相識的邏輯</font>。

  • Curve

    在上面對于Simulation的分析中我們提到,在x方法獲取最新進(jìn)度值的時候會通過Curve的transform方法處理一下,而這一點(diǎn)也正是在動畫機(jī)制中Curve發(fā)揮作用的關(guān)鍵。

    transform方法最終在其父類ParametricCurve中會調(diào)用transformInternal方法,這個方法理應(yīng)由子類實(shí)現(xiàn),Curve的子類有很多,代表著很多中變化曲線算法,這里就不展開講了,Curve的原理在上文中其實(shí)已經(jīng)講完了。

  • Tween

    Tween是用來設(shè)置屬性范圍的,可能有人會講,屬性范圍我完全可以通過AnimationController的lowerBound和upperBound來指定,為什么還需要這個多余的類呢?

    把屬性的類型不設(shè)限于double,你就能體會到他應(yīng)該有的作用了。

    class Tween<T extends Object?> extends Animatable<T> {
      Tween({
        this.begin,
        this.end,
      });
      ...
    }
    

    可見,Tween持有了一個泛型,表示可以設(shè)置任何屬性,通過Tween,我們動畫最終產(chǎn)生的值就不再只局限于double了,我可以轉(zhuǎn)成任何直接使用的屬性值。

    上文我們知道,AnimationController會在vsync的驅(qū)動下執(zhí)行動畫回調(diào)獲取動畫最新值,那么在使用那部分我們看到,我們引用的直接是Tween的animate方法返回的Animation對象的value值,那么這個value值和AnimationController即時獲取的最新值是怎么關(guān)聯(lián)的呢?

    我們先看一下Tween的animate方法返回的是什么:

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

    可以看到,是一個_AnimatedEvaluation實(shí)例,它的parent指定為AnimationController,我們來看一下它的value:

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

    value通過 _evaluatable的evaluate方法獲取, _evaluatable就是前面?zhèn)魅氲膖his,也就是Tween本身:

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

    可見evaluate又調(diào)用了transform方法:

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

    transform方法中如果是中間值的話又會調(diào)用lerp方法:

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

    怎么樣,熟悉吧,是不是和_InterpolationSimulation中的x方法如出一轍,這就是默認(rèn)的進(jìn)度處理邏輯,不同的是,這里的begin、end和返回值都是泛型指定的,而不是固定的double,t為double是因?yàn)閠是刷新的微秒進(jìn)度值,一定是double。

    你可以定義自己的Tween類,繼承并重寫lerp方法即可完成自定義的屬性值轉(zhuǎn)換,比如ColorTween:

    @override
    Color? lerp(double t) => Color.lerp(begin, end, t);
    

    Color的lerp方法如下:

    static Color? lerp(Color? a, Color? b, double t) {
      assert(t != null);
      if (b == null) {
        if (a == null) {
          return null;
        } else {
          return _scaleAlpha(a, 1.0 - t);
        }
      } else {
        if (a == null) {
          return _scaleAlpha(b, t);
        } else {
          return Color.fromARGB(
            _clampInt(_lerpInt(a.alpha, b.alpha, t).toInt(), 0, 255),
            _clampInt(_lerpInt(a.red, b.red, t).toInt(), 0, 255),
            _clampInt(_lerpInt(a.green, b.green, t).toInt(), 0, 255),
            _clampInt(_lerpInt(a.blue, b.blue, t).toInt(), 0, 255),
          );
        }
      }
    }
    

    這就是Tween的原理,可以知道,Tween只不過是內(nèi)部持有了AnimationController,然后通過AnimationController拿到進(jìn)度值然后做自己的處理之后返回給value使用。

    Tween的parent換做持有CurvedAnimation也是一樣的原理:

    @override
    double get value {
      final Curve? activeCurve = _useForwardCurve ? curve : reverseCurve;
      final double t = parent.value;
      if (activeCurve == null)
        return t;
      if (t == 0.0 || t == 1.0) {
        return t;
      }
      return activeCurve.transform(t);
    }
    

    可見,只不過CurvedAnimation持有的是Curve,然后通過Curve去調(diào)用AnimationController而已。

  • TickerProviderStateMixin

    前面講了SingleTickerProviderStateMixin,其實(shí)TickerProvider還有一個子類就是TickerProviderStateMixin:

    @override
    Ticker createTicker(TickerCallback onTick) {
      _tickers ??= <_WidgetTicker>{};
      final _WidgetTicker result = _WidgetTicker(onTick, this, debugLabel: 'created by $this');
      _tickers!.add(result);
      return result;
    }
    
    void _removeTicker(_WidgetTicker ticker) {
      assert(_tickers != null);
      assert(_tickers!.contains(ticker));
      _tickers!.remove(ticker);
    }
    

    可見,這個mixin是用來出來State中有多個AnimationController的情況,主要是用于Ticker的釋放清理工作。

  • 總結(jié)

    Flutter的動畫原理的核心主要在于AnimationController和Scheduler關(guān)聯(lián),通過給Scheduler添加回調(diào)函數(shù)的方式和系統(tǒng)的vsync信號建立關(guān)聯(lián),從而由屏幕的刷新信號驅(qū)動到動畫回調(diào)函數(shù),在回調(diào)函數(shù)中調(diào)用setState方法更新界面,而界面控件中又引用了動畫類的value,value是通過直接或者間接的方式從AnimationController中獲取的最新當(dāng)前進(jìn)度值,這就完成了整個動畫的流程。

    可以通過CurvedAnimation、Tween等包裝類(當(dāng)然也可以自定義)使用包裝的設(shè)計(jì)模式去包裝AnimationController,使得在使用獲取的進(jìn)度值之前可以對其有更靈活的處理。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Flutter的動畫體系是怎么運(yùn)作的,各組件之間的關(guān)聯(lián)關(guān)系及原理什么,隱式動畫、顯式動畫怎么區(qū)分,本文將會進(jìn)行詳細(xì)...
    whqfor閱讀 2,353評論 0 6
  • 概述 動畫API認(rèn)識 動畫案例練習(xí) 其它動畫補(bǔ)充 一、動畫API認(rèn)識 動畫實(shí)際上是我們通過某些方式(某種對象,An...
    IIronMan閱讀 470評論 1 3
  • 對于一個前端的App來說,添加適當(dāng)?shù)膭赢?,可以給用戶更好的體驗(yàn)和視覺效果。所以無論是原生的iOS或Android,...
    5e4c664cb3ba閱讀 1,709評論 0 7
  • 動畫實(shí)現(xiàn)的方式 Flutter中,我們可以簡單的把調(diào)用this.setState()理解為渲染一幀。那么只要我們不...
    shawn_yy閱讀 1,937評論 0 6
  • 在任何系統(tǒng)的UI框架中,動畫實(shí)現(xiàn)的原理都是相同的:在一段時間內(nèi),快速地多次改變UI外觀;由于人眼會產(chǎn)生視覺暫留,所...
    zombie閱讀 215評論 0 0

友情鏈接更多精彩內(nèi)容