进度条天花板-自定义

感谢

在新年到来之际,我想借此机会向大家送上一份特别的礼物------一篇我尚未发版的小册内容。这个小册是我用心筹备、白天黑夜着急与大家见面,希望它能够为大家带来启发和帮助。值此佳节,我衷心祝愿大家和家人身体健康,幸福美满。小册的诞生离不开大家的支持和关注,希望大家能够持续关注,并在需要时轻松地找到小册,与我一同探索知识的海洋。在写作的道路上,有你们的陪伴和鼓励,是我不断前行的动力源泉。让我们一起携手前行,共同创造更美好的明天!

回顾

在上节小册,我们深入学习了裁剪API,并编写了三个实用案例。本节课的目标是通过拟物图片进行实现,将我们最近学到的知识进行融合和复习,以拓展视野、深化理解。只有将不同的知识点相互结合,才能更灵活地运用自定义功能,发挥其最大的潜力。

下面展示了一系列华丽的进度条样式,先看看实现有无思路。在本节课中,让我们逐一实现以下效果图,探索自定义进度条的高度所在。

立体进度条

如下图:比较有立体感的进度条,底部是一个有凹陷程度的进度渠。上方是一个两端弧度渐变绿色进度条,且进度条有阴影立体感,顶部是一个进度文字的显示放置在一个不规则的区域内部,此区域也具有立体感。

1、进度渠绘制

两端头是圆弧的区域,回想当初画笔有类似的属性strokeCap = Cap.ROUND可以让其端头带有弧形帽子。阴影部分我们尝试y方向向上和向下进行阴影投射。代码和结果如下:

改变阴影投射的方向为向下:

就上面图,我们似乎已经偏离了正常轨道,阴影部分很难像案例一样显示在顶部边缘且向下映射到进度渠内侧。大家试想一下,如何才能够让其阴影部分投射到顶部内侧呢?所有复杂的图,都可以通过分层解决,PS是就是通过不同的图层来解决复杂的事情。我们阴影部分处理同样也可以类似处理,底部我们只需要绘制进度渠背景,阴影部分我们在其上层单独绘制。

我们在进度渠顶部绘制阴影部分,设置阴影部分线段宽度为0避免过粗,影响美感。代码效果如下:

kotlin 复制代码
package com.example.draw_android.section05_canvas_clip.b_canvas_clip_examples
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Paint.Cap
import android.util.AttributeSet
import android.view.View


class ProgressBarView : View {
    private var margin: Float = 250f
    private var progressBarHeight: Float = 50f
    private var progressBgOutPaint = Paint()
    private var progressShadowOutPaint = Paint()



    constructor(context: Context) : super(context) {
        init()
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init()
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        init()
    }

    private fun init() {
        progressShadowOutPaint.apply {
 color = Color.parseColor("#CAC7C6")
            style = Paint.Style.STROKE
setShadowLayer(
                6f,
                0f,
                6f,
                Color.BLACK
)
            strokeWidth = 0f
            strokeCap = Cap.ROUND
isAntiAlias = true
        }

progressBgOutPaint.apply {
 color = Color.parseColor("#CAC7C6")
            style = Paint.Style.FILL
strokeWidth = progressBarHeight
            strokeCap = Cap.ROUND
isAntiAlias = true
        }
}

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.translate(0f, height / 2f)
        //绘制进度渠
        canvas.drawLine(
            margin,
            0f,
            width.toFloat() - margin,
            0f,
            progressBgOutPaint
        )
        //绘制阴影
        canvas.drawLine(
            margin,
            0f-progressBarHeight/2f,
            width.toFloat() - margin,
            0f-progressBarHeight/2f,
            progressShadowOutPaint
        )
    }
}

对于中间部分有了阴影部分,但是进度两端弧度部分用一条直线段是无法解决的。我们需要一条两端都有弧度曲线进行绘制阴影。因为进度渠道是用paint自带的弧度帽解决的,需要知道一般的帽子宽度等于线宽度的一半,在结尾处绘制一个半径为高度一半的圆圈,其圆圈和线帽并不是完全重合的,记得早期专门尝试过,是有一点儿差距的,几乎可以吻合。如下面示意图:

Cap部分差不多是一个半圆,其半径是线高度的一半,不难计算出其外层弧度部分的曲线。用二阶曲线便可以绘制出来,因为c1是控制点,且a和b都是圆四分之一,c1作为控制点就可以绘制出四分之一圆的曲线,当然大家可以动手尝试。不难得到a->c1->b之间的路径如下代码中的左侧部分:

scss 复制代码
shadowPath.reset()
shadowPath.apply {
moveTo(margin - progressBarHeight / 2f, 0f)
    //左侧部分
    quadTo(
        margin - progressBarHeight / 2f,//c1控制点的横坐标
        0f - progressBarHeight / 2f,//c1控制点的纵坐标
        margin,//b点的横坐标
        0f - progressBarHeight / 2f//b点的纵坐标
    )
    lineTo(width.toFloat() - margin, 0f - progressBarHeight / 2f)
    //右侧部分,不明白最好画图。确定此时画布的位置。
    quadTo(
        width.toFloat() - margin + progressBarHeight / 2f,
        0f - progressBarHeight / 2f,
        width.toFloat() - margin + progressBarHeight/2f,
        0f
    )
}
//绘制阴影
canvas.drawPath(shadowPath, progressShadowOutPaint)

