Android自带的进度条往往不能满足我们开发上的需求,最重要的理由是颜值不太够。
我需要一个旋转的加载控件,要看上去简单,优雅不妖艳不做作。现有自带的Android控件并不能满足我的要求,这时候就需要用到自定义View来解决了。
自定义View有两种方法一种是继承并重写View,一种是继承重写现有的控件(如:TextView),我选择了前者来做。
别的不多说直接上效果图,看到这里图中要是不是你的菜你就可以不用看下去了。
十分的简洁,实现起来也不会非常的复杂。
控件只要分两个部分一个是不停在旋转的4分之一的圈圈,另一个是数字进度显示。
Android自带的画布已经有接口可以满足上面的两个部分的绘制,一个用圆弧绘制drawArc(),一个绘制文字drawText()。
这个控件很简单,所以功能上我们不需要太多的点缀,我们在个性化上加一点点的小心思,让它在用起来更方便,比如:线的粗细,中间字的大小,字体的颜色,线的颜色等。
把想到的这些自定义属性加到attrs.xml文件里面去
我们在弄清楚实现的思路后,我们可以直接重写一个View来做。继承了View重写它的第二个构造方法
public class RoundProgress extends View {
public RoundProgress(Context context, @Nullable AttributeSet attrs){
super(context, attrs);
}
}
我们在构造方法中通过获取它的自定义属性
TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.RoundProgress);
progress_color = arr.getColor(R.styleable.RoundProgress_progress_color, Color.BLUE);
progress_weight = arr.getDimensionPixelSize(R.styleable.RoundProgress_progress_line_weight,8);
text_size= arr.getDimensionPixelSize(R.styleable.RoundProgress_progress_text_size,16);
text_color = arr.getColor(R.styleable.RoundProgress_progress_text_color,Color.BLACK);
arr.recycle();
注意获取完属性后我们需要记得调用arr的recycle方法进行回收,至于为什么要回收就是另一个知识点了。
然后我们需要初始化我们的画笔
mPaint = new Paint();
mPaint.setAntiAlias(true);
之后下一步我们需要让这个自定义View具有响应match_parent属性的效果,这一步我们需要重写onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = measureDimension(75,widthMeasureSpec);
height = measureDimension(75,heightMeasureSpec);
setMeasuredDimension(width, height);
}
public int measureDimension(int defaultSize, int measureSpec){
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else{
result = defaultSize; //UNSPECIFIED
if(specMode == MeasureSpec.AT_MOST){
result = Math.max(result, specSize);
}
}
return result;
}
长宽的测量做好了,剩下就是绘制的事了,我们根据之前的分析,绘制我们控件的两个部分:
@Override
public void onDraw(Canvas canvas) {
//根据oval这个矩形绘制一个圆弧
if(oval == null){
int shorter = getWidth()>=getHeight()?getHeight():getWidth();
oval = new RectF( (getWidth()-shorter+16)/2, (getHeight()-shorter+16)/2,
(getWidth()+shorter-16)/2, (getHeight()+shorter-16)/2);
}
//根据自定义属性初始化画笔
mPaint.setColor(progress_color);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(progress_weight);
//绘制圆弧,圆弧的角度我们给了120,如果可以这个角度我们也能做成自定义属性来控制
canvas.drawArc(oval, startPosition, 120, false, mPaint);
//根据自定义属性初始化绘制文字的画笔
mPaint.setTextSize(text_size);
mPaint.setStrokeWidth(2);
mPaint.setColor(text_color);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextAlign(Paint.Align.LEFT);
mPaint.setLinearText(true);
//计算字体绘制的位置
float left_add_rate =(float) (progress+"%").length()/2;
canvas.drawText(progress + "%", (getWidth()-(float)text_size*left_add_rate) / 2, (getHeight()+(float)text_size/2) / 2, mPaint);
//绘制完成后起始点增长,用于下一次的绘制
startPosition += 6;
if(startPosition >=360) {
startPosition = 0;
}
}
一次的绘制就这样完成了,一个View要不停的动那么我们就需要不停的重新绘制这个 View, 那么我们需要如何不停地绘制这个View:
我们可以使用一个不停循环的线程,然后不停的发送Message给Handler通知UI线程进行View的绘制。
也可以单纯地使用Handler不停地给自己发Message,循环绘制。
我为了方便选择了后者,理论来说前者的显示效果会好一点,毕竟延时的操作不是在UI线程进行的。
后者的实现方式:
Handler mhandler=new Handler(){
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 0x001:
//重新绘制界面
invalidate();//告诉UI主线程重新绘制
//延迟25毫秒后发送通知
mhandler.sendEmptyMessageDelayed(0x001, 25);
break;
default:
break;
}
}
};
//然后在构造函数里面触发这个Handler
public RoundProgress(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
...
mhandler.sendEmptyMessageDelayed(0x001, 25);
}
最后我们需要暴露一个修改进度数值的方法:
public void setProgress(int progress) {
this.progress = progress;
}
以上是RoundProgress的全部代码,Rebuild项目以后就能使用了。
总结
本文是我在学习过程的一些记录跟分享,这个自定义View还有很多优化的地方,目前实现的仅仅是很简单的效果,大家可以参考一下,比如在动画效果上,在样式属性上,线程优化等方面进行修改优化。
下面是完整代码:
public class RoundProgress extends View {
private int progress = 0;
private int startPosition = 0;
private int width;
private int height;
private int progress_weight;
private int progress_color;
private int text_size;
private int text_color;
private Paint mPaint;
private RectF oval;
private boolean isRun = false;
Handler mhandler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 0x001:
//重新绘制界面
invalidate();//告诉UI主线程重新绘制
mhandler.sendEmptyMessageDelayed(0x001, 25);
break;
default:
break;
}
}
};
public RoundProgress(Context context) {
super(context);
}
public RoundProgress(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.RoundProgress);
progress_color = arr.getColor(R.styleable.RoundProgress_progress_color, Color.BLUE);
progress_weight = arr.getDimensionPixelSize(R.styleable.RoundProgress_progress_line_weight, 8);
text_size = arr.getDimensionPixelSize(R.styleable.RoundProgress_progress_text_size, 16);
text_color = arr.getColor(R.styleable.RoundProgress_progress_text_color, Color.BLACK);
arr.recycle();
mPaint = new Paint();
mPaint.setAntiAlias(true);
mhandler.sendEmptyMessageDelayed(0x001, 15);
}
public RoundProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void onDraw(Canvas canvas) {
if (oval == null) {
int shorter = getWidth() >= getHeight() ? getHeight() : getWidth();
oval = new RectF((getWidth() - shorter + 16) / 2, (getHeight() - shorter + 16) / 2,
(getWidth() + shorter - 16) / 2, (getHeight() + shorter - 16) / 2);
}
mPaint.setColor(progress_color);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(progress_weight);
canvas.drawArc(oval, startPosition, 120, false, mPaint);
mPaint.setTextSize(text_size);
mPaint.setStrokeWidth(2);
mPaint.setColor(text_color);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextAlign(Paint.Align.LEFT);
mPaint.setLinearText(true);
float left_add_rate = (float) (progress + "%").length() / 2;
canvas.drawText(progress + "%", (getWidth() - (float) text_size * left_add_rate) / 2, (getHeight() + (float) text_size / 2) / 2, mPaint);
startPosition += 6;
if (startPosition >= 360) {
startPosition = 0;
}
}
public void startProgress() {
mhandler.sendEmptyMessageDelayed(0x001, 1000);
}
public void setProgress(int progress) {
this.progress = progress;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = measureDimension(75, widthMeasureSpec);
height = measureDimension(75, heightMeasureSpec);
setMeasuredDimension(width, height);
}
public int measureDimension(int defaultSize, int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//Log.i("specMode",specMode+"");
//Log.i("specSize",specSize+"");
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = defaultSize; //UNSPECIFIED
if (specMode == MeasureSpec.AT_MOST) {
result = Math.max(result, specSize);
}
}
//Log.i("result",result+"");
return result;
}
}