Flutter實(shí)戰(zhàn) Flutter動(dòng)畫簡(jiǎn)介

2021-03-08 18:01 更新

在任何系統(tǒng)的 UI 框架中,動(dòng)畫實(shí)現(xiàn)的原理都是相同的,即:在一段時(shí)間內(nèi),快速地多次改變 UI 外觀;由于人眼會(huì)產(chǎn)生視覺暫留,所以最終看到的就是一個(gè)“連續(xù)”的動(dòng)畫,這和電影的原理是一樣的。我們將 UI 的一次改變稱為一個(gè)動(dòng)畫幀,對(duì)應(yīng)一次屏幕刷新,而決定動(dòng)畫流暢度的一個(gè)重要指標(biāo)就是幀率FPS(Frame Per Second),即每秒的動(dòng)畫幀數(shù)。很明顯,幀率越高則動(dòng)畫就會(huì)越流暢!一般情況下,對(duì)于人眼來(lái)說(shuō),動(dòng)畫幀率超過 16FPS,就比較流暢了,超過 32FPS 就會(huì)非常的細(xì)膩平滑,而超過 32FPS,人眼基本上就感受不到差別了。由于動(dòng)畫的每一幀都是要改變 UI 輸出,所以在一個(gè)時(shí)間段內(nèi)連續(xù)的改變 UI 輸出是比較耗資源的,對(duì)設(shè)備的軟硬件系統(tǒng)要求都較高,所以在 UI 系統(tǒng)中,動(dòng)畫的平均幀率是重要的性能指標(biāo),而在 Flutter 中,理想情況下是可以實(shí)現(xiàn) 60FPS 的,這和原生應(yīng)用能達(dá)到的幀率是基本是持平的。

#Flutter中動(dòng)畫抽象

為了方便開發(fā)者創(chuàng)建動(dòng)畫,不同的 UI 系統(tǒng)對(duì)動(dòng)畫都進(jìn)行了一些抽象,比如在 Android 中可以通過 XML 來(lái)描述一個(gè)動(dòng)畫然后設(shè)置給 View。Flutter 中也對(duì)動(dòng)畫進(jìn)行了抽象,主要涉及 Animation、Curve、Controller、Tween 這四個(gè)角色,它們一起配合來(lái)完成一個(gè)完整動(dòng)畫,下面我們一一來(lái)介紹它們。

#Animation

Animation是一個(gè)抽象類,它本身和UI渲染沒有任何關(guān)系,而它主要的功能是保存動(dòng)畫的插值和狀態(tài);其中一個(gè)比較常用的Animation類是Animation<double>。Animation對(duì)象是一個(gè)在一段時(shí)間內(nèi)依次生成一個(gè)區(qū)間(Tween)之間值的類。Animation對(duì)象在整個(gè)動(dòng)畫執(zhí)行過程中輸出的值可以是線性的、曲線的、一個(gè)步進(jìn)函數(shù)或者任何其他曲線函數(shù)等等,這由Curve來(lái)決定。 根據(jù)Animation對(duì)象的控制方式,動(dòng)畫可以正向運(yùn)行(從起始狀態(tài)開始,到終止?fàn)顟B(tài)結(jié)束),也可以反向運(yùn)行,甚至可以在中間切換方向。Animation還可以生成除double之外的其他類型值,如:Animation<Color>Animation<Size>。在動(dòng)畫的每一幀中,我們可以通過Animation對(duì)象的value屬性獲取動(dòng)畫的當(dāng)前狀態(tài)值。

#動(dòng)畫通知

我們可以通過Animation來(lái)監(jiān)聽動(dòng)畫每一幀以及執(zhí)行狀態(tài)的變化,Animation有如下兩個(gè)方法:

  1. addListener();它可以用于給Animation添加幀監(jiān)聽器,在每一幀都會(huì)被調(diào)用。幀監(jiān)聽器中最常見的行為是改變狀態(tài)后調(diào)用setState()來(lái)觸發(fā) UI 重建。
  2. addStatusListener();它可以給Animation添加“動(dòng)畫狀態(tài)改變”監(jiān)聽器;動(dòng)畫開始、結(jié)束、正向或反向(見AnimationStatus定義)時(shí)會(huì)調(diào)用狀態(tài)改變的監(jiān)聽器。

