屬性動畫

稀飯_發表於2018-07-03

上一篇說了View動畫和幀動畫,google為了解決它們帶來的問題,在3.0之後又新增了屬性動畫,目的是為了解決 :

1.不再侷限於檢視View物件

2.不再侷限於4種基本變換:平移、旋轉、縮放 & 透明度

屬性動畫繼承關係圖:

屬性動畫

屬性動畫的工作原理:在一定時間間隔內,通過不斷對值進行改變,並不斷將該值賦給物件的屬性,不斷的呼叫onDrow重新繪製檢視,從而實現該物件在該屬性上的動畫效果。

ValueAnimator類

ValueAnimator本質只是一種值的操作機制,至於實現動畫,是需要開發者手動將這些 值 賦給 物件的屬性值。

原理圖:

屬性動畫

從上面原理可以看出:ValueAnimator類中有3個重要的靜態變換邏輯方法:

  1. ValueAnimator.ofInt(int values)
  2. ValueAnimator.ofFloat(float values)
  3. ValueAnimator.ofObject(int values)

ValueAnimator.ofInt(int values)

將初始值 以整型數值的形式 過渡到結束值。對應的估值器是IntEvaluator

動畫的實現方式同View動畫一樣有兩種寫法,xml(不說明)和程式碼,實際開發中,一般都使用Java程式碼實現屬性動畫:因為很多時候屬性的起始值是無法提前確定的(無法使用XML設定),這就需要在Java程式碼裡動態獲取。

事例程式碼寫法:

tvjump.setOnClickListener {
    // 步驟1:設定動畫屬性的初始值 & 結束值,內部建立一個ValueAnimator物件,並預設設定了正數估值器
    val valueAnimator = ValueAnimator.ofInt(0, 30)
    // ofInt()作用:1.建立動畫例項 ;2.將傳入的多個Int引數進行平滑過渡:此處傳入0和4,表示將值從0平滑過渡到4
    // 如果傳入了3個Int引數 a,b,c ,則是先從a平滑過渡到b,再從b平滑過渡到C,以此類推
    // ValueAnimator.ofInt()內建了整型估值器,直接採用預設的.不需要設定,即預設設定瞭如何從初始值過渡到結束值的邏輯

    // 步驟2:設定動畫的播放各種屬性

    // 設定動畫執行的時長
    valueAnimator.setDuration(500)
    // 設定動畫延遲播放時間
    valueAnimator.setStartDelay(500)
    // 設定動畫重複播放次數 = 重放次數+1,動畫播放次數 = infinite時,動畫無限重複
    valueAnimator.setRepeatCount(0)
    // 設定重複播放動畫模式 ValueAnimator.RESTART(預設):正序重放 ;ValueAnimator.REVERSE:倒序回放
    valueAnimator.setRepeatMode(ValueAnimator.RESTART);

    //步驟3:通過監聽回撥獲取值的每個變化節點
    valueAnimator.addUpdateListener {
        //獲取每個變化的值
       var newValue =  it.getAnimatedValue() as? Int
        Log.e("rrrrrrr",newValue.toString())
        //步驟4:手動賦值
        // 獲取的值可以做任何操作,這裡是設定給一個控制元件的屬性,內部自動去呼叫重繪方法
        tvjump.translationY = newValue?.toFloat()?:0F
    }
    // 啟動值的改變
    valueAnimator.start()
}複製程式碼

注意:上邊改變translationY的值,對應的控制元件的座標引數:左上角xy(3.0之後增加的座標)的座標和translationY會改變,而對應的left,top,right,bottom的值並不會改變,(這些位置都是相對於父容器座標而言)它們之間的對應公式:

x=left+translationX

y=top+translationY

ValueAnimator.oFloat(float values)