效果如下:远看的确还行吧

放大看细节,不难发现我们绘制的路径和线帽并不重合。说明线帽的弧度并达不到圆的弧度。那应该如何解决呢?这个大家可以大胆的想象一下自己有哪些解决方案。

在思考这个问题时,有些同学可能会尝试通过改变控制点来缩小差距。另外一些同学可能会考虑使用二阶曲线来绘制底部曲线部分,还有一些同学可能会直接想到裁剪一个小区域即可。实际上,这三种方法都是可行的,关键是能够实现效果。

目前,我选择通过略微调整控制点的位置来实现。具体来说,我将控制点向内缩进了2个像素。

scss 复制代码
val marginOut = 2
shadowPath.apply {
moveTo(margin - progressBarHeight / 2f, 0f)
    quadTo(
        margin - progressBarHeight / 2f+marginOut,//c1控制点的横坐标
        0f - progressBarHeight / 2f+marginOut,//c1控制点的纵坐标
        margin,//b点的横坐标
        0f - progressBarHeight / 2f//b点的纵坐标
    )
    lineTo(width.toFloat() - margin, 0f - progressBarHeight / 2f)
    //右侧部分,不明白最好画图。确定此时画布的位置。
    quadTo(
        width.toFloat() - margin + progressBarHeight / 2f-marginOut,
        0f - progressBarHeight / 2f+marginOut,
        width.toFloat() - margin + progressBarHeight/2f,
        0f
    )
} 

同样的看看底部进度渠有一个亮度的阴影向上的。会了上面的path计算应该不算太难吧,用同上的道理进行绘制阴影,一定记得画图,能事半功倍。

scss 复制代码
//绘制下边缘的阴影
shadowBottomPath.reset()
shadowBottomPath.apply {
moveTo(margin - progressBarHeight / 2f, 0f)
    quadTo(
        margin - progressBarHeight / 2f + marginOut,
        0f + progressBarHeight / 2f - marginOut,
        margin,
        0f + progressBarHeight / 2f//b点的纵坐标
    )
    lineTo(width.toFloat() - margin, 0f + progressBarHeight / 2f)
    //右侧部分,不明白最好画图。确定此时画布的位置。
    quadTo(
        width.toFloat() - margin + progressBarHeight / 2f - marginOut,
        0f + progressBarHeight / 2f - marginOut,
        width.toFloat() - margin + progressBarHeight / 2f,
        0f
    )
}
//绘制阴影
canvas.drawPath(shadowBottomPath, progressBottomShadowOutPaint)

效果如下:

绘制结果,远看似乎还可以,当放大之后可以发现交界部分还是有所瑕疵的,对于这点大家想想如何解决呢?。

既然我们已经能够绘制出顶部和底部部分的曲线,那么很简单就能得到整个边缘区域的路径。我们可以通过绘制一条背景边缘,覆盖整个边缘路径,从而解决这个问题。由于上下部分都已绘制完成,我们只需将它们连接起来,然后绘制一条覆盖线条即可。

scss 复制代码
overShadowPath.reset()
overShadowPath.apply {
moveTo(margin - progressBarHeight / 2f, 0f)
    quadTo(
        margin - progressBarHeight / 2f + marginOut,//c1控制点的横坐标
        0f - progressBarHeight / 2f + marginOut,//c1控制点的纵坐标
        margin,//b点的横坐标
        0f - progressBarHeight / 2f//b点的纵坐标
    )
    lineTo(width.toFloat() - margin, 0f - progressBarHeight / 2f)
    //右侧部分,不明白最好画图。确定此时画布的位置。
    quadTo(
        width.toFloat() - margin + progressBarHeight / 2f - marginOut,
        0f - progressBarHeight / 2f + marginOut,
        width.toFloat() - margin + progressBarHeight / 2f,
        0f
    )

    //右侧部分,不明白最好画图。确定此时画布的位置。
    quadTo(
        width.toFloat() - margin + progressBarHeight / 2f - marginOut,
        0f + progressBarHeight / 2f - marginOut,
        width.toFloat() - margin,
        progressBarHeight / 2f
    )
    lineTo(width.toFloat() - margin, 0f + progressBarHeight / 2f)
    lineTo(margin, 0f + progressBarHeight / 2f)
    quadTo(
        margin - progressBarHeight / 2f + marginOut,//c1控制点的横坐标
        progressBarHeight / 2f - marginOut,//c1控制点的纵坐标
        margin - progressBarHeight / 2f,
        0f
    )
    close()
}
//覆盖部分需要放在最底部,也就是上下边缘绘制的上层,达到覆盖消除交界问题。
canvas.drawPath(overShadowPath, progressOutPathPaint)

效果如下,到这里基本实现了进度渠的绘制。

我们网络拿一张金属材质的图,作为背景再看看效果,结果还是很不错的。

既然得到了边缘区域的路径,我们可以在绘制之前进行裁剪,避免绘制阴影等画出进度区域外边。需要理解,裁剪一般是为了限制绘制区域,所以裁剪需要放到前面,覆盖部分需要放到最后。

