自定义View视觉艺术

Android实现带进度条的button

2021-08-25  本文已影响0人  12313凯皇

昨天接了一个需求:需要实现一个一个带进度条的button,如下图所示:


示意图

首先想到的就是通过XferMode来实现,不过在实现的过程中踩了坑,特地记录一下

XferMode

在开始之前先去复习了一下XferMode的基础知识,首先肯定是这张经典的示意图,其中蓝底矩形代表src,黄底圆形是Dst

XferMode

使用起来也很简单:

val sc2 = canvas.saveLayer(mBgRectF, mPaint) //使用离屏缓冲
canvas.drawRoundRect(mBgRectF, mBgCorner, mBgCorner, mPaint) //dst
mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP)
canvas.drawRect(mBgRectF.left, mBgRectF.top, progressWidth, mBgRectF.bottom, mPaint)//src
mPaint.xfermode = null
canvas.restoreToCount(sc2)

但是,坑就坑在,实际绘制出来的效果未必是你预期的效果

实现

基础知识复习完了,接下来是先写一个Demo了:

val sc2 = canvas.saveLayer(mBgRectF, mPaint) //使用离屏缓冲
mPaint.color = Color.RED
canvas.drawRoundRect(mBgRectF, mBgCorner, mBgCorner, mPaint) //dst
mPaint.color = Color.BLUE
val progressWidth = mBgRectF.width() * mProgressPercent
mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP) //SRC_ATOP
canvas.drawRect(mBgRectF.left, mBgRectF.top, progressWidth, mBgRectF.bottom, mPaint)//src
mPaint.xfermode = null
canvas.restoreToCount(sc2)

效果图:


看上去好像这就完成了啊,可是当我替换成设计给的颜色时(有透明度),坑就来了

坑1 ---- 若颜色带有透明度,则两个颜色之间的透明度会互相干扰

举个例子,当把上面代码中的Color.RED加上一点透明度之后,例如Color.argb(100, 255, 0 ,0 )之后,效果图是这样的:


可以看到,我们预计的情况是带有一定透明度的红色背景和纯蓝色的进度条,但是实际绘制出来的进度条也被加上了透明度。如果此时把蓝色也加上一些透明度的话,那么绘制出来的进度条将会几乎看不见。所以这样绘制的话仅能支持透明度为1的纯色绘制,但是这显然不是题主想要的效果。

坑2 ---- 如果不是通过drawBitmap来绘制,那么实际效果可能会与预期效果不一致

既然知道了上面的方法是因为绘制区域有重叠导致了,所以题主就想着能不能先绘制一个背景,然后在通过xferMode来绘制进度条,说干就干

//draw bg 
mPaint.color = Color.argb(15, 0, 0, 0)
canvas.drawRoundRect(mBgRectF, mBgCorner, mBgCorner, mPaint) //draw bg

//draw progress
val sc2 = canvas.saveLayer(mBgRectF, mPaint) //使用离屏缓冲
mPaint.color = if (drawType == 1) mHighlightUnreachedColor else Color.RED
canvas.drawRoundRect(mBgRectF, mBgCorner, mBgCorner, mPaint) //dst
mPaint.color = if (drawType == 1) mHighlightBgColor else Color.BLUE
val progressWidth = mBgRectF.width() * mProgressPercent
mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) //SRC_IN
canvas.drawRect(mBgRectF.left, mBgRectF.top, progressWidth, mBgRectF.bottom, mPaint)//src
mPaint.xfermode = null
canvas.restoreToCount(sc2)

实际绘制出来的效果竟然和使用SRC_ATOP一致,与示意图并不一致。去网上查了一下说是需要通过drawBitmap方法来绘制才可以,详情可以跳转至Android PorterDuffXferMode 防坑指南。文内总结主要是三点:
  1. 关闭硬件加速
  2. 使用drawBitmap方法来绘制,且两个bitmap要尽量一样大
  3. bitmap背景需要时透明的,且如果两个bitmap位置不一样,可能最终效果也和预期效果有出入。

再换一个思路

到这里题主已经准备找设计看能不能就用纯色来绘制了,否则感觉可能需要自己计算绘制路径来手动画了;但是在跟设计battle了一阵之后,决定再看看有没有其他的方法。最终思路还是先绘制一个背景,然后在通过xferMode来绘制进度条,只不过这次选用的是DST_OUT

