Android實(shí)現(xiàn)帶進(jìn)度條的button

昨天接了一個(gè)需求:需要實(shí)現(xiàn)一個(gè)一個(gè)帶進(jìn)度條的button碧磅,如下圖所示:


示意圖

首先想到的就是通過XferMode來實(shí)現(xiàn),不過在實(shí)現(xiàn)的過程中踩了坑歌豺,特地記錄一下

XferMode

在開始之前先去復(fù)習(xí)了一下XferMode的基礎(chǔ)知識弱判,首先肯定是這張經(jīng)典的示意圖绑青,其中藍(lán)底矩形代表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)

但是,坑就坑在您宪,實(shí)際繪制出來的效果未必是你預(yù)期的效果奈懒。

實(shí)現(xiàn)

基礎(chǔ)知識復(fù)習(xí)完了,接下來是先寫一個(gè)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)

效果圖:


看上去好像這就完成了啊蚕涤,可是當(dāng)我替換成設(shè)計(jì)給的顏色時(shí)(有透明度),坑就來了

坑1 ---- 若顏色帶有透明度铣猩,則兩個(gè)顏色之間的透明度會互相干擾

舉個(gè)例子揖铜,當(dāng)把上面代碼中的Color.RED加上一點(diǎn)透明度之后,例如Color.argb(100, 255, 0 ,0 )之后达皿,效果圖是這樣的:


可以看到天吓,我們預(yù)計(jì)的情況是帶有一定透明度的紅色背景和純藍(lán)色的進(jìn)度條,但是實(shí)際繪制出來的進(jìn)度條也被加上了透明度峦椰。如果此時(shí)把藍(lán)色也加上一些透明度的話龄寞,那么繪制出來的進(jìn)度條將會幾乎看不見。所以這樣繪制的話僅能支持透明度為1的純色繪制汤功,但是這顯然不是題主想要的效果物邑。

坑2 ---- 如果不是通過drawBitmap來繪制,那么實(shí)際效果可能會與預(yù)期效果不一致

既然知道了上面的方法是因?yàn)槔L制區(qū)域有重疊導(dǎo)致了滔金,所以題主就想著能不能先繪制一個(gè)背景色解,然后在通過xferMode來繪制進(jìn)度條,說干就干

//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)


實(shí)際繪制出來的效果竟然和使用SRC_ATOP一致餐茵,與示意圖并不一致科阎。去網(wǎng)上查了一下說是需要通過drawBitmap方法來繪制才可以,詳情可以跳轉(zhuǎn)至Android PorterDuffXferMode 防坑指南忿族。文內(nèi)總結(jié)主要是三點(diǎn):

  1. 關(guān)閉硬件加速
  2. 使用drawBitmap方法來繪制锣笨,且兩個(gè)bitmap要盡量一樣大
  3. bitmap背景需要時(shí)透明的,且如果兩個(gè)bitmap位置不一樣道批,可能最終效果也和預(yù)期效果有出入错英。

再換一個(gè)思路

到這里題主已經(jīng)準(zhǔn)備找設(shè)計(jì)看能不能就用純色來繪制了,否則感覺可能需要自己計(jì)算繪制路徑來手動(dòng)畫了隆豹;但是在跟設(shè)計(jì)battle了一陣之后走趋,決定再看看有沒有其他的方法。最終思路還是先繪制一個(gè)背景,然后在通過xferMode來繪制進(jìn)度條簿煌,只不過這次選用的是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方法會觸發(fā)重繪
        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方法來限制所繪制的圖形區(qū)域也可實(shí)現(xiàn)預(yù)期效果姨伟,且不會像xfermode那樣受顏色透明度的影響惩琉,使用起來也更方便《峄模看來還是書看少了 = =

總結(jié)

  • 如果不是通過drawBitmap來繪制瞒渠,則最好先寫一個(gè)demo驗(yàn)證一下,因?yàn)榭赡軐?shí)際繪制的效果和示意效果不一致技扼。
  • 如果是通過drawBitmap來繪制伍玖,則需要注意以下問題(未驗(yàn)證):
    1. 關(guān)閉硬件加速
    2. 使用drawBitmap方法來繪制,且兩個(gè)bitmap要盡量一樣大
    3. bitmap背景需要時(shí)透明的剿吻,且如果兩個(gè)bitmap位置不一樣窍箍,可能最終效果也和預(yù)期效果有出入。

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丽旅,一起剝皮案震驚了整個(gè)濱河市椰棘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌榄笙,老刑警劉巖邪狞,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異茅撞,居然都是意外死亡帆卓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進(jìn)店門米丘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鳞疲,“玉大人,你說我怎么就攤上這事蠕蚜∩星ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵靶累,是天一觀的道長腺毫。 經(jīng)常有香客問我,道長挣柬,這世上最難降的妖魔是什么潮酒? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮邪蛔,結(jié)果婚禮上急黎,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好勃教,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布淤击。 她就那樣靜靜地躺著,像睡著了一般故源。 火紅的嫁衣襯著肌膚如雪污抬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天绳军,我揣著相機(jī)與錄音印机,去河邊找鬼。 笑死门驾,一個(gè)胖子當(dāng)著我的面吹牛射赛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奶是,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼楣责,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了诫隅?” 一聲冷哼從身側(cè)響起腐魂,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤帐偎,失蹤者是張志新(化名)和其女友劉穎逐纬,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體削樊,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡豁生,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了漫贞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甸箱。...
    茶點(diǎn)故事閱讀 38,809評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖迅脐,靈堂內(nèi)的尸體忽然破棺而出芍殖,到底是詐尸還是另有隱情,我是刑警寧澤谴蔑,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布豌骏,位于F島的核電站,受9級特大地震影響隐锭,放射性物質(zhì)發(fā)生泄漏窃躲。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一钦睡、第九天 我趴在偏房一處隱蔽的房頂上張望蒂窒。 院中可真熱鬧,春花似錦、人聲如沸洒琢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纬凤。三九已至福贞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間停士,已是汗流浹背挖帘。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恋技,地道東北人拇舀。 一個(gè)月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像蜻底,于是被迫代替她去往敵國和親骄崩。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容