Android屬性動畫,看完這篇夠用了吧

語言: CN / TW / HK

隨著APP的開發週期演進,APP不再滿足基礎的功能保障,需要有較好視覺體驗和互動操作。那麼動畫效果是必不可少的,動畫有幀動畫,補間動畫,屬性動畫等等。

本文通過一些簡單常見的動畫效果,和大家重溫屬性動畫的相關知識點。旨在通過全文,全面掌握屬性動畫~如果看完本文,還需要查閱其他文章,說明本文總結得還不夠好,歡迎留言補充。

一、屬性動畫概覽

顧名思義,通過控制物件的屬性,來實現動畫效果。官方定義:定義一個隨著時間 (注:停個頓)更改任何物件屬性的動畫,無論其是否繪製到螢幕上。

可以控制物件什麼屬性呢?什麼屬性都可以,理論是通過set和get某個屬性來達到動畫效果。例如常用下面一些屬性來實現View物件的一些動畫效果。

  • 位移:translationXtranslationYtranslationZ
  • 透明度:alpha,透明度全透明到不透明:0f->1f
  • 旋轉:rotation,旋轉一圈:0f->360f
  • 縮放:水平縮放scaleX,垂直縮放scaleY

簡單的效果圖:

二、基本使用

簡單介紹View物件幾個屬性動畫的使用。

1、位移屬性動畫

效果圖:

先看一下佈局程式碼的實現:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary">

    <LinearLayout
        android:id="@+id/llAddAccount"
        android:layout_width="wrap_content"
        android:layout_height="35dp"
        android:layout_alignParentRight="true"
        android:layout_marginTop="100dp"
        android:layout_marginRight="-70dp"//將現有檢視藏在螢幕的右邊
        android:background="@drawable/bg_10_10_fff">

        <ImageView
            android:id="@+id/ivMakeNote"
            android:layout_width="35dp"
            android:layout_height="30dp"
            android:layout_gravity="center_vertical"
            android:paddingLeft="2dp"
            android:paddingTop="2dp"
            android:paddingBottom="2dp"
            android:src="@mipmap/ic_account_add" />

        <TextView
            android:id="@+id/tvAddAccount"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center_vertical"
            android:paddingRight="12dp"
            android:text="新增賬戶"
            android:textColor="@color/colorPrimary"
            android:textSize="14sp" />
    </LinearLayout>
</RelativeLayout>
複製程式碼

上面只是簡單實現了佈局,下面看看屬性動畫程式碼的實現:

llAddAccount.setOnClickListener {
    val objectAnimation =ObjectAnimator.ofFloat(llAddAccount, "translationX", 0f, -70f)
    objectAnimation.start()
}
複製程式碼

到這裡,我們才真正看到屬性動畫的影子。通過ObjectAnimator的工廠方法ofFloat我們得到一個ObjectAnimator物件,並通過該物件的start()方法,開啟動畫效果。

ofFloat()方法的第一個引數為要實現動畫效果的View,例如這裡整體效果的LinearLayout第二個引數為屬性名,也就是前面所說的:translationX,translationY,alpha,rotation,scaleX,scaleY等,這裡要實現的是水平平移效果,所以我們採用了translationX;第三引數為可變長引數,第一個值為動畫開始的位置,第二個值為結束值得位置,如果陣列大於3位數,那麼前者將是後者的起始位置。

注意事項:如果可變長引數只有一個值,那麼ObjectAnimator的工廠方法會將值作為動畫結束值,此時屬性必須擁有初始化值和getXXX方法。

translationXtranslationY這裡涉及到的位移都是相對自身位置而言。例如 View在點A(x,y)要移動到點B(x1,y1),那麼ofFloat()方法的可變長引數,第一個值應該0f,第二個值應該x1-x

XML佈局實現:

在res/animator資料夾下,建立animator_translation.xml檔案,內容如下:

<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="translationX"
    android:valueFrom="0dp"
    android:valueTo="-70dp"
    android:valueType="floatType"
/>
複製程式碼

在程式碼上呼叫:

llAddAccount.setOnClickListener {
    val objectAnimation =AnimatorInflater.loadAnimator(this,R.animator.animator_translation)
    objectAnimation.setTarget(llAddAccount)
    objectAnimation.start()
}
複製程式碼

2、透明屬性動畫

透明度屬性動畫比較簡單,即控制View的可見度實現視覺差動畫效果。這裡展示效果是從不透明到透明,再到不透明。

程式碼如下:

tvText.setOnClickListener {
    val objectAnimation =ObjectAnimator.ofFloat(tvText, "alpha", 1f,0f,1f)
    objectAnimation.duration=3000
    objectAnimation.start()
}
複製程式碼

ofFloat()方法將屬性名換成了透明度alpha,並且可變長引數增加到了3個。給ObjectAnimator物件的duration屬性設定了動畫展示時間3秒,預設情況下300毫秒。

3、縮放屬性動畫

縮放可以通過控制scaleXscaleY分別在X軸和Y軸上進行縮放,如下圖在X軸中進行兩次兩倍縮放。

程式碼如下:

tvText.setOnClickListener {
    val objectAnimation =ObjectAnimator.ofFloat(tvText, "scaleX", 1f,2f)
        objectAnimation.duration=3000
        objectAnimation.repeatCount=2
        objectAnimation.repeatMode=ValueAnimator.REVERSE
        objectAnimation.start()
}
複製程式碼

ofFloat()方法傳入引數屬性為scaleXscaleY時,動態引數表示縮放的倍數。設定ObjectAnimator物件的repeatCount屬性來控制動畫執行的次數,設定為ValueAnimator.INFINITE表示無限迴圈播放動畫;通過repeatMode屬性設定動畫重複執行的效果,取值為:ValueAnimator.RESTARTValueAnimator.REVERSE

ValueAnimator.RESTART效果:(即每次都重頭開始)

ValueAnimator.REVERSE效果:(即和上一次效果反著來)

4、旋轉屬性動畫

旋轉動畫也比較簡單,將一個View進行順時針或逆時針旋轉。

程式碼如下:

tvText.setOnClickListener {
    val objectAnimation =
        ObjectAnimator.ofFloat(tvText, "rotation", 0f,180f,0f)
    objectAnimation.duration=3000
    objectAnimation.start()
}
複製程式碼

ofFloat()方法的可變長引數,如果後者的值大於前者,那麼順時針旋轉,小於前者,則逆時針旋轉。

三、AnimatorSet

如果想要一個動畫結束後播放另外一個動畫,或者同時播放,可以通過AnimatorSet來編排。

val aAnimator=ObjectAnimator.ofInt(1)
val bAnimator=ObjectAnimator.ofInt(1)
val cAnimator=ObjectAnimator.ofInt(1)
val dAnimator=ObjectAnimator.ofInt(1)

AnimatorSet().apply {
    play(aAnimator).before(bAnimator)//a 在b之前播放
    play(bAnimator).with(cAnimator)//b和c同時播放動畫效果
    play(dAnimator).after(cAnimator)//d 在c播放結束之後播放
    start()
}
複製程式碼

或者

AnimatorSet().apply {
    playSequentially(aAnimator,bAnimator,cAnimator,dAnimator) //順序播放
    start()
}

AnimatorSet().apply {
    playTogether(animator,bAnimator,cAnimator,dAnimator) //同時播放
    start()
}
複製程式碼

另有:

AnimatorSet ().apply {
    play(aAnimator).after(1000) //1秒後播放a動畫
    start()
}
複製程式碼

四、ViewPropertyAnimator

如果只是針對View物件的特定屬性同時播放動畫,我們也可以採用ViewPropertyAnimator

例如:

 tvText.animate().translationX(100f).translationY(100f).start()
複製程式碼

