昨天接了一个需求:需要实现一个一个带进度条的button,如下图所示:
示意图
首先想到的就是通过XferMode
来实现,不过在实现的过程中踩了坑,特地记录一下
XferMode
在开始之前先去复习了一下XferMode的基础知识,首先肯定是这张经典的示意图,其中蓝底矩形代表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)
但是,坑就坑在,实际绘制出来的效果未必是你预期的效果。
实现
基础知识复习完了,接下来是先写一个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 防坑指南。文内总结主要是三点:
- 关闭硬件加速
- 使用drawBitmap方法来绘制,且两个bitmap要尽量一样大
- 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来绘制,则需要注意以下问题(未验证):
- 关闭硬件加速
- 使用drawBitmap方法来绘制,且两个bitmap要尽量一样大
- bitmap背景需要时透明的,且如果两个bitmap位置不一样,可能最终效果也和预期效果有出入。
网友评论