帶你走一波Android自定義Animator屬性動畫相關事項(一)

一、簡介

image.png

如上圖所示:android動畫分類大致有兩種一種是View動畫一種是轉場動畫

幀動畫:將圖片一張一張按順序播放,展現出動畫效果。

補間動畫:實現動畫alpha(淡入淡出),translate(位移),scale(縮放大小),rotate(旋轉)等效果,一般採用xml文件形式。

屬性動畫:(重點)它是對於對象屬性的動畫。補間動畫的內容,都可以通過屬性動畫實現。

這裏我們就不講幀動畫補間動畫,這兩個大家可以自己百度一下用法。(另外這篇文章中的動畫都是在代碼中實現的,如果要看xml的使用方法,可以看看Android 動畫使用 scale、alpha、translate、rotate、set
這篇其他人寫的這篇文章。

二、屬性動畫

基本使用 (ViewPropertyAnimator)

imag_view.animate().run {
translationX(400f) //設置左移
duration = 1000//設置動畫運行的時間
setInterpolator(LinearInterpolator()) //設置線性插值器
}

gifeditor_20191202_151011.gif
上面是最基本的使用,api提供的有移動旋轉縮放透明,看下面的api:
image.png

上面的api中可以看到 都存在絕對相對(方法後面-by)的方法,其中絕對的方法以上面的代碼爲例子,區別是:
translationX(400f)代表將translationX變成400
translationXBy(400f)代表將translationX增加400

這麼多api就不做逐一展示了。

ObjectAnimator

1. 基本的使用方式:
  1. 用 ObjectAnimator.ofXXX() 創建 ObjectAnimator 對象;
  2. 添加時長、差值器等各種參數
  3. 用 start() 方法執行動畫。
ObjectAnimator.ofFloat(imag_view,View.ROTATION,0f,180f).run {
    duration = 1000
    interpolator = LinearInterpolator()
    start()
}

gifeditor_20191202_164532.gif
上面是對一個系統提供的View進行動畫展示,主要方法是ObjectAnimator.ofFloat(imag_view,View.ROTATION,0f,180f)
第一個參數:傳入要進行屬性動畫的view
第二個參數:要變化的屬性值,這裏是傳入View.ROTATION,也就是"rotation"
第三個參數:屬性起始值
第四個參數:屬性結束值
後面還可以加入多個參數值,從第三個參數開始到最後第n個參數,表示屬性開始 ->中間值->中間值… ->結束值
注意一點:並非所有的屬性都是可以有set get方法,可以進行屬性動畫。

2. 自定義View動畫

自定義View屬性動畫以及使用的步驟:

  1. 爲要改變的屬性添加setter/getter方法
  2. setter方法中調用invalidate()使其重新繪畫
  3. 在onDraw()中根據改變的屬性繪畫出你要的效果
  4. 使用的時候跟ObjectAnimator基本使用方式一致

大致的模板代碼


class CircleView : View {
  //提供給外面改變的屬性值 這裏使用kotlin語法 實際上它默認已經實現了setter/getter方法
// 但是這裏個set方法我們要改寫一下 記得調用 invalidate()
    var progress: Float = 0f
        set(value) {
            field = value
            invalidate()
        }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //省略......
        canvas!!.drawArc(rectF, 135f, progress * 2.7f, false, paint)
        //省略......
        canvas.drawText("${progress.toInt()}%", centerX, centerY+40, paint)
    }

}

真正的實現與調用

class CircleView : View {

    val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    val rectF = RectF()

    init {
        paint.run {
            textSize = dpToPixel(50f)
            textAlign = Paint.Align.CENTER
        }
    }

    var radius = dpToPixel(120f)

    var progress: Float = 0f
        set(value) {
            field = value
            invalidate()
        }

