HenCoder Android 自定義 View 1-6: 屬性動畫(上手篇)

扔物線發表於2019-03-03

這期是 HenCoder 自定義繪製的第 1-6 期:屬性動畫(上手篇)

如果你沒聽說過 HenCoder,可以先看看這個:
HenCoder:給高階 Android 工程師的進階手冊

簡介

前幾期釋出後,經常在回覆裡看到有人問我什麼時候講動畫。本來我是不打算講動畫的,因為動畫其實不算是自定義 View 的內容。但後來考慮了一下,動畫在自定義 View 的開發中也起著很重要的作用,有的時候你對動畫的瞭解不夠,就難以實現一些自定義 View 的效果。

於是決定:加兩期,講動畫!

不過並不是所有的動畫都講,我要講的是屬性動畫。 Android 裡動畫是有一些分類的:動畫可以分為兩類:Animation 和 Transition;其中 Animation 又可以再分為 View Animation 和 Property Animation 兩類: View Animation 是純粹基於 framework 的繪製轉變,比較簡單,如果你有興趣的話可以上網搜一下它的用法;Property Animation,屬性動畫,這是在 Android 3.0 開始引入的新的動畫形式,不過說它新只是相對的,它已經有好幾年的歷史了,而且現在的專案中的動畫 99% 都是用的它,極少再用到 View Animation 了。屬性動畫不僅可以使用自帶的 API 來實現最常用的動畫,而且通過自定義 View 的方式來做出定製化的動畫。除了這兩種 Animation,還有一類動畫是 Transition。 Transition 這個詞的本意是轉換,在 Android 裡指的是切換介面時的動畫效果,這個在邏輯上要複雜一點,不過它的重點是在於切換而不是動畫,所以它也不是這次要討論的內容。這次的內容只專注於一點:

Property Animation(屬性動畫)。在這一期我就基於前面幾期講過的自定義繪製,這一個自定義 View 的分支,來說一下屬性動畫的原理以及使用。

講解

複雜的東西用文字很難講清楚,所以每次遇到難講的內容我都會選擇上視訊,這期也不例外。

話說做視訊太費精力和時間了,這期的視訊竟然做了兩週。以後一定要控制住自己,少做視訊,不然怕會掉頭髮。

(如果你是手機看的,可以點這裡去 B 站看視訊)

下面是講義部分。強烈建議你看完上面的視訊再看下面的內容,不然可能會遭遇理解障礙。

ViewPropertyAnimator

使用方式:View.animate() 後跟 translationX() 等方法,動畫會自動執行。

view.animate().translationX(500);複製程式碼

具體可以跟的方法以及方法所對應的 View 中的實際操作的方法如下圖所示:

從圖中可以看到, View 的每個方法都對應了 ViewPropertyAnimator 的兩個方法,其中一個是帶有 -By 字尾的,例如,View.setTranslationX() 對應了 ViewPropertyAnimator.translationX()ViewPropertyAnimator.translationXBy() 這兩個方法。其中帶有 -By() 字尾的是增量版本的方法,例如,translationX(100) 表示用動畫把 ViewtranslationX 值漸變為 100,而 translationXBy(100) 則表示用動畫把 ViewtranslationX 值漸變地增加 100。l

這些方法的效果都簡單易懂,而且視訊裡也有簡單的演示,所以就不放示例圖了。如果你想看,可以去下面的練習專案。(最好順便也練一下程式碼)

ObjectAnimator

使用方式:

  1. 如果是自定義控制元件,需要新增 setter / getter 方法;
  2. ObjectAnimator.ofXXX() 建立 ObjectAnimator 物件;
  3. start() 方法執行動畫。
public class SportsView extends View {

    float progress = 0;

    ......

    // 建立 getter 方法
    public float getProgress() {
        return progress;
    }

    // 建立 setter 方法
    public void setProgress(float progress) {
        this.progress = progress;
        invalidate();
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        ......

        canvas.drawArc(arcRectF, 135, progress * 2.7f, false, paint);

        ......
    }
}