支援屬性:

  • translationX、translationY、translationZ
  • x、y、z
  • alpha
  • scaleX、scaleY

注意到ViewPropertyAnimator物件具有property(Float)propertyBy(Float)方法,其中property(Float)是指屬性變化多少(可以理解一次有效),而propertyBy(Float)每次變化多少(可以理解多次有效)。

舉例說明:

translationX

tvText.setOnClickListener {
    val animator = tvText.animate()
    animator.duration=1000
    animator.translationX(100f)//點選一次會向右偏移,再點選沒效果
    animator.start()
}
複製程式碼

translationXBy

tvText.setOnClickListener {
    val animator = tvText.animate()
    animator.duration=1000
    animator.translationXBy(100f)//每次點選都會向右偏移
    animator.start()
}
複製程式碼

五、ValueAnimator與ObjectAnimator

ValueAnimator作為ObjectAnimator的父類,主要動態計算目標物件屬性的值,然後設定給物件屬性,達到動畫效果,而ObjectAnimator則在ValueAnimator的基礎上極大地簡化對目標物件的屬性值的計算和新增效果,融合了 ValueAnimator 的計時引擎和值計算以及為目標物件的命名屬性新增動畫效果這一功能。

舉個栗子,通過ValueAnimator的工廠方法ofFloatofIntofArgbofObject來實現動畫效果:

程式碼如下:

    //ValueAnimator實現
    tvText.setOnClickListener {
        val valueAnimator = ValueAnimator.ofFloat(0f, 180f)
        valueAnimator.addUpdateListener {
               tvText.rotationY = it.animatedValue as Float //手動賦值
        }
        valueAnimator.start()
    }
    //ObjectAnimator實現
     ObjectAnimator.ofFloat(tvText, "rotationY", 0f, 180f).apply { start() }
複製程式碼

從上面程式碼可以看出,使用ValueAnimator實現動畫,需要手動賦值給目標物件tvTextrotationY,而ObjectAnimator則是自動賦值,不需要手動賦值就可以達到效果。

動畫過程可以通過AnimatorUpdateListenerAnimatorListener來監聽。

 ObjectAnimator.ofFloat(tvText, "translationX", 0f, 780f, 0f).apply {
            duration=3000//完成動畫所需要時間
            repeatCount=ValueAnimator.INFINITE  //重複次數:無限迴圈
            repeatMode=ValueAnimator.RESTART    //重複模式:重頭開始
            addUpdateListener { //監聽值變化
                tvText.translationX= it.animatedValue as Float
            }
            addListener(object:Animator.AnimatorListener{
                override fun onAnimationRepeat(animation: Animator?) {
                    //動畫重複
                }

                override fun onAnimationEnd(animation: Animator?) {
                    //動畫結束
                }

                override fun onAnimationCancel(animation: Animator?) {
                    //動畫取消
                }

                override fun onAnimationStart(animation: Animator?) {
                    //動畫開始
                }
            })
        }
複製程式碼

動畫可呼叫start()方法開始,也可呼叫cancel()方法取消。

那麼,要正確使屬性動畫實現動畫效果,那麼目標物件應該注意什麼?

  • 屬性必須具有 set<PropertyName>() 形式的 setter 函式(採用駝峰式大小寫形式),例如,如果屬性名稱為 text,則需要使用 setText() 方法。
  • 如果ObjectAnimator的一個工廠方法中僅為 values... 引數指定了一個值,那麼該引數需要提供初始值和getPropertyName()方法。
  • 屬性的初始值和結束值之間必須保持型別相同。
  • 可能需要在UpdateListener物件中呼叫invalidate() 方法,來重新整理屬性作用後的效果。

六、XML實現

本文一開始介紹位移屬性動畫時,有提到通過XML檔案來實現動畫效果,在這裡進一步細講。

在res/animator資料夾下,建立animator_translation.xml檔案。XML檔案有四個標籤可用,要注意到propertyValuesHolder標籤的Android 版本適配。