ValueAnimator.ofInt()ValueAnimator.oFloat()僅僅只是在估值器上的區別:(即如何從初始值 過渡 到結束值)

  • ValueAnimator.oFloat()採用預設的浮點型估值器 (FloatEvaluator)
  • ValueAnimator.ofInt()採用預設的整型估值器(IntEvaluator

在使用上完全沒有區別,此處對ValueAnimator.oFloat()的使用就不作過多描述。

 ValueAnimator.ofObject()

將初始值以物件的形式過渡到結束值。

講解之前先先說一下什麼是插值器,什麼是估值器。

插值器:決定值的變化模式(勻速、加速blabla)

估值器:決定值的具體變化數值

ValueAnimator.ofInt() & ValueAnimator.ofFloat()都具備系統內建的估值器,即FloatEvaluator & IntEvaluator。

對於ValueAnimator.ofObject(),對物件的動畫操作,系統無法知道如何從初始物件過度到結束物件,我們需自定義估值器(TypeEvaluator)來告知系統如何進行從初始物件過渡到結束物件的邏輯:

自定義估值器的步驟:

1.建立物件型別

2.實現TypeEvaluator和傳遞型別

3.複寫evaluate方法,裡邊寫變化邏輯

4.返回計算後的值

5.在需要獲取值的地方手動獲取並使用

類似這樣的程式碼

// 實現TypeEvaluator介面,寫出物件泛型
class ObjectEvaluator : TypeEvaluator<Bean> {

    override//複寫evaluate()在evaluate()裡寫入物件動畫過渡的邏輯
    fun evaluate(fraction: Float, startValue: Bean, endValue: Bean): Bean {
        // 引數說明
        // fraction:表示動畫完成度(根據它來計算當前動畫的值)
        // startValue、endValue:動畫的初始值和結束值

        //......

        // 返回物件動畫過渡的邏輯計算後的值
        return Bean()
    }
}複製程式碼

事例演示1:

步驟1:建立物件型別:

data class Bean(val x: Float, val y: Float) 複製程式碼

步驟2,3,4

class ObjectEvaluator : TypeEvaluator<Bean> {
    override//複寫evaluate()在evaluate()裡寫入物件動畫過渡的邏輯
    fun evaluate(fraction: Float, startValue: Bean, endValue: Bean): Bean {
        //計算出來變化的值,這個方法會自動迴圈被系統呼叫,直到達到設定的結束值為止
        val newX = startValue.x + fraction * (endValue.x - startValue.x)
        val newY = startValue.y + fraction * (endValue.y - startValue.y)
        // 已經變化的值 = 變化率*總需要變化的區間
        //新值 = 開始值+已經變化的值
        return Bean(newX, newY)
    }
}複製程式碼

步驟5:

//開始物件
val startBean = Bean(0F, 0F)
//結束物件
val endBean = Bean(100F, 300F)
//傳遞自定義的估值器和開始物件和結束物件
val objectAnimator = ValueAnimator.ofObject(ObjectEvaluator(), startBean, endBean)
//設定動畫時間
objectAnimator.setDuration(1000)
//通過回撥獲取值的變化,設定給控制元件的屬性,來進行動畫
objectAnimator.addUpdateListener {
    val bean = it.getAnimatedValue() as Bean
    Log.e("rrrrrrr", bean.toString())
    tvjump.translationX = bean.x
    tvjump.translationY = bean.y
}
//最後別忘了開啟動畫
objectAnimator.start()複製程式碼

從上面可以看出,其實ValueAnimator.ofObject()的本質還是操作 ** 值 **,只是是採用將 多個值 封裝到一個物件裡的方式 同時對多個值一起操作而已

事例演示2:自定義控制元件中動畫獲取估值器返回的值

class MyView : View {
    val paint: Paint//畫筆
    var bean: Bean? = null//資料變化物件
    val RADIUS:Float = 50F//圓形的半徑
    //構造方法
    constructor(context: Context?) : this(context, null)
    //構造方法
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
    //構造方法
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        //抗鋸齒
        paint = Paint(Paint.ANTI_ALIAS_FLAG)
        //設定畫筆顏色
        paint.color = Color.BLACK
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (bean == null) {
            bean = Bean(300F, 0F)
            //開啟畫開始的圓形
            canvas.drawCircle(bean!!.x , bean!!.y, RADIUS, paint)
            //傳遞自定義的估值器和開始物件和結束物件
            val objectAnimator = ValueAnimator.ofObject(ObjectEvaluator(), bean, Bean(500F, 300F))
            //設定動畫時間
            objectAnimator.setDuration(1000)
            //獲取資料變化回撥
            objectAnimator.addUpdateListener {
                //從新賦值
                bean = it.getAnimatedValue() as Bean
                //重新整理繪製
                invalidate()
            }
            objectAnimator.start()

        }else{
            canvas.drawCircle(bean!!.x , bean!!.y, RADIUS, paint)
        }

    }

}複製程式碼

