Android 自定義球型水波紋,帶圓弧進度

需求

如下,實現一個圓形水波紋,帶進度,兩層水波紋需要漸變顯示,且外圍有一個圓弧進度。

思路

外圍圓弧進度:可以通過canvas.drawArc()實現。由於圓弧需要實現漸變,可以通過給畫筆設置shader(SweepGradient)渲染,爲了保證圓弧起始的顏色值始終一致,需要動態調整shader的參數。具體參見

SweepGradient(centerX.toFloat(), centerY.toFloat(), circleColors[0], floatArrayOf(0f, value / 100f))

第四個參數需要根據當前進度填寫對應數據比例。不懂的同學可以自行百度查閱。

水波紋的實現:直接使用貝塞爾曲線Path.quadTo()實現,通過拉伸水平直線繪製波浪效果。可以通過控制拉伸點(waveAmplitude)距離水平線的高度,達到波浪高度的控制。至於波浪的移動,可以通過移動平移水平線的起始位置來實現,在使用動畫循環即可,爲了能夠穩定的顯示,繪製波浪時需要嚴格繪製整數倍週期的波浪。

園形的實現:繪製一個完整的圓形,然後通過Path.op()合併裁剪水波紋path。注意點就是Android6有個坑,使用該方法會有明顯的抖動,爲了解決該問題,我的做法是多畫一層圓弧以掩蓋此抖動。

生命週期的控制:爲了減少某些時刻CPU的損耗,通過控制變量自定義lifeDelegate(基於kotlin的代理模式實現)來控制動畫的開始暫停。由於筆者使用的框架基於MVVM,所以代碼就沒有使用attrs控制屬性,這裏就不做過多的修改了。

整體實現

class WaveView(context: Context, attributeSet: AttributeSet? = null) : View(context, attributeSet) {

    companion object {
        const val RESUME = 0x1
        const val STOP = 0x2
        const val DESTROY = 0x3
    }

    private var mWidth = 0 //控件整體寬度
    private var mHeight = 0 //控件整體高度

    //控件中心位置,x,y座標
    private var centerX = 0
    private var centerY = 0

    private var outerRadius = 0//外圈圓環的半徑
    private var innerRadius = 250f//內部圓圈的半徑
    private var radiusDist = 50f//內外圓圈的半徑差距

    private var fWaveShader: LinearGradient? = null
    private var sWaveShader: LinearGradient? = null

    private var wavePath = Path()
    private var waveCirclePath = Path()
    private val waveNum = 2

    //波浪的漸變顏色數組
    private val waveColors by lazy {
        arrayListOf(
                //深紅色
                intArrayOf(Color.parseColor("#E8E6421A"), Color.parseColor("#E2E96827")),
                intArrayOf(Color.parseColor("#E8E6421A"), Color.parseColor("#E2F19A7F")),
                //橙色
                intArrayOf(Color.parseColor("#E8FDA085"), Color.parseColor("#E2F6D365")),
                intArrayOf(Color.parseColor("#E8FDA085"), Color.parseColor("#E2F5E198")),
                //綠色
                intArrayOf(Color.parseColor("#E8009EFD"), Color.parseColor("#E22AF598")),
                intArrayOf(Color.parseColor("#E8009EFD"), Color.parseColor("#E28EF0C6"))
        )
    }

    //外圍圓環的漸變色
    private val circleColors by lazy {
        arrayListOf(
                //深紅色
                intArrayOf(Color.parseColor("#FFF83600"), Color.parseColor("#FFF9D423")),
                //橙色
                intArrayOf(Color.parseColor("#FFFDA085"), Color.parseColor("#FFF6D365")),
                //綠色
                intArrayOf(Color.parseColor("#FF2AF598"), Color.parseColor("#FF009EFD"))
        )
    }

    private val wavePaint by lazy {
        val paint = Paint()
        paint.isAntiAlias = true
        paint.strokeWidth = 1f
        paint
    }
    //波浪高度比例
    private var waveWaterLevelRatio = 0f
    //波浪的振幅
    private var waveAmplitude = 0f
    //波浪最大振幅高度
    private var maxWaveAmplitude = 0f

    //外圍圓圈的畫筆
    private val outerCirclePaint by lazy {
        val paint = Paint()
        paint.strokeWidth = 20f
        paint.strokeCap = Paint.Cap.ROUND
        paint.style = Paint.Style.STROKE
        paint.isAntiAlias = true
        paint
    }

    private val outerNormalCirclePaint by lazy {
        val paint = Paint()
        paint.strokeWidth = 20f
        paint.color = Color.parseColor("#FFF2F3F3")
        paint.style = Paint.Style.STROKE
        paint.isAntiAlias = true
        paint
    }

    private val bgCirclePaint by lazy {
        val paint = Paint()
        paint.color = Color.parseColor("#FFF6FAFF")
        paint.style = Paint.Style.FILL
        paint.isAntiAlias = true
        paint
    }

    private val textPaint by lazy {
        val paint = Paint()
        paint.style = Paint.Style.FILL
        paint.textAlign = Paint.Align.CENTER
        paint.isFakeBoldText = true
        paint.isAntiAlias = true
        paint
    }