<?xml version="1.0" encoding="utf-8"?>
<set>   =>AnimatorSet
    <animator/>  =>ValueAnimator
    <objectAnimator>    =>ObjectAnimator
        <propertyValuesHolder/> =>PropertyValuesHolder
    </objectAnimator>
</set>
複製程式碼

set標籤對應程式碼的AnimatorSet,只有一個屬性可以設定:android:ordering,取值:同時播放together、順序播放sequentially

animator標籤對應程式碼的ValueAnimator,可以設定如下屬性:

  • android:duration:動畫時長
  • android:valueType:屬性型別,intTypefloatTypecolorType
  • android:valueFrom:屬性初始值
  • android:valueTo:屬性結束值
  • android:repeatCount:重複次數
  • android:repeatMode:重複模式
  • android:interpolator:插值器,可看下一節預設插值器。
  • android:startOffset:延遲,對應startOffset()延遲多少毫秒執行

示例:

    <animator
        android:duration="1000"
        android:valueType="floatType"
        android:valueFrom="0f"
        android:valueTo="100f"
        android:repeatCount="infinite"
        android:repeatMode="restart"
        android:interpolator="@android:interpolator/linear"
        android:startOffset="100"
        />
複製程式碼

objectAnimator屬性對應程式碼ObjectAnimator,由於繼承自ValueAnimator,所以屬性相對多了` android:propertyName。

七、估值器與插值器

看到這裡,不知道小夥伴們有沒有這個疑問,屬性動畫是如何計算屬性的值的?

這份工作由型別估值器TypeEvaluator與時間插值器TimeInterpolator來完成的。

插值器:根據時間流逝的百分比計算出當前屬性值改變的百分比。

估值器:根據當前屬性改變的百分比來計算改變後的屬性值。

從它兩的已定義,也可以看出它們之間的協調關係,先由插值器根據時間流逝的百分比計算出目標物件的屬性改變的百分比,再由估值器根據插值器計算出來的屬性改變的百分比計算出目標物件屬性對應型別的值。

從估值器和插值器可以看出屬性動畫的工作原理,下面看看官方對工作原理的解析:

更多的原理可以看看連結

估值器

SDK中預設帶有的估值器有: IntEvaluatorFloatEvaluatorArgbEvaluator,他們分別對應前面我們呼叫 ValueAnimator物件所有對應的ofIntofFloatofArgb函式的估值器,分別用在Int型別,Float,顏色值型別之間計算。而ofObject函式則對應我們自定義型別的屬性計算。

當估值器的型別不滿足需求,就需要自定義型別估值器。例如我們要實現下面效果:

這個效果可以通過AnimatorSet來實現,但我們這裡採用自定義TypeEvaluator來實現TextView從螢幕左上角斜線滑到螢幕右下角。

  1. 定義Point類,我們操作的物件。
data class Point(var x: Float, var y: Float)
複製程式碼
  1. 定義PointEvaluator估值器,繼承自TypeEvaluator,泛型引數為Point型別。通過實現evaluate()方法,可以實現很多複製的動畫效果,我們這裡實現上面簡單演算法。
class PointEvaluator : TypeEvaluator<Point> {
    /**
     * 根據插值器計算出當前物件的屬性的百分比fraction,估算去屬性當前具體的值
     * @param fraction 屬性改變的百分比
     * @param startValue 物件開始的位置,例如這裡點座標開始位置:螢幕左上角位置
     * @param endValue 物件結束的位置,例如這裡點座標結束的位置:螢幕右下角位置
     */
    override fun evaluate(fraction: Float, startValue: Point?, endValue: Point?): Point {
        if (startValue == null || endValue == null) {
            return Point(0f, 0f)
        }

        return Point(
            fraction * (endValue.x - startValue.x),
            fraction * (endValue.y - startValue.y)
        )
    }
}
複製程式碼
  1. 使用
       val animator= ValueAnimator.ofObject(
            PointEvaluator(), 
            Point(0f, 0f),//動畫開始屬性值
            Point(
                ScreenUtils.getScreenWidth(this).toFloat(),
                ScreenUtils.getScreenHeight(this).toFloat()
            )//動畫結束值
        )
        
        animator.addUpdateListener {//手動更新TextView的x和y 屬性
            val point = it.animatedValue as Point
            tvText.x = point.x
            tvText.y = point.y 
            logError("point:${point}")
        }
        animator.duration = 5000

        btnStart.setOnClickListener {
            animator.start()
        }
複製程式碼

一個簡單的自定義估值器就算完成了。數學學的好,任何複雜效果都不是問題。

插值器

TypeEvaluator物件的evaluate()方法的fraction引數就是插值器計算得來,SDK中預設的時間插值器有:

  • LinearInterpolator 線性(勻速)
  • AccelerateInterpolator 持續加速
  • DecelerateInterpolator 持續減速
  • AccelerateDecelerateInterpolator 先加速後減速
  • OvershootInterpolator 結束時回彈一下
  • AnticipateInterpolator 開始回拉一下
  • BounceInterpolator 結束時Q彈一下
  • CycleInterpolator 來回迴圈

看看效果:

LinearInterpolator

AccelerateInterpolator
DecelerateInterpolator
AccelerateDecelerateInterpolator
OvershootInterpolator
AnticipateInterpolator
BounceInterpolator
CycleInterpolator
正常情況下,預設的插值器已經夠用,如果自己數學厲害,想顯擺一下,也是通過實現TimeInterpolator介面的getInterpolation()自定義的。

/**
 * A time interpolator defines the rate of change of an animation. This allows animations
 * to have non-linear motion, such as acceleration and deceleration.
 */
public interface TimeInterpolator {

    /**
     * Maps a value representing the elapsed fraction of an animation to a value that represents
     * the interpolated fraction. This interpolated value is then multiplied by the change in
     * value of an animation to derive the animated value at the current elapsed animation time.
     *
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0 represents
     *        the end
     * @return The interpolation value. This value can be more than 1.0 for
     *         interpolators which overshoot their targets, or less than 0 for
     *         interpolators that undershoot their targets.
     */
    float getInterpolation(float input);
}
複製程式碼

八、Keyframe

要控制動畫速率的變化,就得去自定義插值器或估值器,對我這種數學渣渣來說,簡直比上天一樣難的。

所以渣渣們可以考慮用關鍵幀Keyframe物件來實現。Keyframe讓我們可以指定某個屬性百分比時物件的屬性值

    tvText.setOnClickListener {
        val start = Keyframe.ofFloat(0f, 0f)
        val middle = Keyframe.ofFloat(0.3f, 400f)
        val end = Keyframe.ofFloat(1f, 700f)
        val holder=PropertyValuesHolder.ofKeyframe("translationX",start,middle,end)
        ObjectAnimator.ofPropertyValuesHolder(tvText,holder).apply {
            duration=2000
            start()
        }
    }
複製程式碼

上面程式碼分別定義了三個關鍵幀,分別在屬性百分比為0f0.3f1f對應的translationX的值。

動畫效果:

可以看到效果先快後慢。

Keyframe同樣支援ofFloatofIntofObject。使用關鍵幀,至少需要有兩個關鍵幀,不然坐等奔潰吧。PropertyValuesHolder物件是用來儲存動畫過程所操作的屬性和對應的值。

九、總結

通過ObjectAnimator的工廠方法可以快速實現一個屬性動畫,但預設的屬性動畫不滿足自己的需求是,可以通過ValueAnimator物件來定義自己的屬性,注意屬性的要求。可以通過AnimatorSet來實現屬性組合播放效果。

動畫的原理是通過時間插值器與型別估值器配置使用,控制物件的屬性來實現動畫效果。