2、进度条

进度条部分,我们通过Cap进行实现,既然已经知道了进度条的渠道,那么进度条也不是算有什么难度,仔细分析进度条有渐变。颜色可以通过自带颜色获取工具进行获取,因为颜色渐变是从上往下的,所以进行获取不同阶段的颜色进行设置。

ini 复制代码
progressPaint.apply {
 color = Color.parseColor("#A9B88F")
    style = Paint.Style.STROKE
shader = LinearGradient(
        0f, -progressBarHeight /2f, 0f,
        progressBarHeight /2f,
        intArrayOf(Color.parseColor("#E2EDD2"), Color.parseColor("#A2B289"), Color.parseColor("#7E8E6A")),
        floatArrayOf(0f, 0.4f, 0.8f),
        Shader.TileMode.CLAMP
)
    strokeWidth = progressBarHeight - 6
    strokeCap = Cap.ROUND
isAntiAlias = true
}

//绘制进度条。
canvas.drawLine(
    margin-2f,
    0f,
    margin-2f+230f,
    0f,
    progressPaint
)

看着缺少点啥,阴影部分是需要单独一层去设置的,渐变和阴影同时设置是不起作用的。所以再来一层作为阴影层,一般放置在绘制进度之前比较好,阴影在进度下方。

ini 复制代码
progressShadowPaint.apply {
 color = Color.parseColor("#A9B88F")
    style = Paint.Style.STROKE
setShadowLayer(
        10f,
        0f,
        10f,
        Color.parseColor("#2B2B2B")
    )
    strokeWidth = progressBarHeight - 6
    strokeCap = Cap.ROUND
isAntiAlias = true
}
//绘制进度条阴影
canvas.drawLine(
    margin-2f,
    0f,
    margin-2f+230f,
    0f,
    progressShadowPaint
)
//绘制进度条。
canvas.drawLine(
    margin-2f,
    0f,
    margin-2f+230f,
    0f,
    progressPaint
)

结果如下:

但是大家需要明白,作为一个开源控件,需要提供给外部进度,然后让其根据设置变化。首先进度条绘制区域的长度我们是可以知道的,即val totalProgressWidth = (width - margin2),我们提供给外部接口进度设置progressScale范围在0f-1f,0f代表进度为0,1f代表进度为100%。不难计算实际进度长度 = totalProgressWidthprogressScale。

kotlin 复制代码
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
    context,
    attrs,
    defStyleAttr
) {
    val array: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressBarView)
    //获取customBackground 属性的值【可能是图片可能是颜色】
    progressScale = array.getFloat(R.styleable.ProgressBarView_progressScale, 0f)
    array.recycle()
    init()
}

fun setProgressScale(progressScale: Float) {
    startAnimal(progressScale)
}
private var animator: ValueAnimator? = null

private fun startAnimal(starAnimal: Float) {
    animator?.cancel()
    // 设置弹性动画
    animator = ValueAnimator.ofFloat(
        0f,
        starAnimal
    )
    animator?.apply {
 duration = 1000
        interpolator = DecelerateInterpolator()
        addUpdateListener { animation ->
progressScale = animation.animatedValue as Float
            invalidate()
        }
start()
    }
}

val totalProgressWidth = (width - margin * 2)
val progressWidth = totalProgressWidth * min(progressScale, 1f)
//绘制进度条阴影
canvas.drawLine(
    margin - 2f,
    0f,
    margin - 2f + progressWidth,
    0f,
    progressShadowPaint
)
//绘制进度条。
canvas.drawLine(
    margin - 2f,
    0f,
    margin - 2f + progressWidth,
    0f,
    progressPaint
)

效果如下:

3、文字绘制

这节课,重点就是培养的对画布变换的基础上画草稿图,只有这样,你所绘制的内容才会事半功倍。文字部分,可以看到很多边角都是有弧度的,所以这部分大家自己绘制,主要锻炼大家对二阶曲线的熟练度。这部分我就不细细分析过程了,留给你们自己,属性画布变换和根据草稿实现效果。当然大家可以看我写的源码,下面贴完所有的源码。

kotlin 复制代码
class ProgressBarView : View {
    //...
    //设置动画,由动画控制其进度条。比较丝毫
    fun setProgressScale(progressScale: Float) {
        startAnimal(minOf(progressScale,1f))
    }

    private var animator: ValueAnimator? = null

    private fun startAnimal(starAnimal: Float) {
        animator?.cancel()
        // 设置弹性动画
        animator = ValueAnimator.ofFloat(
            0f,
            starAnimal
        )
        animator?.apply {
            duration = 1000 //时间1秒,当然你可以设置一个属性提供给开发着属性变量
            interpolator = DecelerateInterpolator()
            addUpdateListener { animation ->
                progressScale = animation.animatedValue as Float
                invalidate()
            }
            start()
        }
    }