讀者在此只需要知道幀監(jiān)聽器和狀態(tài)監(jiān)聽器的區(qū)別,在后面的章節(jié)中我們將會(huì)舉例說(shuō)明。

#Curve

動(dòng)畫過程可以是勻速的、勻加速的或者先加速后減速等。Flutter 中通過Curve(曲線)來(lái)描述動(dòng)畫過程,我們把勻速動(dòng)畫稱為線性的(Curves.linear),而非勻速動(dòng)畫稱為非線性的。

我們可以通過CurvedAnimation來(lái)指定動(dòng)畫的曲線,如:

final CurvedAnimation curve =
    new CurvedAnimation(parent: controller, curve: Curves.easeIn);

CurvedAnimationAnimationController(下面介紹)都是Animation<double>類型。CurvedAnimation可以通過包裝AnimationControllerCurve生成一個(gè)新的動(dòng)畫對(duì)象 ,我們正是通過這種方式來(lái)將動(dòng)畫和動(dòng)畫執(zhí)行的曲線關(guān)聯(lián)起來(lái)的。我們指定動(dòng)畫的曲線為Curves.easeIn,它表示動(dòng)畫開始時(shí)比較慢,結(jié)束時(shí)比較快。 Curves (opens new window)類是一個(gè)預(yù)置的枚舉類,定義了許多常用的曲線,下面列幾種常用的:

Curves曲線 動(dòng)畫過程
linear 勻速的
decelerate 勻減速
ease 開始加速,后面減速
easeIn 開始慢,后面快
easeOut 開始快,后面慢
easeInOut 開始慢,然后加速,最后再減速

除了上面列舉的, Curves (opens new window)類中還定義了許多其它的曲線,在此便不一一介紹,讀者可以自行查看 Curves 類定義。

當(dāng)然我們也可以創(chuàng)建自己 Curve,例如我們定義一個(gè)正弦曲線:

class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2);
  }
}

#AnimationController

AnimationController用于控制動(dòng)畫,它包含動(dòng)畫的啟動(dòng)forward()、停止stop() 、反向播放 reverse()等方法。AnimationController會(huì)在動(dòng)畫的每一幀,就會(huì)生成一個(gè)新的值。默認(rèn)情況下,AnimationController在給定的時(shí)間段內(nèi)線性的生成從0.0到1.0(默認(rèn)區(qū)間)的數(shù)字。 例如,下面代碼創(chuàng)建一個(gè)Animation對(duì)象(但不會(huì)啟動(dòng)動(dòng)畫):

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 2000), vsync: this);

AnimationController生成數(shù)字的區(qū)間可以通過lowerBoundupperBound來(lái)指定,如:

final AnimationController controller = new AnimationController( 
 duration: const Duration(milliseconds: 2000), 
 lowerBound: 10.0,
 upperBound: 20.0,
 vsync: this
);

AnimationController派生自Animation<double>,因此可以在需要Animation對(duì)象的任何地方使用。 但是,AnimationController具有控制動(dòng)畫的其他方法,例如forward()方法可以啟動(dòng)正向動(dòng)畫,reverse()可以啟動(dòng)反向動(dòng)畫。在動(dòng)畫開始執(zhí)行后開始生成動(dòng)畫幀,屏幕每刷新一次就是一個(gè)動(dòng)畫幀,在動(dòng)畫的每一幀,會(huì)隨著根據(jù)動(dòng)畫的曲線來(lái)生成當(dāng)前的動(dòng)畫值(Animation.value),然后根據(jù)當(dāng)前的動(dòng)畫值去構(gòu)建 UI,當(dāng)所有動(dòng)畫幀依次觸發(fā)時(shí),動(dòng)畫值會(huì)依次改變,所以構(gòu)建的 UI 也會(huì)依次變化,所以最終我們可以看到一個(gè)完成的動(dòng)畫。 另外在動(dòng)畫的每一幀,Animation對(duì)象會(huì)調(diào)用其幀監(jiān)聽器,等動(dòng)畫狀態(tài)發(fā)生改變時(shí)(如動(dòng)畫結(jié)束)會(huì)調(diào)用狀態(tài)改變監(jiān)聽器。

duration表示動(dòng)畫執(zhí)行的時(shí)長(zhǎng),通過它我們可以控制動(dòng)畫的速度。