......

// 建立 ObjectAnimator 物件
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "progress", 0, 65);
// 執行動畫
animator.start();複製程式碼

通用功能

1. setDuration(int duration) 設定動畫時長

單位是毫秒。

// imageView1: 500 毫秒
imageView1.animate()
        .translationX(500)
        .setDuration(500);

// imageView2: 2 秒
ObjectAnimator animator = ObjectAnimator.ofFloat(
        imageView2, "translationX", 500);
animator.setDuration(2000);
animator.start();複製程式碼

2. setInterpolator(Interpolator interpolator) 設定 Interpolator

視訊裡已經說了, Interpolator 其實就是速度設定器。你在引數裡填入不同的 Interpolator ,動畫就會以不同的速度模型來執行。

// imageView1: 線性 Interpolator,勻速
imageView1.animate()
        .translationX(500)
        .setInterpolator(new LinearInterpolator());

// imageView: 帶施法前搖和回彈的 Interpolator
ObjectAnimator animator = ObjectAnimator.ofFloat(
        imageView2, "translationX", 500);
animator.setInterpolator(new AnticipateOvershootInterpolator());
animator.start();複製程式碼

簡單介紹一下每一個 Interpolator

AccelerateDecelerateInterpolator

先加速再減速。這是預設的 Interpolator,也就是說如果你不設定的話,那麼動畫將會使用這個 Interpolator

視訊裡已經說過了,這個是一種最符合現實中物體運動的 Interpolator,它的動畫效果看起來就像是物體從速度為 0 開始逐漸加速,然後再逐漸減速直到 0 的運動。它的速度 / 時間曲線以及動畫完成度 / 時間曲線都是一條正弦 / 餘弦曲線(這句話看完就忘掉就行,沒用)。具體的效果如下:

好像不太看得出來加速減速過程?你就將就著看吧,畢竟 gif 不是視訊,要啥自行車啊。

用途:就像上面說的,它是一種最符合物理世界的模型,所以如果你要做的是最簡單的狀態變化(位移、放縮、旋轉等等),那麼一般不用設定 Interpolator,就用這個預設的最好。

LinearInterpolator

勻速。

勻速就不用解釋了吧?直接上效果:

AccelerateInterpolator

持續加速。

在整個動畫過程中,一直在加速,直到動畫結束的一瞬間,直接停止。

別看見它加速驟停就覺得這是個神經病模型哦,它很有用的。它主要用在離場效果中,比如某個物體從介面中飛離,就可以用這種效果。它給人的感覺就會是「這貨從零起步,加速飛走了」。到了最後動畫驟停的時候,物體已經飛出使用者視野,看不到了,所以他們是並不會察覺到這個驟停的。

DecelerateInterpolator

持續減速直到 0。

動畫開始的時候是最高速度,然後在動畫過程中逐漸減速,直到動畫結束的時候恰好減速到 0。

它的效果和上面這個 AccelerateInterpolator 相反,適用場景也和它相反:它主要用於入場效果,比如某個物體從介面的外部飛入介面後停在某處。它給人的感覺會是「咦飛進來個東西,讓我仔細看看,哦原來是 XXX」。

AnticipateInterpolator

先回拉一下再進行正常動畫軌跡。效果看起來有點像投擲物體或跳躍等動作前的蓄力。

如果是圖中這樣的平移動畫,那麼就是位置上的回拉;如果是放大動畫,那麼就是先縮小一下再放大;其他型別的動畫同理。

這個 Interpolator 就有點耍花樣了。沒有通用的適用場景,根據具體需求和設計師的偏好而定。

OvershootInterpolator

動畫會超過目標值一些,然後再彈回來。效果看起來有點像你一屁股坐在沙發上後又被彈起來一點的感覺。

AnticipateInterpolator 一樣,這是個耍花樣的 Interpolator,沒有通用的適用場景。