    private fun init(attrs: AttributeSet?) {
        if (attrs != null) {
            val array: TypedArray =
                context.obtainStyledAttributes(attrs, R.styleable.ProgressBarView)
            //获取customBackground 属性的值【可能是图片可能是颜色】
            progressScale = array.getFloat(R.styleable.ProgressBarView_progressScale, 0f)
            array.recycle()
        }
        bitmap = BitmapFactory.decodeResource(resources, R.drawable.img_jinshu)

        progressTopShadowOutPaint.apply {
            color = Color.BLACK
            style = Paint.Style.STROKE
            setShadowLayer(
                6f,
                0f,
                6f,
                Color.BLACK
            )
            strokeWidth = 0f
            strokeCap = Cap.ROUND
            isAntiAlias = true
        }
        progressBottomShadowOutPaint.apply {
            color = Color.WHITE
            style = Paint.Style.STROKE
            setShadowLayer(
                6f,
                0f,
                -6f,
                Color.WHITE
            )
            strokeWidth = 3f
            strokeCap = Cap.ROUND
            isAntiAlias = true
        }

        progressBgDitchPaint.apply {
            color = Color.parseColor("#B7B6B4")
            style = Paint.Style.FILL
            strokeWidth = progressBarHeight
            strokeCap = Cap.ROUND
            isAntiAlias = true
        }

        progressPaint.apply {
            color = Color.parseColor("#A9B88F")
            style = Paint.Style.STROKE
            shader = LinearGradient(
                0f, -progressBarHeight / 2f, 0f,
                progressBarHeight / 2f,
                intArrayOf(
                    Color.parseColor("#E2EDD2"),
                    Color.parseColor("#A2B289"),
                    Color.parseColor("#7E8E6A")
                ),
                floatArrayOf(0f, 0.4f, 0.8f),
                Shader.TileMode.CLAMP
            )
            strokeWidth = progressBarHeight - 6
            strokeCap = Cap.ROUND
            isAntiAlias = true
        }

        progressShadowPaint.apply {
            color = Color.parseColor("#A9B88F")
            style = Paint.Style.STROKE
            setShadowLayer(
                10f,
                0f,
                10f,
                Color.parseColor("#2B2B2B")
            )
            strokeWidth = progressBarHeight - 6
            strokeCap = Cap.ROUND
            isAntiAlias = true
        }

        progressOutPathPaint.apply {
            color = Color.parseColor("#B7B6B4")
            style = Paint.Style.STROKE
            strokeWidth = 3.5f
            strokeCap = Cap.ROUND
            isAntiAlias = true
        }
        textRectBgPaint.apply {
            color = Color.parseColor("#A9B88F")
            style = Paint.Style.FILL
            shader = LinearGradient(
                0f, 0f, 0f,
                textRectHeight,
                intArrayOf(
                    Color.parseColor("#F5F4F6"),
                    Color.parseColor("#D0CFD8"),
                    Color.parseColor("#B8B8C6")
                ),
                floatArrayOf(0f, 0.4f, 0.8f),
                Shader.TileMode.CLAMP
            )
            strokeWidth = progressBarHeight - 6
            strokeCap = Cap.ROUND
            isAntiAlias = true
        }
        textRectShadowPaint.apply {
            strokeWidth = 2f
            color = Color.parseColor("#B8B8C6")
            style = Paint.Style.STROKE
            setShadowLayer(
                6f,
                0f,
                6f,
                Color.BLACK
            )
        }
        textPaint.apply {
            textSize = 30f

            color = Color.parseColor("#514F50")
            style = Paint.Style.FILL
        }
        // 设置字体加粗
        textPaint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        //1、将画布平移到控件中心
        canvas.translate(0f, height / 2f)
        initOverShadowLine()

        //2、绘制进度渠
        canvas.drawLine(
            margin,
            0f,
            width.toFloat() - margin,
            0f,
            progressBgDitchPaint
        )


        //3、绘制上边缘的阴影
        shadowTopPath.reset()
        shadowTopPath.apply {
            moveTo(margin - progressBarHeight / 2f, 0f)
            quadTo(
                margin - progressBarHeight / 2f + marginOut,//c1控制点的横坐标
                0f - progressBarHeight / 2f + marginOut,//c1控制点的纵坐标
                margin,//b点的横坐标
                0f - progressBarHeight / 2f//b点的纵坐标
            )
            lineTo(width.toFloat() - margin, 0f - progressBarHeight / 2f)
            //右侧部分,不明白最好画图。确定此时画布的位置。
            quadTo(
                width.toFloat() - margin + progressBarHeight / 2f - marginOut,
                0f - progressBarHeight / 2f + marginOut,
                width.toFloat() - margin + progressBarHeight / 2f,
                0f
            )
        }
        //绘制阴影
        canvas.drawPath(shadowTopPath, progressTopShadowOutPaint)


        //4、绘制下边缘的阴影
        shadowBottomPath.reset()
        shadowBottomPath.apply {
            moveTo(margin - progressBarHeight / 2f, 0f)
            quadTo(
                margin - progressBarHeight / 2f + marginOut,
                0f + progressBarHeight / 2f - marginOut,
                margin,
                0f + progressBarHeight / 2f//b点的纵坐标
            )
            lineTo(width.toFloat() - margin, 0f + progressBarHeight / 2f)
            //右侧部分,不明白最好画图。确定此时画布的位置。
            quadTo(
                width.toFloat() - margin + progressBarHeight / 2f - marginOut,
                0f + progressBarHeight / 2f - marginOut,
                width.toFloat() - margin + progressBarHeight / 2f,
                0f
            )
        }
        //绘制阴影
        canvas.drawPath(shadowBottomPath, progressBottomShadowOutPaint)

        //5、覆盖部分需要放在最底部,也就是上下边缘绘制的上层,达到覆盖消除交界问题。
        canvas.drawPath(overShadowPath, progressOutPathPaint)

        val totalProgressWidth = (width - margin * 2)
        val progressWidth = totalProgressWidth * min(progressScale, 1f)
        //6、绘制进度条阴影,让其在进度条下方,显得真实,你试一试和进度绘制调换位置。
        canvas.drawLine(
            margin - 2f,
            0f,
            margin - 2f + progressWidth,
            0f,
            progressShadowPaint
        )
        //7、绘制进度条。
        canvas.drawLine(
            margin - 2f,
            0f,
            margin - 2f + progressWidth,
            0f,
            progressPaint
        )


        //8、绘制文字部分。画图画图,除非你可以有很长时间的空间想象力和记忆里。
        textRectPath.reset()
        val textTopCenterX = margin - 2f + progressWidth
        val textRectWidthGeneral = 50f
        val textRectWidthMin = textRectWidthGeneral - 20f
        val textRectCornerHeight =  9f
        val textRectCornerControlHeight =  11f
        canvas.save()
        canvas.translate(0f, -textRectHeight*2.4f)

        textRectPath.apply {
            moveTo(textTopCenterX, 0f)
            //绘制右上角
            lineTo(textTopCenterX + textRectWidthMin, 0f)
            quadTo(textTopCenterX + textRectWidthGeneral, 0f, textTopCenterX + textRectWidthGeneral, 10f)
            //绘制右下角
            lineTo(textTopCenterX + textRectWidthGeneral - 2, textRectHeight - 20f)

            quadTo(
                textTopCenterX + textRectWidthGeneral - 4,
                textRectHeight,
                textTopCenterX + textRectWidthMin,
                textRectHeight
            )
            lineTo(textTopCenterX + textRectWidthMin, textRectHeight)
            lineTo(textTopCenterX + 10f, textRectHeight)

            quadTo(textTopCenterX+5, textRectHeight, textTopCenterX+2, textRectHeight+textRectCornerHeight)
            lineTo(textTopCenterX+2,textRectHeight+textRectCornerHeight)

            //这里是底部最高点
            quadTo(textTopCenterX,textRectHeight+textRectCornerControlHeight,textTopCenterX-2,textRectHeight+textRectCornerHeight)
            lineTo(textTopCenterX-2,textRectHeight+textRectCornerHeight)

            quadTo(textTopCenterX-5, textRectHeight, textTopCenterX - textRectCornerControlHeight, textRectHeight)
            lineTo(textTopCenterX - textRectWidthMin, textRectHeight)
            quadTo(
                textTopCenterX - textRectWidthGeneral,
                textRectHeight,
                textTopCenterX - textRectWidthGeneral,
                textRectHeight - 20f
            )
            lineTo(textTopCenterX - textRectWidthGeneral - 4, 10f)
            quadTo(textTopCenterX - textRectWidthGeneral - 2, 0f, textTopCenterX - textRectWidthMin, 0f)
            close()
        }


        canvas.save()
        //稍作调整画布位置,先绘制阴影部分。
        canvas.translate(0f,-2f)
        canvas.drawPath(textRectPath, textRectShadowPaint)
        //绘制文字框背景之前赶紧恢复画布,让其绘制区域可以覆盖住阴影部分的带有颜色的边缘线
        canvas.restore()
        canvas.drawPath(textRectPath, textRectBgPaint)
        //绘制进度文字
        val formattedNumber = "%.0f".format(progressScale*100)
        val textContent = "$formattedNumber%"
        val rectContent = Rect()
        textPaint.getTextBounds(
            textContent,
            0,
            textContent.length,
            rectContent
        )
        val textWidth =rectContent.width()
        val textHeight = rectContent.height()
        //画图,加减计算。没别的。
        canvas.drawText(textContent,textTopCenterX-textWidth/2f,textRectHeight/2f+textHeight/2f,textPaint)
    }

}

