在我们APP中,经常会用到如下的一张比例图:
这个比例图可以清楚的展示一个项目所占的比例,同时也可以变成一个圆形进度条:

显而易见,这是一个自定义View,那么该如何创建这样一个自定义View呢?我们先来简单的回顾一下自定义View的流程,分三步,onMesure(),onLayout,onDraw()。这个自定义View比较简单,分为三部分:中间的圆,中间显示的文字、外圈的弧线。既然有了这样的思路,我们只要在onDraw()方法中一个一个的去绘制就可以了。
接下来,我们就从代码入手一步一步自定义这个View。
1、我们先来看下需要用到的属性:
2、看看我们的onDraw()方法里面做了哪些工作:
3、我们对外提供一个设置比例(进度)的方法,调用者可以通过调用此方法来改变比例(进度):
4、在布局文件中使用:
现在,运行一下我们的程序,结果如下图:
到此为止,基本已经成型了,但是此时layout_width 和 layout_height 使用 wrap_content无效,对于直接继承View的控件,如果不对wrap_content做特殊的处理,那么使用wrap_content就相当于使用match_parent,我这了不予测试,读者可以自行测试,为什么会这样呢,通过解读View的onMeasure()方法的源码可知,如果View在布局中使用Wrap_content,那么它的specMode为AT_MOST模式,在这种模式下,它的宽高等于specSize,在这种情况下,View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。这下就明白了,View的宽高就等于父容器当前剩余空间的大小,这种效果和在布局中使用match_parent完全一致。如何解决这个问题呢?也很简单,重写onMeasure()方法,,指定一个wrap_content模式的默认宽高:
此时layout_width 和 layout_height 使用 wrap_content已生效。为了追求自定义View的完美,你还可以让你的View支持padding,如果不在onDraw()方法中处理padding,那么padding属性是无法起作用的。
5、在MainActivity中,我们通过Handler发送延迟消息来模拟进度的增长:
最后,代码传送门
//PercentCircleView的宽高
private float mWidth;
private float mHeight;
//圆心坐标
private float mCircleXY;
//圆的半径
private float mRadius;
//弧线的外接矩形
private RectF rectF;
//画笔
private Paint paint;
private Paint textPaint;
private Paint circlePaint;
//最大进度
private int mMaxProgress = 100;
//当前进度
private int mProgress = 30;
//外层弧线的宽
private int mCircleLineStrokeWidth;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//得到视图的宽高取最小值给宽
mWidth = getWidth();
mHeight = getHeight();
mWidth = Math.min(mWidth, mHeight);
//确定圆心,半径,外弧的宽度
mCircleXY = mWidth / 2;
mRadius = mCircleXY / 2;
mCircleLineStrokeWidth = (int) (mRadius / 2);
paint.setAntiAlias(true);
paint.setColor(Color.rgb(0x00, 0xff, 0x00));
canvas.drawColor(Color.TRANSPARENT);
paint.setStrokeWidth(mCircleLineStrokeWidth);
paint.setStyle(Paint.Style.STROKE);
circlePaint.setAntiAlias(true);
circlePaint.setColor(Color.rgb(0x00, 0xff, 0x00));
//绘制弧线,需要制定其椭圆的外接矩形
rectF.left = mCircleLineStrokeWidth / 2;
rectF.top = mCircleLineStrokeWidth / 2;
rectF.right = mWidth - mCircleLineStrokeWidth / 2;
rectF.bottom = mWidth - mCircleLineStrokeWidth / 2;
//绘制圆
canvas.drawCircle(mCircleXY,mCircleXY,mRadius,circlePaint);
//绘制弧线
canvas.drawArc(rectF,-90,((float) mProgress/mMaxProgress) * 360,false,paint);
//绘制文字
String text = mProgress + "%";
float showTextSize = mWidth / 10;
textPaint.setTextSize(showTextSize);
textPaint.setColor(Color.rgb(0xff, 0x00, 0x00));
canvas.drawText(text,0,text.length(),mCircleXY - (showTextSize * text.length())/4,mCircleXY + showTextSize / 3 ,textPaint);
}
//对外提供设置进度的方法
public void setmProgress(int progress){
if(progress >= 0) {
mProgress = progress;
} else {
mProgress = 0;
}
this.invalidate();
}
到此为止,基本已经成型了,但是此时layout_width 和 layout_height 使用 wrap_content无效,对于直接继承View的控件,如果不对wrap_content做特殊的处理,那么使用wrap_content就相当于使用match_parent,我这了不予测试,读者可以自行测试,为什么会这样呢,通过解读View的onMeasure()方法的源码可知,如果View在布局中使用Wrap_content,那么它的specMode为AT_MOST模式,在这种模式下,它的宽高等于specSize,在这种情况下,View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。这下就明白了,View的宽高就等于父容器当前剩余空间的大小,这种效果和在布局中使用match_parent完全一致。如何解决这个问题呢?也很简单,重写onMeasure()方法,,指定一个wrap_content模式的默认宽高:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//取出宽高的测量模式和大小
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//处理layout_width 和 layout_height 为 wrap_content的这种情况,使wrap_content有效
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200,200);
} else if(widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200,heightSpecSize);
} else if(heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize,200);
}
}
5、在MainActivity中,我们通过Handler发送延迟消息来模拟进度的增长:
private int progress;
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
if(progress < 100) {
progress ++;
circleView.setmProgress(progress);
sendEmptyMessageDelayed(0,200);
} else {
removeCallbacksAndMessages(null);
}
}
};
private PercentCircleView circleView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
circleView = (PercentCircleView) findViewById(R.id.circle);
// circleView.setmProgress(80);
handler.sendEmptyMessageDelayed(0,200);
}
最后,代码传送门