Android实现带进度条的button

昨天接了一个需求:需要实现一个一个带进度条的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位置不一样,可能最终效果也和预期效果有出入。

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容