滚动进度条

暂时无法在飞书文档外展示此内容

1、进度渠

如下图,我们可以看到球体中心左边是黄色,右侧是红色。上个案例我们已经实现了进度渠,只需要上面覆盖一个进度渠,且背景色为黄色即可。

复制粘贴上一个案例代码,修改类名为ProgressBallView,修改进度渠道颜色和背景颜色即可。

kotlin 复制代码
class ProgressBallView : View {
   private fun init(attrs: AttributeSet?) {
      progressBgDitchPaint.apply {
     color = Color.parseColor("#DF0038")//红色,工具吸取
        style = Paint.Style.FILL
    strokeWidth = progressBarHeight
        strokeCap = Cap.ROUND
    isAntiAlias = true
     }
   
   }
}

换个颜色和背景,看看进度渠,自己可以调节一下,这里将就用。效果如下:

水渠在球体运动时候,球体中间左边是黄色,右侧是红色。这个也不难吧,我们在上层再拿水渠路径去绘制个黄色的即可,只是上层黄色是动态的。如下是绘制一层黄色的进度渠之后的效果。

1、但是黄色末尾部分,不需要突出去的弧度,所以路径绘制时候,右侧不需要绘制弧度,直接连接即可,当然如果你这部分用路径绘制是前提。

2、上个案例,这部分是通过paint的Cap来解决的,所以改变绘制路径不太适用,我们可以用裁剪来解决,当然也可以将多出来的Cap向左移动半径长度,当球体覆盖之后会解决此问题。

