Flutter動畫篇

_karl發表於2021-08-21

開篇

動畫在APP的設計中的有著重要的地位,具有目的性和功能性的動畫,不僅僅能夠增添美感的裝飾,更能使使用者獲得良好的體驗。

下面就由淺入深的來介紹一下在Flutter中動畫是如何使用的,以及如何來選擇合適自己需求的動畫,也會帶著原始碼來剖析在Flutter中動畫是如何實現的。

隱式動畫(Implicit Animaition)

介紹

ImplicitlyAnimatedWidget顧名思義是Flutter中用來做隱式動畫的Widget,它是一個抽象類,其有很多子類可以使用,一般都可以用很少的程式碼來做出動畫效果。

由於使用起來很簡單,就不做過多的篇幅去介紹,下面就就說一下它有哪些子類、優缺點以及拿兩個子類來做些範例。

隱式動畫是如何實現的,會在文章後面去剖析。

隱式動畫大家族

從上圖可以看出ImplicitlyAnimatedWidget有11個公有子類和2個私有子類,其中的11個公有子類都可以拿來直接使用來做出動畫,且很容易使用。

優缺點

  • 優點

    • 子類多,也就是動畫多
    • 易使用,易上手
    • 程式碼量少
  • 缺點

    • 過程無法控制
    • 只是簡單的從一個狀態切換到另一個狀態
    • 靈活性差
    • 無法監聽整個動畫過程

例子

下面通過幾個簡單的例子來看一下如何來使用隱式動畫

AnimatedContainer

AnimatedContainer有著跟Container類似的API,只需要在修改完AnimatedContainer的屬性後重新重新整理,Widget就會以動畫的形式從舊值過渡到新值。

相對於普通的Container,AnimatedContainer多出了兩個動畫相關的屬性,durationcurve

duration為必傳引數,即執行動畫的時長。

curve為動畫曲線,後面會專門講解。


AnimatedOpacity

AnimatedOpacity也有著跟Opacity類似的API,也只是在Opacity的原有基礎上增加的一些動畫相關的屬性,duration、curve和onEnd, 當修改opacity值後,重新整理Widget就會在規定時間內,以動畫的形式過渡到新值。

duration為必傳引數,即執行動畫的時長。

curve為動畫曲線,後面會專門講解

onEnd為動畫結束的回撥

隱式動畫的Widget很多都是在普通的Widget的基礎上增加了動畫屬性。

Container --> AnimatedContainer

Opacity --> AnimatedOpacity

Padding --> AnimatedPadding

Align --> AnimatedAlign

... ...

但是此時如果我們想要做一個隱式動畫,但是我們又不知道該使用哪個隱式動畫Widget去做,或者是沒有能滿足場景的隱式動畫Widget,我們應該如何去做呢,這個時候就可以用上隱式動畫中的全能選手TweenAnimationBuilder

隱式動畫中的全能選手:TweenAnimationBuilder

TweenAnimationBuilder可以做到隱式動畫大家族中其他Wigdet所能做到的所有動畫。

構建一個TweenAnimationBuilder需要兩個必要引數:

1、tween: Tween

構建一個Tween需要兩個引數

Tween({ this.begin, this.end })

動畫的過渡值就是在Tween的begin和end之間計算出來的。

假設我們現在需要Widget的寬從50放大到100,就可以構建一個50~100的tween

Tween(begin: 50, end: 100)

構建Tween時begin和end接受的都是泛型引數,因此Tween可以是任何型別的區間,所以Tween可能做到任意兩個值之間的過渡。Flutter也幫我們提供好了很多實現好的Tween,如:ColorTween,SizeTween,BoxConstraintsTween等等,方便我們去使用。

2、builder: Widget Function(BuildContext context, T value, Widget child)

動畫過程中會不斷呼叫的回撥函式,返回需要展示的Widget。

value則是當前動畫時根據tween計算出來的需要展示的值。

顏色動畫

3秒鐘由紅色變為藍色

尺寸動畫

3秒鐘有100變到300

尺寸,顏色,圓角同時做動畫

初始化一個0~1的tween,用Color和BorderRadius的lerp函式能夠計算出當前動畫時間應該得到的值,寬高則是從200漲到300。

總結

隱式動畫使用起來相對簡單,更易上手,Flutter已經幫我們封裝了很多場景下可以直接使用的隱式動畫Widget,如果都不能滿足的話可以使用TweenAnimationBuilder

