UI 说需要实现这样圆角横向进度条,好,于是我就去屁颠屁颠的 Google。下面就是我的辛酸历程。
android:progressDrawable
属性首先找到的一种实现方法就是为 ProgressBar
设置 android:progressDrawable
属性,类似于 Progress内外圆角进度条 这篇文章里面说的。
实现起来比较简单方便,但是后来在测试的时候就会发现问题,比如说在 progress 值很小的时候(max == 100 && progress == 1
),进度条的显示就会出现如下情况:
所以,不行!不行!不行!
首先,我还是去了 GayHub 去找看有没有现成的或者类似的轮子,毕竟我还是喜欢偷懒的。
在筛选了一番之后,找到一个类似的 【yongfengnice/CircleProgress】
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画背景
RectF bgRectF = new RectF(0, 0, mViewWidth, mViewHeight);
canvas.drawRoundRect(bgRectF, mViewHeight / 2, mViewHeight / 2, mBackgroundPaint);
//画进度条
int width = (int) (mViewWidth * (mProgress * 1.0f / mMaxProgress) + 0.5);
if (width <= mViewHeight) {//圆形
canvas.drawCircle(width / 2, mViewHeight / 2, width / 2, mProgressPaint);
} else {
RectF progressRectF = new RectF(0, 0, width, mViewHeight);
canvas.drawRoundRect(progressRectF, mViewHeight / 2, mViewHeight / 2, mProgressPaint);
}
}
其实现原理,就是先用个 canvas.drawRoundRect
画背景(即图中灰色部分),然后根据进度值,如果是还没有超过圆弧的区域时,就直接画一个圆,否则就同样画一个 RoundRect
。
这样,在进度值小的时候,就会感觉怪怪的,因为它右边的弧没有契合。
于是乎,追求完美主义的我(实际上是混不过产品眼睛)就犯轴了,怎么才能实现下面那样更加完美的效果呢?
首先我想到的是,黄色进度区域用画椭圆来实现,但是苦逼的是以我现在的能力,无法算出下图标记出的两个点的 y 坐标。(当然了,赛贝尔曲线我更加 Hold 不住)
于是乎,我就想,我既然无法直接画出黄色进度条的部分,那么我可以再画一个跟背景一样的 RoundRect,然后去移动它,再利用 android Xfermode 去实现只显示重叠的部分(也就是进度条部分)以及原本的背景部分。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = (int) (mViewWidth * (mProgress * 1.0f / mMaxProgress) + 0.5f);
//int saved = canvas.saveLayer(0,0,mViewWidth,mViewHeight, null, Canvas.ALL_SAVE_FLAG); 设定生效的区域
//canvas.saveLayer() 有两个重载函数,如果没有指定 Layer 的区域,则默认为当前 View 的范围
int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);//开始
onDrawPaint.setColor(colorBcg);
//画背景
canvas.drawRoundRect(new RectF(0, 0, mViewWidth, mViewHeight), mViewHeight / 2, mViewHeight / 2, onDrawPaint);
onDrawPaint.setXfermode(xfermode);
onDrawPaint.setColor(colorProgress);
//画进度条
canvas.drawRoundRect(new RectF(width-mViewWidth, 0, width, mViewHeight), mViewHeight / 2, mViewHeight / 2, onDrawPaint);
onDrawPaint.setXfermode(null);
canvas.restoreToCount(saved);//结束
}
上面就是核心代码了,主要是利用
xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP);
来实现的。
注意,画背景和进度条的逻辑需要根据 Xfermode 设置的模式结合画的顺序来弄,且需要包含在注释标注的 “开始” 与 “结束” 之间。
题外话,我开始天真的以为 Xfermode 只适用于 drawBitmap…
参考文章:
完整的源码实现
public class RoundedProgressBar extends View {
private int mViewWidth;
private int mViewHeight;
private int mMaxProgress = 100;//最大进度
private int mProgress = 0;//当前进度
Paint onDrawPaint;
Xfermode xfermode;
private String colorBcg;
private String colorProgress;
public RoundedProgressBar(Context context) {
this(context, null);
}
public RoundedProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
setBackgroundResource(0);//移除设置的背景资源
xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP);
onDrawPaint = new Paint();
onDrawPaint.setFilterBitmap(false);
colorBcg = "#1B1E33";
colorProgress = "#FFD600";
//需要注意的是,如果颜色设置透明度,叠加后的图也会因为设置的透明度而变化,
//且会因 xfermode 的各种模式而受到影响,导致跟预期不同
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mViewWidth = getMeasuredWidth();
mViewHeight = getMeasuredHeight();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = (int) (mViewWidth * (mProgress * 1.0f / mMaxProgress) + 0.5f);
// int saved = canvas.saveLayer(0,0,mViewWidth,mViewHeight, null, Canvas.ALL_SAVE_FLAG); //设定生效的区域
int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
onDrawPaint.setColor(Color.parseColor(colorBcg));
canvas.drawRoundRect(new RectF(0, 0, mViewWidth, mViewHeight), mViewHeight / 2, mViewHeight / 2, onDrawPaint);
onDrawPaint.setXfermode(xfermode);
onDrawPaint.setColor(Color.parseColor(colorProgress));
canvas.drawRoundRect(new RectF(width-mViewWidth, 0, width, mViewHeight), mViewHeight / 2, mViewHeight / 2, onDrawPaint);
onDrawPaint.setXfermode(null);
canvas.restoreToCount(saved);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
invalidate();
}
/**
* 设置当前进度
*
* @param progress
*/
public void setProgress(int progress) {
progress = progress >= 0 ? progress : 0;
progress = progress <= mMaxProgress ? progress : mMaxProgress;
mProgress = progress;
invalidate();
}
/**
* 设置最大进度
*
* @param maxProgress
*/
public void setMaxProgress(int maxProgress) {
mMaxProgress = maxProgress;
invalidate();
}
}