所以我们将其动态的渠道末尾结束部分的横坐标减去Cap部分的宽度即可。如果基础知识没忘记,需要知道Cap的突出部分的半径是画笔宽度的一半。

css 复制代码
//顶层动态水渠
canvas.drawLine(
    margin,
    0f,
    textTopCenterX-progressBgAnimalDitchPaint.strokeWidth/2f,
    0f,
    progressBgAnimalDitchPaint
)

2、球体

球体部分是一个旋转的球体,很多开发者看其效果之后,可能已经拒绝实现了。下面咱们来慢慢分析,如何实现其滚动的球体。

试想一下,为什么gif动态图会动起来,如果你查看过gif资源的开发者会了解,gif正是很多图片的组合,将其按照一定的顺序段时间内播放就可以达到动态效果。如下打开一张gif其左侧可以拿到每个时刻的单独图片也就是所谓的【帧】,在影视作品和现代科技中都叫做帧,一台电脑是否有强大的渲染能力,也可以查看其绘制的帧率等做比对,人眼在30帧率,也就是1分钟切换30张图片,就可以丝滑无感知的当作电视观看。

同样,我们这个案例,球体的转动,我们可以通过不同时刻绘制一定顺序的不同图片进行还原其效果。我们作为自定义基础练习小节,我希望你可以通过绘制球体的方式进行实现,目的是锻炼其绘制能力,这个案例,我希望带来点不一样的,可以省时省力的方法。那就是贴图,canvas.drawBitmap。这是之前章节费了很大章节讲解的内容了。我们也需要时刻使用。

首先贴图需要素材,这时候可以交给你们的UI或者和我一样大概用简单的PS进行处理素材,将一组完整的旋转球体扣出来。根据工具分析也就七八张。

我们将其绘制到路径上看看效果,并且用动画去不断的播放旋转,我们7张关键帧,用动画进行不断的显示就实现了转动效果。这部分也没什么难度,贴图在4.画布基础绘制【Canvas】 章节已经讲的很详细了,过程不再详细讲解,代码如下:

kotlin 复制代码
private var animator: ValueAnimator? = null
private var animatorBall: ValueAnimator? = null

private fun startAnimal(starAnimal: Float) {
animatorBall?.cancel()
    // 设置弹性动画
    animatorBall = ValueAnimator.ofInt(
        0,1
    )
    animatorBall?.apply {
 duration = (500* starAnimal).toLong()//时间1秒,当然你可以设置一个属性提供给开发着属性变量
        repeatCount = 1
        interpolator = LinearInterpolator()
        addUpdateListener { animation ->
Log.e("frameIndex animation=", animation.toString())
            frameIndex++
            if (frameIndex>6){
                frameIndex = 0
            }
            Log.e("frameIndex=", frameIndex.toString())
            invalidate()
        }
start()
    }
}
private val ballBitmapArray = ArrayList<Bitmap>()
private fun init(attrs: AttributeSet?) {
    ballBitmapArray.add(BitmapFactory.decodeResource(resources, R.drawable.ball_one))
    ballBitmapArray.add(BitmapFactory.decodeResource(resources, R.drawable.ball_two))
    ballBitmapArray.add(BitmapFactory.decodeResource(resources, R.drawable.ball_three))
    ballBitmapArray.add(BitmapFactory.decodeResource(resources, R.drawable.ball_four))
    ballBitmapArray.add(BitmapFactory.decodeResource(resources, R.drawable.ball_five))
    ballBitmapArray.add(BitmapFactory.decodeResource(resources, R.drawable.ball_six))
    ballBitmapArray.add(BitmapFactory.decodeResource(resources, R.drawable.ball_seven))
}
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    //1、将画布平移到控件中心
    //...
    //...
    //绘制球体在最顶层。
    srcRect =
        Rect(0, 0, ballBitmapArray[frameIndex].width, ballBitmapArray[frameIndex].height)
    val dstRect = Rect(
        (textTopCenterX - progressBarHeight / 2f).toInt(),
        (-progressBarHeight / 2f).toInt(),
        (textTopCenterX + progressBarHeight / 2f).toInt(),
        (progressBarHeight / 2f).toInt()
    )
    canvas.drawBitmap(ballBitmapArray[frameIndex], srcRect, dstRect, progressBgAnimalDitchPaint)
    //绘制两个小黄点。
    canvas.drawCircle(textTopCenterX,-progressBarHeight*1f,8f,circlePaint)
    canvas.drawCircle(textTopCenterX,progressBarHeight*1f,8f,circlePaint)
}

3、最终实现

最终代码如下,到这里我们已经完成了两个算是天花板级别的进度控件了,作为学习案例,不管是素材还是写作过程都是比较随意的,所以效果可能也不是完美的,但应该让大家看到了自定义的高度了吧,它可以很高,就看我们是否感想,当然重要在于理解和锻炼对API的使用。学习完成之后,头部几个其他的案例应该不再是问题了吧。