AnticipateOvershootInterpolator

上面這兩個的結合版:開始前回拉,最後超過一些然後回彈。

依然耍花樣,不多解釋。

BounceInterpolator

在目標值處彈跳。有點像玻璃球掉在地板上的效果。

耍花樣 +1。

CycleInterpolator

這個也是一個正弦 / 餘弦曲線,不過它和 AccelerateDecelerateInterpolator 的區別是,它可以自定義曲線的週期,所以動畫可以不到終點就結束,也可以到達終點後回彈,回彈的次數由曲線的週期決定,曲線的週期由 CycleInterpolator() 構造方法的引數決定。

引數為 0.5f:

引數為 2f:

PathInterpolator

自定義動畫完成度 / 時間完成度曲線。

用這個 Interpolator 你可以定製出任何你想要的速度模型。定製的方式是使用一個 Path 物件來繪製出你要的動畫完成度 / 時間完成度曲線。例如:

Path interpolatorPath = new Path();

...

// 勻速
interpolatorPath.lineTo(1, 1);複製程式碼

Path interpolatorPath = new Path();

...

// 先以「動畫完成度 : 時間完成度 = 1 : 1」的速度勻速執行 25%
interpolatorPath.lineTo(0.25f, 0.25f);
// 然後瞬間跳躍到 150% 的動畫完成度
interpolatorPath.moveTo(0.25f, 1.5f);
// 再勻速倒車,返回到目標點
interpolatorPath.lineTo(1, 1);複製程式碼

你根據需求,繪製出自己需要的 Path,就能定製出你要的速度模型。

不過要注意,這條 Path 描述的其實是一個 y = f(x) (0 ≤ x ≤ 1) (y 為動畫完成度,x 為時間完成度)的曲線,所以同一段時間完成度上不能有兩段不同的動畫完成度(這個好理解吧?因為內容不能出現分身術呀),而且每一個時間完成度的點上都必須要有對應的動畫完成度(因為內容不能在某段時間段內消失呀)。所以,下面這樣的 Path 是非法的,會導致程式 FC:

出現重複的動畫完成度,即動畫內容出現「分身」——程式 FC

有一段時間完成度沒有對應的動畫完成度,即動畫出現「中斷」——程式 FC

除了上面的這些,Android 5.0 (API 21)引入了三個新的 Interpolator 模型,並把它們加入了 support v4 包中。這三個新的 Interpolator 每個都和之前的某個已有的 Interpolator 規則相似,只有略微的區別。

FastOutLinearInInterpolator

加速運動。

這個 Interpolator 的作用你不能看它的名字,一會兒 fast 一會兒 linear 的,完全看不懂。其實它和 AccelerateInterpolator 一樣,都是一個持續加速的運動路線。只不過 FastOutLinearInInterpolator 的曲線公式是用的貝塞爾曲線,而 AccelerateInterpolator 用的是指數曲線。具體來說,它倆最主要的區別是 FastOutLinearInInterpolator 的初始階段加速度比 AccelerateInterpolator 要快一些。

FastOutLinearInInterpolator

AccelerateInterpolator

能看出它倆的區別嗎?

能看出來就怪了。這倆的速度模型幾乎就是一樣的,不信我把它們的動畫完成度 / 時間完成度曲線放在一起給你看:

看到了嗎?兩條線幾乎是一致的,只是紅線比綠線更早地到達了較高的斜率,這說明在初始階段,FastOutLinearInInterpolator 的加速度比 AccelerateInterpolator 更高。

那麼這意味著什麼呢?

意味個毛。實際上,這點區別,在實際應用中使用者根本察覺不出來。而且,AccelerateInterpolator 還可以在構造方法中調節變速係數,分分鐘調節到和 FastOutLinearInInterpolator (幾乎)一模一樣。所以你在使用加速模型的時候,這兩個選哪個都一樣,沒區別的。

