进度条天花板-自定义

感谢

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

回顾

在上节小册,我们深入学习了裁剪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,你可以定义出与众不同的进度条控件。如果大家有问题,可以在评论区留言。如果觉得有难度,也可以在评论区探讨。当然,如果很多人有需要,我也会抽时间写出来,贴在这篇文章后面。

总结

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

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

相关推荐
前端小小王26 分钟前
React Hooks
前端·javascript·react.js
C4rpeDime33 分钟前
自建MD5解密平台-续
android
迷途小码农零零发36 分钟前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀1 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪1 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
鲤籽鲲2 小时前
C# Random 随机数 全面解析
android·java·c#
ekskef_sef3 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6413 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻4 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云4 小时前
npm淘宝镜像
前端·npm·node.js