ObjectAnimator類

繼承自ValueAnimator類,即底層的動畫實現機制是基於ValueAnimator類對值的操作,通過不斷控制值的變化,再不斷自動賦給物件的屬性,從而實現動畫效果。既然是繼承關係,那麼子類也有用父類手動去設定動畫的方法,並且豐富了自動設定屬性的方法

屬性動畫

從上面的工作原理可以看出:ObjectAnimatorValueAnimator類的區別:

  • ValueAnimator 類是先改變值,然後 手動賦值 給物件的屬性從而實現動畫;是 間接 對物件屬性進行操作;
  • ObjectAnimator 類是先改變值,然後 自動賦值 給物件的屬性從而實現動畫;是 直接 對物件屬性進行操作;

類似類似設定一個控制元件讓他進行平移一段距離如果用ValueAnimtor去設定,需要程式碼如下:

ValueAnimtor寫法:

val valueAnimator = ValueAnimator.ofFloat(100F)
valueAnimator.setDuration(500)
valueAnimator.addUpdateListener {
    var newValue = it.getAnimatedValue() as? Float
    tvjump.translationY = newValue!!
}
valueAnimator.start()複製程式碼

ObjectAnimator寫法

// 建立動畫例項並引數設定,系統內建的浮點型估值器FloatEvaluator
val anim = ObjectAnimator.ofFloat(tvjump, "translationX", 100F)
//引數1:需要動畫的物件
//引數2:需要改變的屬性key
//引數3:float ....values:動畫初始值 & 結束值(不固定長度)
// 開啟動畫
anim.start()複製程式碼
很明顯objectAnimator少了監聽,封裝隱藏了變換的細節,和賦值的細節。

同View動畫一樣系統還給我們提供了旋轉,平移,縮放,透明度的屬性值供我們使用:

1.透明度

val anim = ObjectAnimator.ofFloat(tvjump, "alpha", 100F)
anim.setDuration(1000)
anim.start()複製程式碼

2.旋轉

val anim = ObjectAnimator.ofFloat(tvjump, "rotation", 100F)
anim.setDuration(1000)
anim.start()複製程式碼

3.平移

val anim = ObjectAnimator.ofFloat(tvjump, "translationX", 100F)
anim.setDuration(1000)
anim.start()複製程式碼

4.縮放

val anim = ObjectAnimator.ofFloat(tvjump, "scaleX", 100F)
anim.setDuration(1000)
anim.start()複製程式碼

對於透明度,平移,縮放,旋轉還可以設定一下屬性: Alpha ,TranslationX ,TranslationY , ScaleX , ScaleY , Rotation ,  RotationX , RotationY 。

除了系統給我們的一些常用的動畫屬性之外,我們還可以自己去自定義屬性動畫,這是由自定屬性動畫的原理決定。

需要注意:採用ObjectAnimator 類實現動畫效果,那麼需要操作的物件就必須有該屬性的set() & get()方法 。

回顧一下之前的知識,並利用之前的知識去讓一個控制元件去同時去做兩個動畫:

如果有一個需求是讓一個控制元件同時去做平移個旋轉組合動畫我們可以這樣寫:

使用ValueAnimator:

val tran = ValueAnimator.ofFloat(300F)
tran.duration = 1000
val roat = ValueAnimator.ofFloat(180F)
roat.duration = 1000
tran.addUpdateListener { tvjump.translationY= it.getAnimatedValue() as Float }
roat.addUpdateListener { tvjump.rotation = it.getAnimatedValue() as Float }
tran.start()
roat.start()複製程式碼

或者用ofObject把多個資料當成物件去處理,然後自定義估值器

使用ObjectAnimator:

val tranY = ObjectAnimator.ofFloat(tvjump, "translationY", 300F)
val roatall = ObjectAnimator.ofFloat(tvjump, "rotation", 180F)
tranY.start()
roatall.start()複製程式碼

我們很明顯就會發現有很多重複的程式碼,當然google工程師同樣能發現這個問題,類似View的組合動畫有AnimationSet類,同樣的屬性動畫也有對應的AnimatorSet類。

