首頁 > 軟體

詳解Android如何實現自定義的動畫曲線

2022-04-11 10:01:50

前言

最近在寫動畫相關的篇章,經常會用到 Curve 這個動畫曲線類,那這個類到底怎麼實現的?如果想自己來一個自定義的動畫曲線該怎麼弄?本篇我們就來一探究竟。

曲線

Curve 類定義

檢視原始碼, Curve 類定義如下:

abstract class Curve extends ParametricCurve<double> {
  const Curve();

  @override
  double transform(double t) {
    if (t == 0.0 || t == 1.0) {
      return t;
    }
    return super.transform(t);
  }
  
  Curve get flipped => FlippedCurve(this);
}

看上去好像沒定義什麼, 實際這裡只是做了兩個處理,一個是明確的資料型別為 double,另一個是對 transform 做了過載,也只是對引數 t 做了特殊處理,保證引數 t 的範圍在0-1之間,且起點值0.0和終點值1.0不被轉換函數轉換。主要定義在上一層的ParametricCurve。檔案是建議子類過載transformInternal方法,那我們就繼續往上看ParametricCurve這個類的實現,程式碼如下:

abstract class ParametricCurve<T> {
  const ParametricCurve();

  T transform(double t) {
    assert(t != null);
    assert(t >= 0.0 && t <= 1.0, 'parametric value $t is outside of [0, 1] range.');
    return transformInternal(t);
  }

  @protected
  T transformInternal(double t) {
    throw UnimplementedError();
  }

  @override
  String toString() => objectRuntimeType(this, 'ParametricCurve');
}

可以看到,實際上 transform 方法除了做引數合法性驗證以外,其實就是呼叫了transformInternal方法,因此子類必須要實現該方法,否則會丟擲UnimplementedError異常。

範例解析

上面的原始碼可以看到,關鍵在於引數 t。這個引數 t 代表什麼呢?註釋裡說的是:

Returns the value of the curve at point t. — 返回 t 點的曲線對應的值。

因此 t 可以認為是曲線的橫座標,而為了保證曲線的一致性,做了歸一化處理,也就是t的取值都是在0-1之間。這麼說可能有點抽象,我們來看2個例子來對比就明白了,先看最簡單 Curves.linear 的實現。

class _Linear extends Curve {
  const _Linear._();

  @override
  double transformInternal(double t) => t;
}

超級簡單吧,直接返回 t,其實對應我們的數學的函數就是:

y = f(t) = t

對應的曲線就是一條斜線。也就是說在設定的動畫時間內,會完成從0-1的線性轉變,也就是變化是均勻的。線性這個很好理解,我們再來看一個減速曲線decelerate的實現。

class _DecelerateCurve extends Curve {
  const _DecelerateCurve._();

  @override
  double transformInternal(double t) {
    t = 1.0 - t;
    return 1.0 - t * t;
  }
}

我們先看一下_DecelerateCurve 的計算表示式是什麼。

回憶一下我們高中物理學的勻減速運動,加速度為負(即減速)的距離計算公式:

上面的減速曲線其實就可以看做是初始速度是2,加速度也是2的減速運動。為什麼要是2這個值呢,這是因為 t 的取值範圍是0-1,這樣計算完的結果的取值範圍還是0-1。你肯定會問,為什麼要保證曲線的計算結果要是0-1?我們來假設計算結果不為0-1會發生什麼情況,比如我們要在螢幕上移動一個元件為60畫素。假設動畫曲線初始值不為0。那就意味著一開始的移動距離是跳變的。同樣的,如果結束值不為1.0,意味著在最後一個點的距離值不是60.0,那麼就意味著結束時需要從最後一個點跳到最終的60畫素的位置(動畫需要保證最終的移動距離是60畫素)這樣意味著動畫會出現跳變的效果,繪製曲線的話會是下面的樣子(綠色是正常的,紅線是異常的)。這樣的動畫體驗是很糟糕的!因此,這是一個關鍵點,如果你的自定義曲線的 transformInternal 方法的返回值範圍不是0-1,就意味著動畫會出現跳變,導致動畫缺幀的感覺。

image.png

有了這個基礎,我們就可以解釋動畫曲線的基本機制了,實際上就是在給定的動畫時間(Duration)範圍內,完成元件的初始狀態到結束狀態的轉變,這個轉變是沿著設定的 Curve 類完成的,而其橫座標是0-1.0,曲線的初始值和結束值分別是0和1.0,而至於中間值是可以低於0或超過1的。我們可以想像是我們沿著設定的曲線運動,最終無論如何都會達到設定的目的地,而至於怎麼走,拐多少道彎,速度怎麼變化都是曲線控制的。但是,如果你的曲線初始值不為0或結束值不為1,就像是跳懸崖的那種感覺!

正弦動畫曲線

我們來一個正弦曲線的動畫驗證一下上面的說法。

class SineCurve extends Curve {
  final int count;
  const SineCurve({this.count = 1}) : assert(count > 0);

  @override
  double transformInternal(double t) {
    return sin(2 * count* pi * t);
  }
}

count 引數用於控制週期,即達到目的地之前可以多來幾個來回。這裡我們發現,初始值是0,但是一個週期(2π)結束值也是0,這樣在動畫結束前會出現跳變的結果。來看一下範例程式碼,這個範例是讓圓形向下移動60畫素。

AnimatedContainer(
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(30.0),
  ),
  transform: Matrix4.identity()..translate(0.0, up ? 60.0 : 0.0, 0.0),
  duration: Duration(milliseconds: 3000),
  curve: SineCurve(count: 1),
  child: ClipOval(
    child: Container(
      width: 60.0,
      height: 60.0,
      color: Colors.blue,
    ),
  ),
)

執行效果如下,注意看最後一幀從0的位置直接跳到了60的位置。

跳動動畫

這個怎麼調呢,我們來看一下正弦曲線的樣子。

正弦曲線

如果我們要滿足0-1範圍的要求,那麼要往後再移動90度才能夠達到。但是,這樣還有個問題,這樣破壞了週期性,比如設定 count=2的時候結果又不對了。我們來看一下規律,實際上只有第一個週期需要多移動90度(圖中箭頭指向的點),後面的都是按360度(即2π)為週期了。也就是角度其實是按2.5π,4.5π,6.5π……規律來的,對應的角度公式其實就是:

所以調整後的正弦曲線程式碼為:

class SineCurve extends Curve {
  final int count;
  const SineCurve({this.count = 1}) : assert(count > 0);

  @override
  double transformInternal(double t) {
    // 需要補償pi/2個角度,使得起始值是0.終止值是1,避免出現最後突然回到0
    return sin(2 * (count + 0.25) * pi * t);
  }
}

再看調整後的效果,是不是絲滑般地過渡了?

總結

本篇介紹了 Flutter 動畫曲線類的原理和控制動畫的機制,實際上 Curve 類就是在指定的時間內,沿曲線完成從起點到終點的過渡。但是為了保證動畫平滑過渡,應該保證自定義曲線的transformInternal方法返回值的起始值和結束值分別是0和1。

到此這篇關於詳解Android如何實現自定義的動畫曲線的文章就介紹到這了,更多相關Android動畫曲線內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com