但是如果碰到複雜的場景和想要更好的去控制動畫,隱式動畫就沒法去勝任了,這個時候就需要用到顯示動畫或者更底層的動畫去做了。

顯式動畫(Explicit Animaition)

介紹

AnimatedWidget是用來做顯示動畫的,它是一個抽象類,它也有很多子類可以幫助開發者來快速完成一些特定場景下的動畫。

AnimatedWidget可以接收一個Listenable屬性,可以監聽動畫的過程。AnimationController繼承自Listenable,如果將一個AnimationController物件傳給AnimatedWidget,便可以控制動畫和監聽動畫的過程了。

顯示動畫大家族

顯示動畫也是有11個公有類和2個私有類,11個公有類中除了AnimatedBuilder,其它的都可以幫助開發者來快速完成一些特定場景下的動畫效果。

SizeTransition可以用來做大小的動畫

ScaleTransition可以用來做縮放的動畫

PositionedTransition可以用來做位置的動畫

... ...

AnimationController

AnimationController是一個動畫控制器,可以起到控制和監聽動畫的作用。

初始化一個AnimationController必傳一個引數是vsync,vsycn是一個TickerProvider型別的引數,它能夠註冊並觸發每一幀的回撥,我們在後面會專門介紹TickerProvider。

如果初始化了一個AnimationController,就必須要手動dispose掉,否則會造成記憶體洩露。

AnimationController控制動畫:

可以控制動畫從什麼值開始播放forward(from:)

反轉動畫reverse(from:)

停止動畫stop()

重置動畫reset()

讓動畫播放到什麼值animateTo(0.7)

等等一系列控制行為。

監聽動畫:

值監聽:

 // 監聽動畫值的變化過程

 // value的取值範圍是AnimationController的lowerBound ~ upperBound

 // 預設取值範圍是0.0 ~ 1.0

animationController.addListener(() {

    print(animationController.value);

});
複製程式碼

狀態監聽

// 用來監聽動畫的狀態。

// status是AnimationStatus型別的一個值,取值範圍有4個

animationController.addStatusListener((status) {

    print(status);

});
複製程式碼

AnimationStatus

AnimationStatus描述
dismissed當controller.value==controller.lowerBound時的狀態,也可以理解為當controller呼叫reverse()來執行動畫,且動畫完成時的狀態
forward當controller呼叫forward()來執行動畫時,在動畫完成前整個動畫過程的狀態
reverse當controller呼叫reverse()來執行動畫時,在動畫完成前整個動畫過程的狀態
completed當controller.value==controller.upperBound時的狀態,也可以理解為當controller呼叫forward()來執行動畫,且動畫完成時的狀態

Curve

Curve我們在說隱式動畫的時候也有用到,它其實就是動畫曲線,就是在執行動畫的過程中,可以讓動畫有線性、加速減速、彈性等動畫曲線效果。

下面兩個示意圖就是展示如果在動畫中使用了curve會是大概是什麼樣的效果。

更多Curve效果:api.flutter.dev/flutter/ani…

例子

上面介紹了一些概念性的東西啊,下面也是用幾個例子來帶大家快速感受一個顯式動畫是如何使用的。

由於初始化一個AnimationController必傳一個TickerProvider型別的vsync引數,Flutter已經幫助我們實現了兩個TickerProvider,分別是SingleTickerProviderStateMixinTickerProviderStateMixin,我們只需要將當前需要做動畫的State去混入就可以直接使用。

SingleTickerProviderStateMixin是在當前State只需要一個AnimationController時使用,如果當前類有多個AnimationController時就需要混入TickerProviderStateMixin了。

RotationTransition

顯式動畫的核心步驟:

  1. 定義一個AnimationController變數
  2. 在initState()初始化animationController,設定動畫時長和vsync。
  3. 將animationController賦值給顯式動畫
  4. 在需要執行動畫的時候執行animationController.forward()即可。
  5. 一定要在dispose()方法裡呼叫animationController.dispose(),否則會造成記憶體洩露。

在上面這個例子中我們用的是RotationTransition,這個顯式動畫Widget是做旋轉的,它有個Animation型別的turns屬性,而AnimationController就是繼承Animation的。

所有的顯式動畫Widget也都有一個Animation型別的屬性,不用的Widget屬性名會不同,但是都可以接收AnimationController。

顯式動畫中的全能選手:AnimatedBuilder

跟隱式動畫一樣,有時候我們不知道有哪些顯式動畫可用,或者已有的顯式動畫不能滿足使用場景,該怎麼去處理呢?在顯式動畫家族裡面也有一個全能選手,那就是AnimatedBuilder