那麼既然都一樣,我做這麼多對比,講這麼些幹什麼呢?

因為我得讓你瞭解。它倆雖然「用起來沒區別」,但這是基於我對它足夠了解所做出的判斷,可我如果直接甩給你一句「它倆沒區別,想用誰用誰,少廢話別問那麼多」,你心裡肯定會有一大堆疑問,在開發時用到它們的時候也會畏畏縮縮心裡打鼓的,對吧?

FastOutSlowInInterpolator

先加速再減速。

同樣也是先加速再減速的還有前面說過的 AccelerateDecelerateInterpolator,不過它們的效果是明顯不一樣的。FastOutSlowInInterpolator 用的是貝塞爾曲線,AccelerateDecelerateInterpolator 用的是正弦 / 餘弦曲線。具體來講, FastOutSlowInInterpolator 的前期加速度要快得多

FastOutSlowInInterpolator

AccelerateDecelerateInterpolator

不論是從動圖還是從曲線都可以看出,這二者比起來,FastOutSlowInInterpolator 的前期加速更猛一些,後期的減速過程的也減得更迅速。用更直觀一點的表達就是,AccelerateDecelerateInterpolator 像是物體的自我移動,而 FastOutSlowInInterpolator 則看起來像有一股強大的外力「推」著它加速,在接近目標值之後又「拽」著它減速。總之,FastOutSlowInterpolator 看起來有一點「著急」的感覺。

二者曲線對比圖:

LinearOutSlowInInterpolator

持續減速。

它和 DecelerateInterpolator 比起來,同為減速曲線,主要區別在於 LinearOutSlowInInterpolator 的初始速度更高。對於人眼的實際感覺,區別其實也不大,不過還是能看出來一些的。

LinearOutSlowInInterpolator

DecelerateInterpolator

二者曲線對比:

對於所有 Interpolator 的介紹就到這裡。這些 Interpolator,有的較為常用且有通用的使用場景,有的需要你自己來根據情況而定。把它們瞭解清楚了,對於製作出觀感舒服的動畫很有好處。

3. 設定監聽器

給動畫設定監聽器,可以在關鍵時刻得到反饋,從而及時做出合適的操作,例如在動畫的屬性更新時同步更新其他資料,或者在動畫結束後回收資源等。

設定監聽器的方法, ViewPropertyAnimatorObjectAnimator 略微不一樣: ViewPropertyAnimator 用的是 setListener()setUpdateListener() 方法,可以設定一個監聽器,要移除監聽器時通過 set[Update]Listener(null) 填 null 值來移除;而 ObjectAnimator 則是用 addListener()addUpdateListener() 來新增一個或多個監聽器,移除監聽器則是通過 remove[Update]Listener() 來指定移除物件。

另外,由於 ObjectAnimator 支援使用 pause() 方法暫停,所以它還多了一個 addPauseListener() / removePauseListener() 的支援;而 ViewPropertyAnimator 則獨有 withStartAction()withEndAction() 方法,可以設定一次性的動畫開始或結束的監聽。

3.1 ViewPropertyAnimator.setListener() / ObjectAnimator.addListener()

這兩個方法的名稱不一樣,可以設定的監聽器數量也不一樣,但它們的引數型別都是 AnimatorListener,所以本質上其實都是一樣的。 AnimatorListener 共有 4 個回撥方法:

3.1.1 onAnimationStart(Animator animation)

當動畫開始執行時,這個方法被呼叫。

3.1.2 onAnimationEnd(Animator animation)

當動畫結束時,這個方法被呼叫。

3.1.3 onAnimationCancel(Animator animation)

當動畫被通過 cancel() 方法取消時,這個方法被呼叫。

需要說明一下的是,就算動畫被取消,onAnimationEnd() 也會被呼叫。所以當動畫被取消時,如果設定了 AnimatorListener,那麼 onAnimationCancel()onAnimationEnd() 都會被呼叫。onAnimationCancel() 會先於 onAnimationEnd() 被呼叫。