注意: 在某些情況下,動(dòng)畫值可能會(huì)超出AnimationController的[0.0,1.0]的范圍,這取決于具體的曲線。例如,fling()函數(shù)可以根據(jù)我們手指滑動(dòng)(甩出)的速度(velocity)、力量(force)等來(lái)模擬一個(gè)手指甩出動(dòng)畫,因此它的動(dòng)畫值可以在[0.0,1.0]范圍之外 。也就是說(shuō),根據(jù)選擇的曲線,CurvedAnimation的輸出可以具有比輸入更大的范圍。例如,Curves.elasticIn等彈性曲線會(huì)生成大于或小于默認(rèn)范圍的值。

#Ticker

當(dāng)創(chuàng)建一個(gè)AnimationController時(shí),需要傳遞一個(gè)vsync參數(shù),它接收一個(gè)TickerProvider類型的對(duì)象,它的主要職責(zé)是創(chuàng)建Ticker,定義如下:

abstract class TickerProvider {
  //通過一個(gè)回調(diào)創(chuàng)建一個(gè)Ticker
  Ticker createTicker(TickerCallback onTick);
}

Flutter 應(yīng)用在啟動(dòng)時(shí)都會(huì)綁定一個(gè)SchedulerBinding,通過SchedulerBinding可以給每一次屏幕刷新添加回調(diào),而Ticker就是通過SchedulerBinding來(lái)添加屏幕刷新回調(diào),這樣一來(lái),每次屏幕刷新都會(huì)調(diào)用TickerCallback。使用Ticker(而不是Timer)來(lái)驅(qū)動(dòng)動(dòng)畫會(huì)防止屏幕外動(dòng)畫(動(dòng)畫的UI不在當(dāng)前屏幕時(shí),如鎖屏?xí)r)消耗不必要的資源,因?yàn)?Flutter 中屏幕刷新時(shí)會(huì)通知到綁定的SchedulerBinding,而Ticker是受SchedulerBinding驅(qū)動(dòng)的,由于鎖屏后屏幕會(huì)停止刷新,所以Ticker就不會(huì)再觸發(fā)。

通常我們會(huì)將SingleTickerProviderStateMixin添加到State的定義中,然后將 State 對(duì)象作為vsync的值,這在后面的例子中可以見到。

#Tween

默認(rèn)情況下,AnimationController對(duì)象值的范圍是[0.0,1.0]。如果我們需要構(gòu)建 UI 的動(dòng)畫值在不同的范圍或不同的數(shù)據(jù)類型,則可以使用Tween來(lái)添加映射以生成不同的范圍或數(shù)據(jù)類型的值。例如,像下面示例,Tween生成[-200.0,0.0]的值:

final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);

Tween構(gòu)造函數(shù)需要beginend兩個(gè)參數(shù)。Tween的唯一職責(zé)就是定義從輸入范圍到輸出范圍的映射。輸入范圍通常為[0.0,1.0],但這不是必須的,我們可以自定義需要的范圍。

Tween繼承自Animatable<T>,而不是繼承自Animation<T>,Animatable中主要定義動(dòng)畫值的映射規(guī)則。

下面我們看一個(gè) ColorTween 將動(dòng)畫輸入范圍映射為兩種顏色值之間過渡輸出的例子:

final Tween colorTween =
    new ColorTween(begin: Colors.transparent, end: Colors.black54);

Tween對(duì)象不存儲(chǔ)任何狀態(tài),相反,它提供了evaluate(Animation<double> animation)方法,它可以獲取動(dòng)畫當(dāng)前映射值。 Animation對(duì)象的當(dāng)前值可以通過value()方法取到。evaluate函數(shù)還執(zhí)行一些其它處理,例如分別確保在動(dòng)畫值為0.0和1.0時(shí)返回開始和結(jié)束狀態(tài)。

#Tween.animate

要使用 Tween 對(duì)象,需要調(diào)用其animate()方法,然后傳入一個(gè)控制器對(duì)象。例如,以下代碼在500毫秒內(nèi)生成從0到255的整數(shù)值。

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);

注意animate()返回的是一個(gè)Animation,而不是一個(gè)Animatable

以下示例構(gòu)建了一個(gè)控制器、一條曲線和一個(gè) Tween:

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
    new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)