AnimatedBuilder接收兩個必傳引數

Listenable animation;

Widget Function(BuildContext context, Widget child) builder;

animation可以直接將AnimationController賦值給它

builder方法中需要返回動畫過程中需要展示的Widget

基礎用法

程式碼解釋:

在上個例子中,我們在initState()中初始化了controller,並且設定了2秒的動畫時長,還設定了lowerBound為100,upperBound為200。

當我們呼叫controller.forward()後,就會在螢幕重新整理的每一幀中呼叫AnimatedBuilder的builder函式,且controller的value會在2秒鐘的時間內以線性的速度從100增加到200。

所以在builder中將controller.value賦值給width和height就會有放大的動畫效果。

同時做多種動畫

我們希望同時做多種動畫,改變大小,改變顏色,增加圓角,旋轉

程式碼解釋:

在上面的例子中,我們做的是在2秒鐘內,Container尺寸從100漲到200,紅色變成藍色,圓角從0增加到20,並且做了一圈的旋轉。

從程式碼中我們可以看到,我們在State裡面不止定義了一個controller,還定義了另外三個Animation型別的屬性,分別是:

sizeAnimation:用來做尺寸動畫

decorationAnimation:用來做顏色和圓角動畫

rotationAnimation:用來做旋轉動畫

並且在AnimatedBuilder的builder函式裡沒有用到controller的屬性,而是分別用了另外三個animation的value值。這三個animation究竟是什麼?

initState()裡面,我們先初始化了controller,設定了2秒的時長和vsync。

然後在下面分別初始化了另外三個Animation。

sizeAnimation就是一個100~200的Tween,然後用tween呼叫animate方法,並將controller傳入,則就會返回一個Animation物件。

rotationAnimation跟sizeAnimation類似。

decorationAnimation我們可以看到,它是一個DecorationTween,DecorationTween其實就是繼承Tween的一個類,它的begin和end接收Decoration型別。因為我們要做的是顏色和圓角的變化,而BoxDecoration就是繼承Decoration,BoxDecoration裡面又有顏色屬性和圓角屬性。所以這裡就取巧用了DecorationTween,他的begin為color:red,borderRadius:0 end為color:blue,borderRadius:20,就是由紅到藍,圓角由0到20。

那為什麼在builder裡面用animation.value就會有動畫效果呢?

真實的animation其實是_AnimatedEvaluation型別,在_AnimatedEvaluation裡,他會同時持有Tween和AnimationController兩個變數,所以當我們當用animation.value時其實是進行了一些運算的。

呼叫路徑如下圖:

注意?: 上圖的第五步僅針對Tween,如果是Tween的子類,則有些子類會重寫lerp函式,如DecorationTween就是重寫了lerp函式。

所以在隨著動畫過程中controller的value的不斷變化,在AnimatedBuilder的builder方法中呼叫animation的value也都是變化的,所以就產生的動畫效果。

交錯動畫

我們有時希望有多個動畫,但是並不是所有動畫都是一起執行動的,而是有先後順序。尺寸、顏色、圓角、旋轉幾個動畫要依次去執行。

程式碼解釋:

上面的示例我們可以看到,動畫是分了4段。

  1. 大小的變化
  2. 顏色的變化
  3. 圓角的變化
  4. 旋轉

我們從程式碼中可以看到,這段程式碼和上面那個同時做動畫的程式碼有百分之八九十都是一樣的,只是在初始化各個Animation的時候多呼叫了一個chain方法,並且傳入了一個CurveTween。

我們這裡就主要看一下chain和CurveTween是做什麼的,其他的整個動畫步驟跟上面同時做多種動畫是一模一樣的,就不再講解一遍了。

chain方法

chain方法是Animatable的一個方法,顧名思義是連結的意思,就是將多個Animatable連結到一起。Tween就是繼承Animatable。

我們還記得上面在計算animation.value的時候,中間有一個步驟是呼叫tween的transform方法,如果我們將多個tween連結到一塊後生成了animation,然後再去呼叫animation.value時,就會依次呼叫被連結的tween的transform去計算出最終的值。

CurveTween

就是一個可以生成動畫曲線的Tween,可以更好的跟其他Tween結合。

CurveTween({ @required this.curve })

其他的Curve不再多說,上面也有介紹,這裡介紹一個有特殊用法的Curve,它就是Interval。

Interval(this.begin, this.end, { this.curve = Curves.`` linear ``})

