[Android]多层波纹扩散动画——自定义View绘制
之前整理过一些属性动画的基本操作,这一段时间的动画相关需求都安然度过了。直到这次……
一、另一种动画需求
多数交互中的动画都是让单个页面元素动起来,这种就很适合用属性动画实现。但是对于 多个元素、非页面内元素 的动画需求,就不方便用View+属性动画实现了。
举个例子,也就是这次做的:
![[Android]多层波纹扩散动画——自定义View绘制](https://static.blogweb.cn/article/3595a670bf8a43d08bdf0d31d7eb44cf.webp) 
波纹效果需要同时绘制 多个 同心圆,而且这些圆 不是页面内的元素,未触发之前不需要显示。如果用属性动画实现,至少需要在xml布局文件中添加多个ImageView画圆,效率低还不易复用。
二、WaveView分析
使用自定义View绘制动画的主要思路是这样的:
- 分解动画成帧,考虑如何在onDraw中绘制每一帧
- 提取出绘制所需参数,分为随时间变化和不可变两种,不可变参数可以暴露出去(setter方法/attribute设置)
- 总结随时间变化的参数变化规律,实现时间轴
- 按需求提供出播放,暂停,停止,重置等等方法
按照这个思路,一步步实现一下WaveView吧。
第一步:
在onDraw中画圆需要用到canvas.drawCircle方法,四个参数:圆心x、y坐标,半径和Paint。WaveView绘制的每一帧都是圆,区别是圆的半径,数量,透明度。还需要设置圆的最小半径和最大半径,以及扩散的时候两个圆的半径差。
每次绘制只需要把所有的圆画出来:
![[Android]多层波纹扩散动画——自定义View绘制](https://static.blogweb.cn/article/8a8f558bb619400586392fbc26a3931f.webp) 
第二步: 扩散过程中随时间变化的参数只有半径和透明度。圆的数量,最小最大半径和半径差则是不可变参数。所有的都加了默认值,防止
![[Android]多层波纹扩散动画——自定义View绘制](https://static.blogweb.cn/article/755d943efc8d44f98a28ced785a95c33.webp) 
第三步:
最艰难的一步,时间轴。我们需要一个定时触发的机制去改变mWaveList中的每一个值,还要同时修改paint的alpha值。这个变量嘛,还是越少越方便,波纹的效果是半径越大alpha越小,只要控制半径变化就可以计算出alpha。然后便是循环的问题,当波纹半径大于最大半径波纹就会消失,此时便可循环使用,再从最小半径扩散一次。
我这里的时间轴用了CountDownTimer,应该不是一种很好的选择,只是个人习惯写起来顺手了…
![[Android]多层波纹扩散动画——自定义View绘制](https://static.blogweb.cn/article/e11b2509625645bc886a777e5841b921.webp) 
第四步:
最后这个就简单了,直接控制CountDownTimer的start和cancel就可以了。
![[Android]多层波纹扩散动画——自定义View绘制](https://static.blogweb.cn/article/9438f49f84104e0db689eebe4f61cfa5.webp) 
三、再完善一下?
内部功能已经实现了,还要提取参数的设置方法,方便其他地方使用呀。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吧。
如有问题或是建议,欢迎留言评论。
转载自:https://juejin.cn/post/6844903560841265159




