昨天接了一個(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怠褐。
使用起來也很簡單:
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):
- 關(guān)閉硬件加速
- 使用drawBitmap方法來繪制锣笨,且兩個(gè)bitmap要盡量一樣大
- 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)證):
- 關(guān)閉硬件加速
- 使用drawBitmap方法來繪制,且兩個(gè)bitmap要盡量一樣大
- bitmap背景需要時(shí)透明的剿吻,且如果兩個(gè)bitmap位置不一樣窍箍,可能最終效果也和預(yù)期效果有出入。