之前整理過一些屬性動畫的基本操作,這一段時間的動畫相關需求都安然度過了。直到這次……
一、另一種動畫需求
多數互動中的動畫都是讓單個頁面元素動起來,這種就很適合用屬性動畫實現。但是對於 多個元素、非頁面內元素 的動畫需求,就不方便用View+屬性動畫實現了。
舉個例子,也就是這次做的:
波紋效果需要同時繪製 多個 同心圓,而且這些圓 不是頁面內的元素,未觸發之前不需要顯示。如果用屬性動畫實現,至少需要在xml佈局檔案中新增多個ImageView畫圓,效率低還不易複用。
二、WaveView分析
使用自定義View繪製動畫的主要思路是這樣的:
- 分解動畫成幀,考慮如何在
onDraw
中繪製每一幀 - 提取出繪製所需引數,分為隨時間變化和不可變兩種,不可變引數可以暴露出去(setter方法/attribute設定)
- 總結隨時間變化的引數變化規律,實現時間軸
- 按需求提供出播放,暫停,停止,重置等等方法
按照這個思路,一步步實現一下WaveView吧。
第一步:
在onDraw
中畫圓需要用到canvas.drawCircle
方法,四個引數:圓心x、y座標,半徑和Paint。WaveView繪製的每一幀都是圓,區別是圓的半徑,數量,透明度。還需要設定圓的最小半徑和最大半徑,以及擴散的時候兩個圓的半徑差。
每次繪製只需要把所有的圓畫出來:
第二步:
擴散過程中隨時間變化的引數只有半徑和透明度。圓的數量,最小最大半徑和半徑差則是不可變引數。所有的都加了預設值,防止
第三步:
最艱難的一步,時間軸。我們需要一個定時觸發的機制去改變mWaveList
中的每一個值,還要同時修改paint
的alpha值。這個變數嘛,還是越少越方便,波紋的效果是半徑越大alpha越小,只要控制半徑變化就可以計算出alpha。然後便是迴圈的問題,當波紋半徑大於最大半徑波紋就會消失,此時便可迴圈使用,再從最小半徑擴散一次。
我這裡的時間軸用了CountDownTimer
,應該不是一種很好的選擇,只是個人習慣寫起來順手了…
第四步:
最後這個就簡單了,直接控制CountDownTimer
的start和cancel就可以了。
三、再完善一下?
內部功能已經實現了,還要提取引數的設定方法,方便其他地方使用呀。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啦,使用方法大概是這樣的:
- 在xml中放一個WaveView
- findViewById之後,set基本引數
- 在需要的時機執行
wave()
方法
四、後記
做成這樣應該可以滿足設計師dalao們的需求了,呼~
自定義View是一個涵蓋內容很廣泛的課題,除了完全實現一個可展示、可互動的控制元件,還能優化佈局的繪製效率,實現迷之互動,以及這特別的動畫效果,需要深入學習的內容還有很多呀。
近期在惡補Kotlin,打算進入實踐了,所以寫Demo的時候都用的Kotlin。但是目前的專案都是Java的,所以又翻譯了一版Java的。完整程式碼見Github吧。
如有問題或是建議,歡迎留言評論。