    constructor(context: Context) : super(context)
    constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet)


    @RequiresApi(Build.VERSION_CODES.M)
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var centerX = width / 2f
        var centerY = height / 2f
        //畫弧形進度條
        paint.run {
            color = context.getColor(R.color.colorAccent)
            style = Paint.Style.STROKE
            strokeCap = Paint.Cap.ROUND
            strokeWidth = dpToPixel(20f)
        }
        rectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius)
        canvas!!.drawArc(rectF, 135f, progress * 2.7f, false, paint)

        //畫百分比的數值
        paint.run {
            color = Color.BLACK
            style = Paint.Style.FILL
        }
        canvas.drawText("${progress.toInt()}%", centerX, centerY+40, paint)
    }

}
//調用代碼
ObjectAnimator.ofFloat(circle_view,"progress",0f,80f).run {
  duration = 2000
  interpolator = OvershootInterpolator() //插值器 超過結束值後再回彈
  repeatCount = INFINITE  // 重複次數-無限循環
  repeatMode = RESTART //重複模式-重新開始
  start()
}

gifeditor_20191202_172002.gif

這裏設置了OvershootInterpolator插值器,使得它可以超過後回彈回來。這裏可以使用PropertyValuesHolders.ofKeyframe()做到類似效果,可以翻到下面對應內容看一下
#####ValueAnimator
這個是ObjectAnimator的父類,這個我並沒怎麼接觸,後期再補充吧

2. 設置監聽器

  • 設置ObjectAnimator的監聽器

監聽器類型.png

//添加停止的監聽器
addPauseListener( object : Animator.AnimatorPauseListener{
  override fun onAnimationPause(animation: Animator?) {
    Log.i("MainActivity","addPauseListener -- onAnimationPause ------------------------------------")
  }
  override fun onAnimationResume(animation: Animator?) {
    Log.i("MainActivity","addPauseListener -- onAnimationResume ------------------------------------")
  }
})
//添加更新的監聽器
addUpdateListener(object :ValueAnimator.AnimatorUpdateListener{
  override fun onAnimationUpdate(animation: ValueAnimator?) {
//    Log.i("MainActivity","addUpdateListener -- onAnimationUpdate ------------------------------------")
    }
  })
//添加監聽器
addListener(object : Animator.AnimatorListener{
  override fun onAnimationRepeat(animation: Animator?) {
    Log.i("MainActivity","AnimatorListener -- onAnimationRepeat------------------------------------")
    }
   override fun onAnimationEnd(animation: Animator?) {
    Log.i("MainActivity","AnimatorListener -- onAnimationEnd------------------------------------")
    }
  override fun onAnimationCancel(animation: Animator?) {
  Log.i("MainActivity","AnimatorListener -- onAnimationCancel------------------------------------")
  }
  override fun onAnimationStart(animation: Animator?) {
      Log.i("MainActivity","AnimatorListener -- onAnimationStart------------------------------------")
     }
  })
}

設置點擊事件監聽

R.id.start_object_animator ->{
                省略...
                mObjectAnim.start()
                Log.i("MainActivity","點擊開始狀態 --${!mObjectAnim.isStarted}")
            }

            R.id.end_object_animator ->{
                mObjectAnim.end()
                Log.i("MainActivity","點擊結束狀態 --${!mObjectAnim.isRunning}")

            }
            R.id.cancel_object_animator ->{
                mObjectAnim.cancel()
                Log.i("MainActivity","點擊取消狀態 -- ")
            }
            R.id.pause_object_animator->{
                if (mObjectAnim.isRunning) {
                    mObjectAnim.pause()
                    Log.i("MainActivity","點擊暫停狀態 --${mObjectAnim.isPaused}")
                }
            }
            R.id.reverse_object_animator->{
                Log.i("MainActivity","點擊反向狀態 --")
                mObjectAnim.reverse()
            }
            R.id.resume_object_animator->{
                mObjectAnim.resume()
                Log.i("MainActivity","點擊繼續執行 --")
            }