Interval的begin到end的取值範圍是0 ~ 1。

它可以用於動畫的延遲。

假如我們有個8秒的動畫,它使用了Interval,並且Interval的值是0.5~1,則這個動畫的前4秒是沒有效果的,後4秒才會開始執行動畫,且最後4秒會完成所有動畫效果。

如果Interval的值是0.25~0.5,則這個動畫是從第2秒開始執行,到第4秒的時候動畫就會執行結束,且第2秒到第4秒這兩秒鐘之間會完成所有動畫效果。

這時我們再去看上面的程式碼就不難理解那4段動畫是如何執行的。

我們總共有個8秒的動畫,將尺寸變化的動畫放在前2秒,將顏色變化的動畫放在2秒到4秒之間,將圓角的動畫放在4秒到6秒之間,將旋轉的動畫放在最後2秒。

總結

顯式動畫的使用比起隱式動畫要稍微複雜一些,但也靈活了許多,AnimatedBuilder應該可以滿足我們所有顯式動畫的場景。

在這一節我們講解了AnimationController的使用,雖然沒有逐個去說它的各個方法如何使用,但是講了一些它的核心方法,有些方法看到方法名也就能清楚是做什麼用的。

我們也介紹了Curve和Interval。

還有組合動畫和交錯動畫的用法和原理。

TickerProvider

TickerProvider是動畫中不可或缺的一個類,它能構建出一個Ticker物件,一個Ticker物件能夠註冊到SchedulerBinding裡,並觸發每一幀的回撥。

而AnimationController的value就是在每一幀的回撥裡去計算出來的。

TickerProvider是一個抽象類,Flutter已經幫我實現了兩個TickerProvider,分別是TickerProviderStateMixin和SingleTickerProviderStateMixin。SingleTickerProviderStateMixin的效能要好於TickerProviderStateMixin。當我們的State裡只用到一個AnimationController的時候,推薦使用SingleTickerProviderStateMixin,而且大部分的動畫場景一個AnimationController就已經能夠勝任了。

接下來我們就剖析一下AnimationController、TickerProvider以及Ticker是如何合作的。

TickerProvider是一個抽象類,它裡面只有一個例項方法createTicker(TickerCallback onTick)

我們來看一下SingleTickerProviderStateMixin是如何實現的。

省略掉一些註釋和debug程式碼,可以看到裡面就是簡單的建立了一個Ticker物件,並將onTick這個回撥傳到ticker物件裡去。

那我們將SingleTickerProviderStateMixin混入到State裡去後,這個createTicker又是哪裡去呼叫的呢?

我們通過前面的學習可以知道,在初始化一個AnimationController的時候需要傳入一個TickerProvider物件,在AnimationController的初始化方法裡可以看到有呼叫createTicker方法,將建立的ticker例項儲存了下來,並且傳入了一個_tick回撥方法。

那麼究竟_ticker這個例項是有什麼用的呢,我們來看一下Ticker這個類裡面究竟是做了什麼。

先看一下Flutter是怎麼介紹Ticker的,簡單的說就是它用於動畫幀的回撥,剛初始化出來預設是不啟動的,呼叫start方法後開始啟動,被SchedulerBinding所驅動。

我們來看看start裡面做了什麼操作,首先判斷自身狀態是否可用,如果可用就會呼叫scheduleTick方法,在scheduleTick方法裡面就會將_tick這個回撥方法傳入到了SchedulerBinding裡面。

而在SchedulerBinding的scheduleFrameCallback方法裡,首先會呼叫一下scheduleFrame,然後將callback封裝到_FrameCallbackEntry例項物件裡面,並將物件存到_transientCallbacks這個回撥集合裡面。

下面圖裡圈出來的就是_transientCallbacks的解釋,通俗點說就是在幀重新整理前,會回撥這個集合裡面的所有回撥函式,用於將應用程式的行為同步到系統的顯示,我的理解就是在頁面重新整理前,將資料準備好,重新整理的時候就會用準備的資料去渲染介面。

呼叫scheduleFrame後會先初始化螢幕幀重新整理的回撥,然後標記螢幕幀重新整理需要呼叫handleBeginFrame

在handleBeginFrame這個方法裡,就會呼叫我們之前Ticker的回撥函式,呼叫完以後就會清掉儲存回撥的集合。

上面呼叫回撥函式相當於就是去呼叫AnimationController的_tick函式