private fun draw2(canvas: Canvas) {
    //draw bg
    mPaint.color = mHighlightUnreachedColor
    canvas.drawRoundRect(mBgRectF, mBgCorner, mBgCorner, mPaint) //dst

    val sc2 = canvas.saveLayer(mBgRectF, mPaint) //使用离屏缓冲

    mPaint.color = mHighlightBgColor
    canvas.drawRoundRect(mBgRectF, mBgCorner, mBgCorner, mPaint) //dst
    Log.d(TAG, "draw dst: ${mBgRectF.left} ${mBgRectF.right}")

    val progressWidth = mBgRectF.width() * mProgressPercent
    mPaint.alpha = 255  //如果颜色带有透明度,为了不影响绘制,这里将透明度置为1
    mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
    canvas.drawRect(progressWidth, mBgRectF.top, mBgRectF.right, mBgRectF.bottom, mPaint) //src
    Log.d(TAG, "draw src: ${mBgRectF.left} ${mBgRectF.right}")

    mPaint.xfermode = null
    canvas.restoreToCount(sc2)
}

完整代码

class ProgressButton(context: Context, attr: AttributeSet?, defStyleAttr: Int) :
    AppCompatTextView(context, attr, defStyleAttr) {

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attr: AttributeSet?) : this(context, attr, 0)

    private var mCurStatus: Status = Status.NORMAL

    private var mNormalTextColor: Int = DEFAULT_TEXT_COLOR
    private var mHighlightTextColor: Int = HIGHLIGHT_TEXT_COLOR
    private var mNormalBgColor: Int = DEFAULT_BG_COLOR
    private var mHighlightBgColor: Int = HIGHLIGHT_BG_COLOR
    private var mHighlightUnreachedColor: Int = HIGHLIGHT_UNREACHED_BG_COLOR

    private var mBgCorner: Float
    private var mCurProgress: Int = 0
    private var mMaxProgress: Int = 100
    private val mProgressPercent: Float get() = mCurProgress * 1.0F / mMaxProgress

    private val mPaint: Paint
    private var mBgRectF: RectF = RectF()

    init {
        gravity = Gravity.CENTER
        mPaint = Paint().apply {
            style = Paint.Style.FILL
        }
        mBgCorner = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, DEFAULT_BG_CORNER,
            context.resources.displayMetrics
        )
    }

    fun setStatus(status: Status) {
        if (mCurStatus == status) return
        mCurStatus = status
        //注意setText,setTextColor方法会触发重绘
        when (mCurStatus) {
            Status.NORMAL -> setTextColor(mNormalTextColor)
            Status.HIGHLIGHT -> setTextColor(mHighlightTextColor)
        }
    }

    fun updateProgress(progress: Int) {
        mCurProgress = progress
        text = String.format("%d%s", (mProgressPercent * 100).toInt(), "%")
        setStatus(Status.HIGHLIGHT)
    }

    override fun onDraw(canvas: Canvas?) {
        Log.d(TAG, "onDraw: $canvas $mCurStatus")
        mBgRectF.set(0F, 0F, measuredWidth.toFloat(), measuredHeight.toFloat())
        canvas?.let {
            when (mCurStatus) {
                Status.NORMAL -> canvas.drawRoundRect(
                    mBgRectF,
                    mBgCorner,
                    mBgCorner,
                    mPaint.also { it.color = mNormalBgColor })
                Status.HIGHLIGHT -> drawProgress(canvas)
            }
        }
        super.onDraw(canvas)
    }

    private fun drawProgress(canvas: Canvas) {
        //draw bg
        mPaint.color = mHighlightUnreachedColor
        canvas.drawRoundRect(mBgRectF, mBgCorner, mBgCorner, mPaint) //dst
        mPaint.alpha = 255 //还原透明度

        val sc2 = canvas.saveLayer(mBgRectF, mPaint) //使用离屏缓冲

        mPaint.color = mHighlightBgColor
        canvas.drawRoundRect(mBgRectF, mBgCorner, mBgCorner, mPaint) //dst
        Log.d(TAG, "draw dst: ${mBgRectF.left} ${mBgRectF.right}")

        val progressWidth = mBgRectF.width() * mProgressPercent
        mPaint.alpha = 255
        mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
        canvas.drawRect(progressWidth, mBgRectF.top, mBgRectF.right, mBgRectF.bottom, mPaint) //src
        Log.d(TAG, "draw src: ${mBgRectF.left} ${mBgRectF.right}")

        mPaint.xfermode = null
        canvas.restoreToCount(sc2)
    }

    companion object {
        private val DEFAULT_TEXT_COLOR = Color.rgb(0, 0, 0)
        private val DEFAULT_BG_COLOR = Color.argb(15, 0, 0, 0)
        private const val DEFAULT_BG_CORNER = 100F //dp

        private val HIGHLIGHT_TEXT_COLOR = Color.rgb(255, 97, 46)
        private val HIGHLIGHT_BG_COLOR = Color.argb(51, 255, 97, 46)
        private val HIGHLIGHT_UNREACHED_BG_COLOR = Color.argb(15, 255, 97, 46)
    }

    sealed class Status {
        object NORMAL : Status()
        object HIGHLIGHT : Status()
    }
}

更新:

总结

参考文章

上一篇 下一篇

猜你喜欢

热点阅读