AnimatorSet

專門用在組合屬性動畫的類

我們知道之前View動畫要設定動畫的先後順序,我們一般都是通過延遲時間,系統並沒有給我們提供其他的控制方法。幸運的是,AnimatorSet給我們提供了動畫執行先後順序的方法:

  • AnimatorSet.play(Animator anim) :要播放的動畫
  • AnimatorSet.with(Animator anim) :將現有動畫和傳入的動畫同時執行 
  • AnimatorSet.after(Animator anim) :將現有動畫插入到傳入的動畫之後執行 
  • AnimatorSet.before(Animator anim) : 將現有動畫插入到傳入的動畫之前執行
  • AnimatorSet.after(long delay) :將現有動畫延遲x毫秒後執行

play(a1).with(a2) a1和a2同時開始

play(a1).before(a2) 先a1後a2

play(a1).after(a2) 先a2後a1

順序連線

上邊的程式碼我們可以通過AnimatorSet組合到一起去執行:

val tran = ValueAnimator.ofFloat(0F, 300F)
val roat = ValueAnimator.ofFloat(0F, 180F)
tran.addUpdateListener { tvjump.translationY = it.getAnimatedValue() as Float }
roat.addUpdateListener { tvjump.rotation = it.getAnimatedValue() as Float }
val animatorSet = AnimatorSet()
animatorSet.play(tran).with(roat)
animatorSet.duration=1000
animatorSet.start()複製程式碼

或者這樣寫:

val tranY = ObjectAnimator.ofFloat(tvjump, "translationY", 300F)
val roatall = ObjectAnimator.ofFloat(tvjump, "rotation", 180F)
val animatorSet = AnimatorSet()
animatorSet.play(tranY).with(roatall)
animatorSet.duration = 1000
animatorSet.start()複製程式碼

動畫監聽

所有屬性動畫都可以設定關鍵幀的監聽,因為他在基類中:

animatorSet.addListener(object:Animator.AnimatorListener{
    override fun onAnimationRepeat(animation: Animator?) {
    
    }
    override fun onAnimationEnd(animation: Animator?) {

    }
    override fun onAnimationCancel(animation: Animator?) {

    }
    override fun onAnimationStart(animation: Animator?) {

    }
})複製程式碼

有些時候我們並不需要監聽動畫的所有時刻,這個時候我們可以使AnimatorListenerAdapter來解決不必要的重寫問題:

animatorSet.addListener(object : AnimatorListenerAdapter() {

    override fun onAnimationStart(animation: Animator?) {

    }
})複製程式碼

小結:

  • 屬性動畫的本質是對值的變換的操作,然後賦值給屬性。
  • ValueAnimator手動賦值,可以自定義估值器對自定義的物件操作值的變化
  • ObjectAnimator自動賦值,隱藏實現細節
  • AnimatorSet組合動畫,提供同時,延遲,之前,之後執行的方法
  • 聽動畫執行的特殊幀狀態addListener,所有屬性動畫都有這個監聽
  • 聽動畫所有幀的狀態,只有ValueAnimator或者繼承ValueAnimator的類才有這個監聽
  • 屬性動畫在頁面關閉的時候應該停止動畫,防止記憶體洩漏

常見問題:

OOM問題:發生在幀動畫的使用中,儘量少用幀動畫

記憶體洩漏問題:無限迴圈屬性動畫有可能出現記憶體洩漏,需要在頁面關閉的時候取消動畫

setVisibility(View.GONE)失效:出現在View動畫中,這個時候需要view.clearAnimation()清除View動畫

擴充思考

既然屬性動畫是對值的操作,那麼我們能不能用迴圈去代替值的變換呢?

不能!!!

一定不能!!!

原因:

for迴圈是沒有時間概念的,而屬性動畫是有時間概念!

比如我們要畫一條動態的直線動畫,如果建立一個屬性,並且寫了set方法如下,

private var progress = 0
    set(value) {
        field = value
        invalidate()
    }複製程式碼

這個時候迴圈去改變這個值,是沒有動畫的,(除非我們每隔一段時間改變一個值,比如寫個定時器去改變)

而我們用屬性動畫去改變,它是有動畫的!

所以,屬性動畫準確的說是:具有時間概念的,對值改變的操作!


相關文章