[Android]多層波紋擴散動畫——自定義View繪製

万俟霜風發表於2019-03-04

之前整理過一些屬性動畫的基本操作,這一段時間的動畫相關需求都安然度過了。直到這次……

一、另一種動畫需求

多數互動中的動畫都是讓單個頁面元素動起來,這種就很適合用屬性動畫實現。但是對於 多個元素非頁面內元素 的動畫需求,就不方便用View+屬性動畫實現了。

舉個例子,也就是這次做的:

waveview

波紋效果需要同時繪製 多個 同心圓,而且這些圓 不是頁面內的元素,未觸發之前不需要顯示。如果用屬性動畫實現,至少需要在xml佈局檔案中新增多個ImageView畫圓,效率低還不易複用。

二、WaveView分析

使用自定義View繪製動畫的主要思路是這樣的:

  1. 分解動畫成幀,考慮如何在onDraw中繪製每一幀
  2. 提取出繪製所需引數,分為隨時間變化不可變兩種,不可變引數可以暴露出去(setter方法/attribute設定)
  3. 總結隨時間變化的引數變化規律,實現時間軸
  4. 按需求提供出播放,暫停,停止,重置等等方法

按照這個思路,一步步實現一下WaveView吧。

第一步:
onDraw中畫圓需要用到canvas.drawCircle方法,四個引數:圓心x、y座標,半徑和Paint。WaveView繪製的每一幀都是圓,區別是圓的半徑,數量,透明度。還需要設定圓的最小半徑和最大半徑,以及擴散的時候兩個圓的半徑差。

每次繪製只需要把所有的圓畫出來:

onDraw

第二步:
擴散過程中隨時間變化的引數只有半徑和透明度。圓的數量,最小最大半徑和半徑差則是不可變引數。所有的都加了預設值,防止

variable

第三步:
最艱難的一步,時間軸。我們需要一個定時觸發的機制去改變mWaveList中的每一個值,還要同時修改paint的alpha值。這個變數嘛,還是越少越方便,波紋的效果是半徑越大alpha越小,只要控制半徑變化就可以計算出alpha。然後便是迴圈的問題,當波紋半徑大於最大半徑波紋就會消失,此時便可迴圈使用,再從最小半徑擴散一次。

我這裡的時間軸用了CountDownTimer,應該不是一種很好的選擇,只是個人習慣寫起來順手了…

timeline

第四步:
最後這個就簡單了,直接控制CountDownTimer的start和cancel就可以了。

[Android]多層波紋擴散動畫——自定義View繪製

三、再完善一下?

內部功能已經實現了,還要提取引數的設定方法,方便其他地方使用呀。java中需要新增setter/getter方法,用模版程式碼生成就好,kotlin的話直接把需要暴露的field改為public即可。

如果需要在xml佈局檔案中設定預設引數,還需要新增對應的attribute,現在這樣已經夠用了,我就不加了?

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.os.CountDownTimer
import android.util.AttributeSet
import android.view.View


class WaveView@JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {


    private val paint:Paint = Paint()
    public var mBgColor = Color.TRANSPARENT
    public var mWaveColor = Color.WHITE

    public var mRadiusMin = 0f
    public var mRadiusMax = 1080f
    public var mWaveInterval = 240f
    //用每次擴散半徑增加的值作為速度引數
    public var speed = 10f

    private val mWaveList = ArrayList<Float>()

    private val timeline = object:CountDownTimer(300000,16){

        override fun onTick(millisUntilFinished: Long) {

            // 每個時間間隔都把正在擴散的波紋半徑增加
            for (i in 0 until mWaveList.size){
                mWaveList[i] = mWaveList[i] + speed
                if (mWaveList[i] < mRadiusMin + mWaveInterval){
                    break
                }
            }

            //最外層波紋超過最大值時,重新把它新增到波紋佇列末尾
            if (mWaveList[0] > mRadiusMax){
                mWaveList[0] = mRadiusMin
                val newList = transList(mWaveList)
                mWaveList.clear()
                mWaveList.addAll(newList)
            }

            invalidate()
        }

        override fun onFinish() {
            //儘量保證手動呼叫waveStop,就不會執行到這裡
            reset()
        }
    }

    init {
        initWave(mWaveList)
        paint.color = mWaveColor
    }


    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas!!.drawColor(mBgColor)
        val centerX = canvas.width.div(2).toFloat()
        val centerY = canvas.height.div(2).toFloat()

        for (i in 0 until mWaveList.size){
            paint.alpha = calcAlpha(mWaveList[i])
            canvas.drawCircle(centerX, centerY, mWaveList[i], paint)
        }
    }

    fun wave(){
        timeline.start()
    }

    fun stopWave(){
        timeline.cancel()
    }

    fun reset(){
        timeline.cancel()
        mWaveList.clear()
        initWave(mWaveList)
    }

    private fun initWave(waveList: ArrayList<Float>){
        val waveNum = ((mRadiusMax-mRadiusMin)/mWaveInterval).toInt() + 2
        for (i in 1..waveNum){
            waveList.add(mRadiusMin)
        }
    }

    private fun transList(list: ArrayList<Float>):ArrayList<Float>{
        val newList = ArrayList<Float>()

        (1 until list.size).mapTo(newList) { list[it] }
        newList.add(list[0])
        return newList
    }

    // 通過半徑計算透明度,趨勢是半徑越大越透明,直到看不見
    private fun calcAlpha(r:Float):Int = ((mRadiusMax - r)/ mRadiusMax * 120).toInt()
}

複製程式碼

最終得到的就是這樣的一個View啦,使用方法大概是這樣的:

  1. 在xml中放一個WaveView
  2. findViewById之後,set基本引數
  3. 在需要的時機執行wave()方法

四、後記

做成這樣應該可以滿足設計師dalao們的需求了,呼~

自定義View是一個涵蓋內容很廣泛的課題,除了完全實現一個可展示、可互動的控制元件,還能優化佈局的繪製效率,實現迷之互動,以及這特別的動畫效果,需要深入學習的內容還有很多呀。

近期在惡補Kotlin,打算進入實踐了,所以寫Demo的時候都用的Kotlin。但是目前的專案都是Java的,所以又翻譯了一版Java的。完整程式碼見Github吧。

前往github

如有問題或是建議,歡迎留言評論。

相關文章