點擊順序 (這裏點擊開始按鈕是重新初始化動畫,請大家不要誤解)
開始start -> 暫停pause ->繼續執行resume->結束end
開始start->取消cancel
開始start->結束end

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-1LU1soK0-1583756850159)(https://user-gold-cdn.xitu.io/2019/12/6/16ed8de489e5844e?w=411&h=726&f=gif&s=7632472)]

MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 點擊開始狀態 --false
MainActivity: addPauseListener -- onAnimationPause ------------------------------------
MainActivity: 點擊暫停狀態 --true
MainActivity: addPauseListener -- onAnimationResume ------------------------------------
MainActivity: 點擊繼續執行 --
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 點擊結束狀態 --true
MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 點擊開始狀態 --false
MainActivity: AnimatorListener -- onAnimationCancel------------------------------------
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 點擊取消狀態 -- 
MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 點擊開始狀態 --false
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 點擊結束狀態 --true

上面是點擊產生的log日誌,其中有個addUpdateListener監聽我沒有打印信息,因爲動畫運行中就會不停打印出來,所以就沒有打印出來了。
可以看到所有的狀態都是可以有回調方法監聽的。

這裏有個方法要注意一下 cancel()end()這個兩個方法。

如果動畫是已經結束了end()的時候,就不會有回調onAnimationCancelonAnimationEnd兩個監聽方法了,看一下ValueAnimator源碼中有體現了

    @Override
    public void cancel() {
關鍵代碼1
        if (mAnimationEndRequested) {
            return;
        }
        if ((mStarted || mRunning) && mListeners != null) {
            if (!mRunning) {
                notifyStartListeners();
            }
            ArrayList<AnimatorListener> tmpListeners =
                    (ArrayList<AnimatorListener>) mListeners.clone();
            for (AnimatorListener listener : tmpListeners) {
關鍵代碼2
                  listener.onAnimationCancel(this);
            }
        }
關鍵代碼3
        endAnimation();
    }
    private void endAnimation() {
關鍵代碼4
        mAnimationEndRequested = true;
  省略......
            for (int i = 0; i < numListeners; ++i) {
關鍵代碼5
                tmpListeners.get(i).onAnimationEnd(this, mReversing);
            }
        }
省略......
    }

關鍵代碼1:mAnimationEndRequested == true則不走下面的邏輯,設置true是在關鍵代碼4中設置的,也就是說當動畫結束調用了endAnimation()就不會調用到兩個回調onAnimationCancelonAnimationEnd
關鍵代碼2跟3跟5:就是動畫未結束,所以調用了回調onAnimationCancel()再調用回調onAnimationEnd()

小結:當調用cancel()的時候,動畫未結束時則回調onAnimationCancel()onAnimationEnd(),當動畫結束時,則不會調用任何監聽回調方法

end方法有時候會回調兩個回調 分別是onAnimationStartonAnimationEnd,看一下ValueAnimator源碼中的邏輯

    public void end() {
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        if (!mRunning) {
            // Special case if the animation has not yet started; get it ready for ending
            startAnimation();
            mStarted = true;
        } else if (!mInitialized) {
            initAnimation();
        }
        animateValue(shouldPlayBackward(mRepeatCount, mReversing) ? 0f : 1f);
        endAnimation();
    }

從上面的邏輯可以看到,當調用到!mRunning == true的時候,會調用startAnimation()導致回調多一個onAnimationStart方法。

小結: 當調用end()的時候,如果動畫處於運行中,則回調onAnimationEnd,如果不處於運行中,則回調onAnimationStartonAnimationEnd

  • 設置ViewPropertyAnimator的監聽器

image.png
相比ObjectAnimator的監聽器,這裏的ViewPropertyAnimator多了兩個回調方法

withStartActionwithEndAction分別在開始跟結束調用,但是隻會被調用一次。
setListener中的onAnimationRepeat回調不會被調用,因爲ViewPropertyAnimator不支持重複
其它的回調跟ObjectAnimator一致

三. 屬性動畫組合

image.png

  • ViewPropertyAnimator

改變尺寸同時改變透明度,下面這種寫法是幾種動畫一塊運行的

R.id.btn_viewProperty_mulity->{
  if (isSelect) {
    imag_view.animate().scaleX(1.5f).scaleY(1.5f).alpha(0f).duration =2000
  }else{
    imag_view.animate().scaleX(1.0f).scaleY(1.0f).alpha(1f).duration =2000
  }
   isSelect = !isSelect
}