kotlin 复制代码
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    //1、将画布平移到控件中心
    canvas.translate(0f, height / 2f)
    initOverShadowLine()

    //2、绘制进度渠
    canvas.drawLine(
        margin,
        0f,
        width.toFloat() - margin,
        0f,
        progressBgDitchPaint
    )


    //3、绘制上边缘的阴影
    shadowTopPath.reset()
    shadowTopPath.apply {
        moveTo(margin - progressBarHeight / 2f, 0f)
        quadTo(
            margin - progressBarHeight / 2f + marginOut,//c1控制点的横坐标
            0f - progressBarHeight / 2f + marginOut,//c1控制点的纵坐标
            margin,//b点的横坐标
            0f - progressBarHeight / 2f//b点的纵坐标
        )
        lineTo(width.toFloat() - margin, 0f - progressBarHeight / 2f)
        //右侧部分,不明白最好画图。确定此时画布的位置。
        quadTo(
            width.toFloat() - margin + progressBarHeight / 2f - marginOut,
            0f - progressBarHeight / 2f + marginOut,
            width.toFloat() - margin + progressBarHeight / 2f,
            0f
        )
    }
    //绘制阴影
    canvas.drawPath(shadowTopPath, progressTopShadowOutPaint)


    //4、绘制下边缘的阴影
    shadowBottomPath.reset()
    shadowBottomPath.apply {
        moveTo(margin - progressBarHeight / 2f, 0f)
        quadTo(
            margin - progressBarHeight / 2f + marginOut,
            0f + progressBarHeight / 2f - marginOut,
            margin,
            0f + progressBarHeight / 2f//b点的纵坐标
        )
        lineTo(width.toFloat() - margin, 0f + progressBarHeight / 2f)
        //右侧部分,不明白最好画图。确定此时画布的位置。
        quadTo(
            width.toFloat() - margin + progressBarHeight / 2f - marginOut,
            0f + progressBarHeight / 2f - marginOut,
            width.toFloat() - margin + progressBarHeight / 2f,
            0f
        )
    }
    //绘制阴影
    canvas.drawPath(shadowBottomPath, progressBottomShadowOutPaint)

    //5、覆盖部分需要放在最底部,也就是上下边缘绘制的上层,达到覆盖消除交界问题。
    canvas.drawPath(overShadowPath, progressOutPathPaint)

    val totalProgressWidth = (width - margin * 2)
    val progressWidth = totalProgressWidth * min(progressScale, 1f)
    //6、绘制进度条阴影,让其在进度条下方,显得真实,你试一试和进度绘制调换位置。
    canvas.drawLine(
        margin - 2f,
        0f,
        margin - 2f + progressWidth,
        0f,
        progressShadowPaint
    )
    //7、绘制进度条。
    canvas.drawLine(
        margin - 2f,
        0f,
        margin - 2f + progressWidth,
        0f,
        progressPaint
    )


    //8、绘制文字部分。画图画图,除非你可以有很长时间的空间想象力和记忆里。
    textRectPath.reset()
    val textTopCenterX = margin - 2f + progressWidth
    val textRectWidthGeneral = 50f
    val textRectWidthMin = textRectWidthGeneral - 20f
    val textRectCornerHeight =  9f
    val textRectCornerControlHeight =  11f
    canvas.save()
    canvas.translate(0f, -textRectHeight*2.4f)

    textRectPath.apply {
        moveTo(textTopCenterX, 0f)
        //绘制右上角
        lineTo(textTopCenterX + textRectWidthMin, 0f)
        quadTo(textTopCenterX + textRectWidthGeneral, 0f, textTopCenterX + textRectWidthGeneral, 10f)
        //绘制右下角
        lineTo(textTopCenterX + textRectWidthGeneral - 2, textRectHeight - 20f)

        quadTo(
            textTopCenterX + textRectWidthGeneral - 4,
            textRectHeight,
            textTopCenterX + textRectWidthMin,
            textRectHeight
        )
        lineTo(textTopCenterX + textRectWidthMin, textRectHeight)
        lineTo(textTopCenterX + 10f, textRectHeight)

        quadTo(textTopCenterX+5, textRectHeight, textTopCenterX+2, textRectHeight+textRectCornerHeight)
        lineTo(textTopCenterX+2,textRectHeight+textRectCornerHeight)

        //这里是底部最高点
        quadTo(textTopCenterX,textRectHeight+textRectCornerControlHeight,textTopCenterX-2,textRectHeight+textRectCornerHeight)
        lineTo(textTopCenterX-2,textRectHeight+textRectCornerHeight)

        quadTo(textTopCenterX-5, textRectHeight, textTopCenterX - textRectCornerControlHeight, textRectHeight)
        lineTo(textTopCenterX - textRectWidthMin, textRectHeight)
        quadTo(
            textTopCenterX - textRectWidthGeneral,
            textRectHeight,
            textTopCenterX - textRectWidthGeneral,
            textRectHeight - 20f
        )
        lineTo(textTopCenterX - textRectWidthGeneral - 4, 10f)
        quadTo(textTopCenterX - textRectWidthGeneral - 2, 0f, textTopCenterX - textRectWidthMin, 0f)
        close()
    }


    canvas.save()
    //稍作调整画布位置,先绘制阴影部分。
    canvas.translate(0f,-2f)
    canvas.drawPath(textRectPath, textRectShadowPaint)
    //绘制文字框背景之前赶紧恢复画布,让其绘制区域可以覆盖住阴影部分的带有颜色的边缘线
    canvas.restore()
    canvas.drawPath(textRectPath, textRectBgPaint)
    //绘制进度文字
    val formattedNumber = "%.0f".format(progressScale*100)
    val textContent = "$formattedNumber%"
    val rectContent = Rect()
    textPaint.getTextBounds(
        textContent,
        0,
        textContent.length,
        rectContent
    )
    val textWidth =rectContent.width()
    val textHeight = rectContent.height()
    //画图,加减计算。没别的。
    canvas.drawText(textContent,textTopCenterX-textWidth/2f,textRectHeight/2f+textHeight/2f,textPaint)
}

