美文网首页自定义View视觉艺术
Android实现带进度条的button

Android实现带进度条的button

作者: 12313凯皇 | 来源:发表于2021-08-25 20:27 被阅读0次

    昨天接了一个需求:需要实现一个一个带进度条的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()
        }
    }
    

    更新:

    • 可以通过使用canvas.clicpRect方法来限制所绘制的图形区域也可实现预期效果,且不会像xfermode那样受颜色透明度的影响,使用起来也更方便。看来还是书看少了 = =

    总结

    • 如果不是通过drawBitmap来绘制,则最好先写一个demo验证一下,因为可能实际绘制的效果和示意效果不一致。
    • 如果是通过drawBitmap来绘制,则需要注意以下问题(未验证):
      1. 关闭硬件加速
      2. 使用drawBitmap方法来绘制,且两个bitmap要尽量一样大
      3. bitmap背景需要时透明的,且如果两个bitmap位置不一样,可能最终效果也和预期效果有出入。

    参考文章

    相关文章

      网友评论

        本文标题:Android实现带进度条的button

        本文链接:https://www.haomeiwen.com/subject/syjeiltx.html