gifeditor_20191203_163831.gif

  • PropertyValuesHolder

ObjectAnimator使用時則是要通過PropertyValuesHolder來實現
上面的代碼可以用

R.id.btn_viewProperty_mulity->{
  if (isSelect) {
    var propertyValueHolder1 = PropertyValuesHolder.ofFloat("scaleX",1f,1.5f)
    var propertyValueHolder2 = PropertyValuesHolder.ofFloat("scaleY",1f,1.5f)
    var propertyValueHolder3 = PropertyValuesHolder.ofFloat("alpha",1f,0f)
    ObjectAnimator.ofPropertyValuesHolder(imag_view,propertyValueHolder1,propertyValueHolder2,propertyValueHolder3)
    .setDuration(2000).start()
  }else{
    var propertyValueHolder1 = PropertyValuesHolder.ofFloat("scaleX",1.5f,1f)
    var propertyValueHolder2 = PropertyValuesHolder.ofFloat("scaleY",1.5f,1f)
    var propertyValueHolder3 = PropertyValuesHolder.ofFloat("alpha",0f,1f)
    ObjectAnimator.ofPropertyValuesHolder(imag_view,propertyValueHolder1,propertyValueHolder2,propertyValueHolder3)
    .setDuration(2000).start()
    }
    isSelect = !isSelect
}
  • PropertyValuesHolders.ofKeyframe()同一個屬性拆分

ofKeyframe (關鍵幀),可以把同一個動畫屬性拆分成多個階段

var keyframe = Keyframe.ofFloat(0f,0f)//關鍵幀 0剛纔開時的時候
var keyframe1 = Keyframe.ofFloat(0.5f,95f)//關鍵幀 0.5進行到一半的時候
var keyFrame2 = Keyframe.ofFloat(1f,80f)//關鍵幀1最後的時候
var holder = PropertyValuesHolder.ofKeyframe("progress", keyframe, keyframe1, keyFrame2)
ObjectAnimator.ofPropertyValuesHolder(circle_view,holder).setDuration(3000).start()

gifeditor_20191203_175839.gif
實現類似interpolator = OvershootInterpolator()插值器超過結束值後再回彈的效果

  • AnimatorSet

也是組合動畫的,可以讓多個動畫配合執行,不過它可以讓動畫先後有序執行~~
playTogether(同時執行)、playSequentially(順序執行)
精確配置順序with(),before(),after()

示例代碼:
var animator1 = ObjectAnimator.ofFloat(imag_view,"alpha",0f,1f)
animator1.interpolator = AccelerateDecelerateInterpolator()
var animator3 = ObjectAnimator.ofFloat(imag_view,"scaleX",0f,1f)
var animator2 = ObjectAnimator.ofFloat(imag_view,"scaleY",0f,1f)
var animator4 = ObjectAnimator.ofFloat(imag_view,"translationX",0f,200f)
animator4.interpolator = LinearInterpolator()
AnimatorSet().apply {
  playTogether(animator2,animator3)
  playSequentially(animator1,animator4)
  duration = 2000
  start()
}

gifeditor_20191203_172107.gif
解釋一下上面的代碼
上面使用 AnimatorSet將多個ObjectAnimator放在一塊運行,並且playTogether(animator2,animator3) 代表一起運行
playSequentially(animator1,animator4) 代表先後順序運行
所以就有了下面的效果圖 (x軸、y軸方向放大跟透明度由0-1)是一起的,然後在x軸方向移動。

上面的播放邏輯也可以使用精確配置順序with(),before(),after()來實現

AnimatorSet().apply {
  play(animator1).with(animator2).with(animator3).before(animator4)
  duration = 2000
  start()
 }

注意:每個傳入給AnimatorSetanimator可以自己定義自己的動畫運行時間、差值器等,但是如果在AnimatorSet設置了運行時間的話則以在AnimatorSet設置的爲準。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章