/**
 * 初始化覆盖阴影的路径
 */
private fun initOverShadowLine() {
    overShadowPath.reset()
    overShadowPath.apply {
        moveTo(margin - progressBarHeight / 2f, 0f)
        quadTo(
            margin - progressBarHeight / 2f + marginOut,//c1控制点的横坐标
            0f - progressBarHeight / 2f + marginOut,//c1控制点的纵坐标
            margin,//b点的横坐标
            0f - progressBarHeight / 2f//b点的纵坐标
        )
        lineTo(width.toFloat() - margin, 0f - progressBarHeight / 2f)
        //右侧部分,不明白最好画图。确定此时画布的位置。
        quadTo(
            width.toFloat() - margin + progressBarHeight / 2f - marginOut,
            0f - progressBarHeight / 2f + marginOut,
            width.toFloat() - margin + progressBarHeight / 2f,
            0f
        )

        //右侧部分,不明白最好画图。确定此时画布的位置。
        quadTo(
            width.toFloat() - margin + progressBarHeight / 2f - marginOut,
            0f + progressBarHeight / 2f - marginOut,
            width.toFloat() - margin,
            progressBarHeight / 2f
        )
        lineTo(width.toFloat() - margin, 0f + progressBarHeight / 2f)
        lineTo(margin, 0f + progressBarHeight / 2f)
        quadTo(
            margin - progressBarHeight / 2f + marginOut,//c1控制点的横坐标
            progressBarHeight / 2f - marginOut,//c1控制点的纵坐标
            margin - progressBarHeight / 2f,
            0f
        )
        close()
    }
}

练习案例

以下两个案例中,大家可以选择一个来实现。特别是在动画部分,用代码来实现可能会比较困难,但是贴图API提供了很好的支持。因此,你可以随意地设计一个具有创意的进度条,这样你的进度条就能在博客圈里鹤立鸡群了吧。灵活运用画布变换、状态存储以及贴图API,你可以定义出与众不同的进度条控件。如果大家有问题,可以在评论区留言。如果觉得有难度,也可以在评论区探讨。当然,如果很多人有需要,我也会抽时间写出来,贴在这篇文章后面。

总结

这节课中,我们深入进阶案例,并编写了两平时都没见过的实用案例。我相信,只要你自己根据思路和提示能自行写下来的开发者,就已经达到了本节课的目标。将我们最近学到的知识融合在一起,拓展视野、加深理解。只有将不同的知识点相互结合,才能更灵活地运用自定义功能,发挥其最大的潜力。

心动不如行动,这是我提醒大家最多的意思了,那个区域不适合自己,就在那个区域多多动手,不断实战。有时候其实我们只因缺一个引导和方向呆在原地,误认为遥不可及,我相信只要你动手,坚持,理解了前面的内容,在实践了这本小册的部分内容之后,应该对自定义会有很好的理解,对于日常的开发应该胸有成竹。

相关推荐
Zhu7583 小时前
在k8s集群部署MySQL单实例,支持多个主流发行版
android·mysql·kubernetes
AFinalStone6 小时前
Android 7系统异常问题排查(四)Framework层(上)—System Server Watchdog
android·系统异常
codeGoogle6 小时前
自研 IM 还是选择第三方 SDK?企业开发者应该如何权衡?
前端·后端·程序员
宝杰X77 小时前
Android Room3 多平台数据库
android·数据库
用户938515635077 小时前
React Context 与自定义 Hook 从底层到实践:「跨层级通信 + 副作用封装」全解析
前端·javascript·react.js
滴滴答答哒8 小时前
VUE3+element-plus MultiSelect 多选下拉组件
前端·javascript·vue.js
其美杰布-富贵-李8 小时前
04 watch 与 Vue 响应式数据流
前端·javascript·vue.js
赵大仁9 小时前
生成式 UI 实战:用 JSON Schema + React 动态渲染 AI 界面
前端·ai·react·next.js·前端架构·生成式ui
huibin14785236910 小时前
热缓解学习记录
android·学习
kyriewen11 小时前
我排查了一个React内存泄漏——罪魁祸首是这3个被忽略的清理函数
前端·javascript·面试