到這裡我們總結一下幾個主要的步驟:

  1. TickerProvider提供了一個建立Ticker物件的方法createTicker
  2. 將TickerProvider傳入到AnimationController的初始化方法裡
  3. 在AnimationController初始化的時候就會呼叫TickerProvider的createTicker來初始化一個Ticker物件,並將一個函式_tick傳入這個Ticker物件裡
  4. Ticker物件在啟動後會將傳進來的函式再次傳入到SchedulerBinding裡的_transientCallbacks這個回撥集合裡。
  5. SchedulerBinding呼叫標記函式,標記螢幕幀重新整理的時候,要呼叫handleBeginFrame函式
  6. 在handleBeginFrame函式裡就會呼叫_transientCallbacks裡面的所有回撥函式
  7. 最終AnimationController的_tick函式會被呼叫,在_tick裡面會計算動畫當前的值和動畫狀態,並通知給監聽者

原始碼解析

我們在這一節將去看一下隱式動畫和顯式動畫的原始碼,來看看他們究竟是怎麼動起來的。

我們就選隱式動畫和顯式動畫裡最全能的那兩個Widget來看,分別是TweenAnimationBuilder和AnimatedBuilder。

TweenAnimationBuilder

再來看下它是怎麼用的,傳入一個Tween,傳入一個時間,傳入一個build回撥。

就會在1秒的時間內不停的呼叫build回撥,而builder裡的value就是從0到1,在1秒內線性增長的值。

TweenAnimationBuilder的繼承結構。

通過上面的學習我們知道,想要做一個動畫離不開螢幕幀重新整理的回撥,Ticker有監聽幀重新整理的能力,TickerPorvider有建立Ticker的能力,AnimationController接受一個TickerProvider。

我們在使用隱式動畫的時候,全程是沒有建立AnimationController的,那它是怎麼工作的?

上面的繼承圖可以看到ImplicitlyAnimatedWidgetState是混入了TickerPorvider的。我們來到ImplicitlyAnimatedWidgetState裡面看一下。

ImplicitlyAnimatedWidgetState建立了AnimationController

AnimatedWidgetBaseState監聽了AnimationController的值的變化,然後值變化後就是不停的呼叫setState來重新整理自身

因為父類呼叫setState,所以_TweenAnimationBuilderState的build會被不停的呼叫,而在build裡面就是計算好tween對應的值,拋給了TweenAnimationBuilder的builder回撥方法。

TweenAnimationBuilder總結:

  1. ImplicitlyAnimatedWidgetState建立AnimationController
  2. AnimatedWidgetBaseState監聽AnimationController值的變化,並呼叫setState。
  3. _TweenAnimationBuilderState重寫了build方法,所有父類呼叫setState,子類的build會被呼叫。
  4. 在build裡面根據AnimationController的值計算出Tween的值,並回撥給widget傳入的builder方法。

AnimatedBuilder

AnimatedBuilder的用法,自己建立和管理AnimationController,將AnimationController傳給AnimatedBuilder,再傳入一個回撥builder。

AnimatedBuilder的程式碼看起來更加簡單

AnimatedBuilder總結:

  1. 手動建立AnimationController傳給AnimatedBuilder
  2. AnimatedBuilder將AnimationController傳給了父類AnimatedWidget,並重寫了父類的build方法
  3. 在AnimatedWidget的State裡面監聽了AnimationController的value的變化,並呼叫setState
  4. 呼叫setState就會呼叫state的build方法,在build方法裡面呼叫了widget的build
  5. 由於AnimatedBuilder重寫了build方法,所以在_AnimatedState裡面呼叫的widget.build()就相當於呼叫的AnimatedBuilder的build方法
  6. AnimatedBuilder的build就是呼叫構建AnimatedBuilder時傳入的builder回撥函式。

總結

TweenAnimationBuilder和AnimatedBuilder的原始碼看起來都不困難,在理解動畫原理後都可以很輕鬆的能知道原始碼是如何工作的,無非就是AnimationController的監聽、value的計算、父類和子類之間的能力配合等。

結尾

在整個動畫篇我們介紹了隱式動畫、顯式動畫、AnimationController、Tween、Curve、組合動畫、TickerProvider、原始碼解析等內容。

整個篇章可以讓我們快速上手Flutter動畫,理解Flutter動畫的原理,輕鬆去應付各種動畫場景。

參考

Flutter動畫相關官方文件:api.flutter-io.cn/flutter/ani…

Curve: api.flutter.dev/flutter/ani…

如何選擇自己想要的動畫:medium.com/flutter/how…

相關文章