3.1.4 onAnimationRepeat(Animator animation)

當動畫通過 setRepeatMode() / setRepeatCount()repeat() 方法重複執行時,這個方法被呼叫。

由於 ViewPropertyAnimator 不支援重複,所以這個方法對 ViewPropertyAnimator 相當於無效。

3.2 ViewPropertyAnimator.setUpdateListener() / ObjectAnimator.addUpdateListener()

和上面 3.1 的兩個方法一樣,這兩個方法雖然名稱和可設定的監聽器數量不一樣,但本質其實都一樣的,它們的引數都是 AnimatorUpdateListener。它只有一個回撥方法:onAnimationUpdate(ValueAnimator animation)

3.2.1 onAnimationUpdate(ValueAnimator animation)

當動畫的屬性更新時(不嚴謹的說,即每過 10 毫秒,動畫的完成度更新時),這個方法被呼叫。

方法的引數是一個 ValueAnimatorValueAnimatorObjectAnimator 的父類,也是 ViewPropertyAnimator 的內部實現,所以這個引數其實就是 ViewPropertyAnimator 內部的那個 ValueAnimator,或者對於 ObjectAnimator 來說就是它自己本身。

ValueAnimator 有很多方法可以用,它可以檢視當前的動畫完成度、當前的屬性值等等。不過 ValueAnimator 是下一期才講的內容,所以這期就不多說了。

3.3 ObjectAnimator.addPauseListener()

由於 ObjectAnimator.pause() 是下期的內容,所以這個方法在這期就不講了。當然,如果你有興趣的話,現在就瞭解一下也可以。

3.3 ViewPropertyAnimator.withStartAction/EndAction()

這兩個方法是 ViewPropertyAnimator 的獨有方法。它們和 set/addListener() 中回撥的 onAnimationStart() / onAnimationEnd() 相比起來的不同主要有兩點:

  1. withStartAction() / withEndAction() 是一次性的,在動畫執行結束後就自動棄掉了,就算之後再重用 ViewPropertyAnimator 來做別的動畫,用它們設定的回撥也不會再被呼叫。而 set/addListener() 所設定的 AnimatorListener 是持續有效的,當動畫重複執行時,回撥總會被呼叫。

  2. withEndAction() 設定的回撥只有在動畫正常結束時才會被呼叫,而在動畫被取消時不會被執行。這點和 AnimatorListener.onAnimationEnd() 的行為是不一致的。

關於監聽器,就說到這裡。本期內容的講義部分也到此結束。

練習專案

為了避免轉頭就忘,強烈建議你趁熱打鐵,做一下這個練習專案:HenCoderPracticeDraw

下期預告

下期內容是屬性動畫的進階篇,主要講怎樣針對特殊型別的屬性做動畫,以及怎樣做複雜的動畫。雖然對於很多人來說,這期的內容學完就可以在很多人面前裝逼了,但我還是要說,下期的內容學完,你還能更上一層樓。

各位還記得幾周之前講三維旋轉時的這個動畫麼:

等下期內容結束後,這個動畫對你將是小意思。(我猜有些人在這期之後就能做出來這個,但下期之後做起來會更輕鬆。)

感謝

這期做了兩週多,腰痠背痛加茶飯不思,感覺快上西天了。

感謝我的廚師:我岳父。每天到了飯點下樓蹭飯、蹭完飯嘴一抹凳子一踢就走的感覺,還真不錯。
感謝我的按摩師:我老婆。
感謝我的字幕組:我老婆。
感謝我兒子終於上了幼兒園,不會有事沒事就推開我的房門讓我陪他玩了。
最後,我再假模假樣地感謝一下催更和不催更的各位的耐心或不耐心的等待。

覺得贊?

如果你看完覺得有收穫,把文章轉發到你的微博、微信群、朋友圈、公眾號,讓其他需要的人也看到吧。

相關文章