    private val ringPaint by lazy {
        val paint = Paint()
        paint.style = Paint.Style.STROKE
        paint.color = Color.WHITE
        paint.isAntiAlias = true
        paint
    }

    //外圍圓圈所在的矩形
    private val outerCircleRectf by lazy {
        val rectF = RectF()
        rectF.set(
                centerX - outerRadius + outerCirclePaint.strokeWidth,
                centerY - outerRadius + outerCirclePaint.strokeWidth,
                centerX + outerRadius - outerCirclePaint.strokeWidth,
                centerY + outerRadius - outerCirclePaint.strokeWidth
        )
        rectF
    }

    //外圍圓圈的顏色漸變器矩陣,用於從90度開啓漸變,由於線條頭部有個小圓圈會導致顯示差異,因此從88度開始繪製
    private val sweepMatrix by lazy {
        val matrix = Matrix()
        matrix.setRotate(88f, centerX.toFloat(), centerY.toFloat())
        matrix
    }

    //進度 0-100
    var percent = 0
        set(value) {
            field = value
            waveWaterLevelRatio = value / 100f
            //y = -4 * x2 + 4x拋物線計算振幅,水波紋振幅規律更加真實
            waveAmplitude =
                    (-4 * (waveWaterLevelRatio * waveWaterLevelRatio) + 4 * waveWaterLevelRatio) * maxWaveAmplitude
//            waveAmplitude = if (value < 50) 2f * waveWaterLevelRatio * maxWaveAmplitude else (-2 * waveWaterLevelRatio + 2) * maxWaveAmplitude
            val shader = when (value) {
                in 0..46 -> {
                    fWaveShader = LinearGradient(
                            0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio),
                            waveColors[0],
                            null, Shader.TileMode.CLAMP
                    )
                    sWaveShader = LinearGradient(
                            0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio),
                            waveColors[1],
                            null, Shader.TileMode.CLAMP
                    )
                    SweepGradient(
                            centerX.toFloat(),
                            centerY.toFloat(),
                            circleColors[0],
                            floatArrayOf(0f, value / 100f)
                    )
                }
                in 47..54 -> {
                    fWaveShader = LinearGradient(
                            0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio),
                            waveColors[2],
                            null, Shader.TileMode.CLAMP
                    )
                    sWaveShader = LinearGradient(
                            0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio),
                            waveColors[3],
                            null, Shader.TileMode.CLAMP
                    )
                    SweepGradient(
                            centerX.toFloat(),
                            centerY.toFloat(),
                            circleColors[1],
                            floatArrayOf(0f, value / 100f)
                    )
                }
                else -> {

                    fWaveShader = LinearGradient(
                            0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio),
                            waveColors[4],
                            null, Shader.TileMode.CLAMP
                    )
                    sWaveShader = LinearGradient(
                            0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio),
                            waveColors[5],
                            null, Shader.TileMode.CLAMP
                    )
                    SweepGradient(
                            centerX.toFloat(),
                            centerY.toFloat(),
                            circleColors[2],
                            floatArrayOf(0f, value / 100f)
                    )
                }
            }
            shader.setLocalMatrix(sweepMatrix)
            outerCirclePaint.shader = shader
            invalidate()
        }

    private val greedTip = "Greed Index"

    //文本的字體大小
    private var percentSize = 80f
    private var greedSize = 30f
    private var textColor = Color.BLACK

    //外圍圓圈的畫筆大小
    private var outerStrokeWidth = 10f

    private var fAnimatedValue = 0f
    private var sAnimatedValue = 0f
    //動畫
    private val fValueAnimator by lazy {
        val valueAnimator = ValueAnimator()
        valueAnimator.duration = 1500
        valueAnimator.repeatCount = ValueAnimator.INFINITE
        valueAnimator.interpolator = LinearInterpolator()
        valueAnimator.setFloatValues(0f, waveWidth)
        valueAnimator.addUpdateListener { animation ->
            fAnimatedValue = animation.animatedValue as Float
            invalidate()
        }
        valueAnimator
    }
    private val sValueAnimator by lazy {
        val valueAnimator = ValueAnimator()
        valueAnimator.duration = 2000
        valueAnimator.repeatCount = ValueAnimator.INFINITE
        valueAnimator.interpolator = LinearInterpolator()
        valueAnimator.setFloatValues(0f, waveWidth)
        valueAnimator.addUpdateListener { animation ->
            sAnimatedValue = animation.animatedValue as Float
            invalidate()
        }
        valueAnimator
    }

    //一小段完整波浪的寬度
    private var waveWidth = 0f

    var lifeDelegate by Delegates.observable(0) { _, old, new ->
        when (new) {
            RESUME -> onResume()
            STOP -> onPause()
            DESTROY -> onDestroy()
        }
    }

    //設置中間進度文本的字體大小
    fun setPercentSize(size: Float) {
        percentSize = size
        invalidate()
    }

    //設置中間提示文本的字體大小
    fun setGreedSize(size: Float) {
        greedSize = size
        invalidate()
    }

    //設置文本顏色
    fun setTextColor(color: Int) {
        textColor = color
        textPaint.color = textColor
        invalidate()
    }

    //設置外圍圓圈的寬度
    fun setOuterStrokeWidth(width: Float) {
        outerStrokeWidth = width
        outerCirclePaint.strokeWidth = outerStrokeWidth
        outerNormalCirclePaint.strokeWidth = outerStrokeWidth
        invalidate()
    }

    //設置內圓半徑
    fun setInnerRadius(radius: Float) {
        innerRadius = radius
        invalidate()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mWidth = width - paddingStart - paddingEnd
        mHeight = height - paddingTop - paddingBottom
        centerX = mWidth / 2
        centerY = mHeight / 2
        outerRadius = mWidth.coerceAtMost(mHeight) / 2
        radiusDist = outerRadius - innerRadius
        waveWidth = mWidth * 1.8f
        maxWaveAmplitude = mHeight * 0.15f
    }

    private fun onResume() {
        if (fValueAnimator.isStarted) {
            animatorResume()
        } else {
            fValueAnimator.start()
            sValueAnimator.start()
        }
    }

    private fun animatorResume() {
        if (fValueAnimator.isPaused || !fValueAnimator.isRunning) {
            fValueAnimator.resume()
        }
        if (sValueAnimator.isPaused || !sValueAnimator.isRunning) {
            sValueAnimator.resume()
        }
    }

    private fun onPause() {
        if (fValueAnimator.isRunning) {
            fValueAnimator.pause()
        }
        if (sValueAnimator.isRunning) {
            sValueAnimator.pause()
        }
    }

    private fun onDestroy() {
        fValueAnimator.cancel()
        sValueAnimator.cancel()
    }

    //當前窗口銷燬時,回收動畫資源
    override fun onDetachedFromWindow() {
        onDestroy()
        super.onDetachedFromWindow()
    }

    override fun onDraw(canvas: Canvas) {
        drawCircle(canvas)
        drawWave(canvas)
        drawText(canvas)
    }

    private fun drawWave(canvas: Canvas) {
        //波浪當前高度
        val level = (1 - waveWaterLevelRatio) * innerRadius * 2 + radiusDist
        //繪製所有波浪
        for (num in 0 until waveNum) {
            //重置path
            wavePath.reset()
            waveCirclePath.reset()
            var startX = if (num == 0) {//第一條波浪的起始位置
                wavePath.moveTo(-waveWidth + fAnimatedValue, level)
                -waveWidth + fAnimatedValue
            } else {//第二條波浪的起始位置
                wavePath.moveTo(-waveWidth + sAnimatedValue, level)
                -waveWidth + sAnimatedValue
            }
            while (startX < mWidth + waveWidth) {
                wavePath.quadTo(
                        startX + waveWidth / 4,
                        level + waveAmplitude,
                        startX + waveWidth / 2,
                        level
                )
                wavePath.quadTo(
                        startX + waveWidth / 4 * 3,
                        level - waveAmplitude,
                        startX + waveWidth,
                        level
                )
                startX += waveWidth
            }
            wavePath.lineTo(startX, mHeight.toFloat())
            wavePath.lineTo(0f, mHeight.toFloat())
            wavePath.close()

            waveCirclePath.addCircle(
                    centerX.toFloat(),
                    centerY.toFloat(),
                    innerRadius,
                    Path.Direction.CCW
            )
            waveCirclePath.op(wavePath, Path.Op.INTERSECT)
            //繪製波浪漸變色
            wavePaint.shader = if (num == 0) {
                sWaveShader
            } else {
                fWaveShader
            }
            canvas.drawPath(waveCirclePath, wavePaint)
        }

        //Fixme android6設置Path.op存在明顯抖動,因此多畫一圈圓環
        val ringWidth = outerRadius - outerStrokeWidth - innerRadius
        ringPaint.strokeWidth = ringWidth / 2
        canvas.drawCircle(centerX.toFloat(), centerY.toFloat(), innerRadius + ringWidth / 4, ringPaint)
    }

    private fun drawText(canvas: Canvas) {
        //繪製進度文字
        textPaint.isFakeBoldText = true
        textPaint.textSize = percentSize
        canvas.drawText(
                percent.toString(),
                centerX.toFloat(),
                centerY.toFloat() + textPaint.textSize / 2,
                textPaint
        )
        textPaint.isFakeBoldText = false
        textPaint.textSize = greedSize
        canvas.drawText(
                greedTip,
                centerX.toFloat(),
                centerY.toFloat() - textPaint.textSize * 2,
                textPaint
        )
    }

    private fun drawCircle(canvas: Canvas) {
        //繪製外圍進度圓圈
        canvas.drawArc(outerCircleRectf, 0f, 360f, false, outerNormalCirclePaint)
        canvas.drawArc(outerCircleRectf, 90f, percent * 3.6f, false, outerCirclePaint)
        canvas.drawCircle(
                centerX.toFloat(),
                centerY.toFloat(),
                innerRadius,
                bgCirclePaint
        )
    }
}

總結

整體實現不會很複雜,都很好理解。對你有幫助的點個贊鼓勵下,謝謝啦~~~

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