这种效果经常会遇到,但却一直不知道这个效果图应该怎么描述,所以暂且以“仪表盘进度控件”来描述,各位博友如果有更好的描述这种效果的词汇,请回复博文告诉我,在此先谢谢各位博友了!
其实做出这样的效果并不困难,只需要了解自定义控件的常规步骤,Canvas绘图操作,外加一点点数学基础就行了,因为在绘制控件的过程中,需要计算一些坐标点和圆弧位置等信息。
为了更加方便的使用该控件,该控件支持自定义控件属性,并提供支持链式编程的方法供开发者设置各种参数,以下是控件实现的各个步骤。
1.自定义控件属性,在布局中直接设置控件参数
在res->values->attrs.xml中新增自定义的控件属性如下:
<declare-styleable name="ArcProgressBar">
<attr name="current_progress" format="float"/>
<attr name="chart_title" format="string"/>
<attr name="max_progress" format="float"/>
<attr name="progress_unit" format="string"/>
</declare-styleable>
PS:如不存在attrs.xml文件,则可以新建任意支持自定义属性的xml文件,且文件名无需保持一致。
2.自定义AroProgressBar过程
2.1 新建ArcProgressBar类并继承View
2.2 获取自定义控件属性并进行相关资源(画笔,颜色等)的初始化
由于需要从布局文件中的自定义属性获取相关布局初始化数据,所以必须实现如下构造函数并从自定义属性对象中获取相关属性数据。
public ArcProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ArcProgressBar);
float hours = typedArray.getFloat(R.styleable.ArcProgressBar_current_progress, 0);
String chartTitle = typedArray.getString(R.styleable.ArcProgressBar_chart_title);
float maxProgress = typedArray.getFloat(R.styleable.ArcProgressBar_max_progress, 0);
String progressUnit = typedArray.getString(R.styleable.ArcProgressBar_progress_unit);
if (hours > 0) {
this.currentProgress = hours;
}
if (maxProgress > 0) {
this.maxProgress = maxProgress;
}
if (!TextUtils.isEmpty(chartTitle)) {
this.chartName = chartTitle;
}
if (!TextUtils.isEmpty(progressUnit)) {
this.progressUnitString = progressUnit;
}
typedArray.recycle();
init();
}
需要注意的一点,在获取自定义属性完毕后,请调用typedArray.recycle();方法释放自定义属性对象。
其中init()方法中是对相关要使用到的画笔进行初始化操作,代码如下:
private void init() {
backArcPaint = new Paint();
backArcPaint.setAntiAlias(true);
backArcPaint.setColor(INNER_CIRCLE_BORDER_COLOR);
backArcPaint.setStrokeWidth(arcStrokeWidth);
backArcPaint.setStyle(Paint.Style.STROKE);
backArcPaint.setStrokeCap(Paint.Cap.ROUND);
fontArcPaint = new Paint();
fontArcPaint.setAntiAlias(true);
fontArcPaint.setColor(FONT_CIRCLE_BORDER_COLOR);
fontArcPaint.setStrokeWidth(arcStrokeWidth);
fontArcPaint.setStyle(Paint.Style.STROKE);
fontArcPaint.setStrokeCap(Paint.Cap.ROUND);
chartNamePaint = new Paint();
chartNamePaint.setStyle(Paint.Style.FILL);
chartNamePaint.setAntiAlias(true);
chartNamePaint.setTextSize(chartNameTextSize);
chartNamePaint.setColor(FONT_CIRCLE_BORDER_COLOR);
unitTextWidth = chartNamePaint.measureText(progressUnitString);
currentProgressNumberPaint = new Paint();
currentProgressNumberPaint.setStyle(Paint.Style.FILL);
currentProgressNumberPaint.setAntiAlias(true);
currentProgressNumberPaint.setTextSize(unitTextSize);
currentProgressNumberPaint.setColor(Color.WHITE);
}
相关要涉及到的控件的成员变量声明如下:
private int circleRectWidth;
//圆弧边框宽度
private float arcStrokeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
10, getContext().getResources().getDisplayMetrics());
//图标名称字符大小
private float chartNameTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
15, getContext().getResources().getDisplayMetrics());
//圆形中心当前进度数字字符大小
private float unitTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
30, getContext().getResources().getDisplayMetrics());
//底层圆弧画笔
private Paint backArcPaint;
//前层圆弧画笔
private Paint fontArcPaint;
//绘制图标名称的画笔
private Paint chartNamePaint;
//绘制数字的画笔
private Paint currentProgressNumberPaint;
//圆弧半径
private int circleRadius;
//中心点X轴坐标
private int centerX;
//中心店Y轴坐标
private int centerY;
//半径占控件宽度的比例
private final float RADIUS_RATIO = 0.3f;
//圆弧开始绘制的角度
private final int START_ANGLE = 135;
//底层圆弧扫过的角度
private final int INNER_CIRCLE_SWEEP_ANGLE = 270;
//底层圆弧的颜色
private final int INNER_CIRCLE_BORDER_COLOR = Color.parseColor("#aaf0f1f2");
//上层圆弧的颜色
private final int FONT_CIRCLE_BORDER_COLOR = Color.parseColor("#eef0f1f2");
private final int BG_COLOR = Color.parseColor("#fe751a");
//默认图标名称
private String chartName = "无标题";
//默认当前进度
private float currentProgress = 0;
//当前进度单位
private String progressUnitString = "";
//进度单位所占的宽度
private float unitTextWidth;
//底部文案的y轴坐标
private float yPosBottomAlign;
//最大进度數
private float maxProgress = 8;
private RectF rectF;
2.3 控件的测量过程
控件的测量过程需要重写父类的onMeasure方法并通过setMeasuredDimension方法将最终的测量结果设置给控件。
代码如下:
@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);
if (widthSpecMode == MeasureSpec.AT_MOST || heightSpecMode == MeasureSpec.AT_MOST) {
float defaultSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
200, getContext().getResources().getDisplayMetrics());
widthSpecSize = (int) defaultSize;
heightSpecSize = (int) defaultSize;
}
setMeasuredDimension(Math.min(widthSpecSize, heightSpecSize), Math.min(widthSpecSize, heightSpecSize));
circleRectWidth = widthSpecSize;
circleRadius = (int) (circleRectWidth * RADIUS_RATIO);
centerX = circleRectWidth / 2;
centerY = circleRectWidth / 2;
float rad = (float) (45 * Math.PI / 180);
yPosBottomAlign = (float) (circleRadius * Math.sin(rad) + centerY);
}
这里的测量过程就是获取控件的宽和高,并取宽和高中较小的一个作为控件的宽高,因为我们实现的控件的宽高是一致的。
这里需要特别注意的是对控件AT_MOST测量模式的处理,如果在布局文件中设置的宽高是wrap_content,则获取到的宽高是0,这时候就需要对控件设置默认的宽高,这也是自定义控件中的应该需要做的处理过程之一。
2.4 控件的绘制过程
控件的绘制操作需要重写父类的onDraw,并通过Canvas对图形进行绘制。
每个控件的绘制都是有先后顺序的,并且后面绘制的图形如果在坐标上与前面绘制的图形有交集,则后面绘制的图形会在坐标存在交集的区域覆盖前面绘制的图形。
该控件的绘制过程代码如下:
@Override
protected void onDraw(final Canvas canvas) {
super.onDraw(canvas);
drawChart(canvas, currentProgress);
}
private void drawChart(Canvas canvas, float loopIndex) {
canvas.drawColor(BG_COLOR);
//1.绘制背景圆弧
if (rectF == null) {
rectF = new RectF(centerX - circleRadius,//left
centerY - circleRadius,//top
centerX + circleRadius,//right
centerY + circleRadius);//bottom
}
canvas.drawArc(rectF, START_ANGLE, INNER_CIRCLE_SWEEP_ANGLE, false, backArcPaint);
//2.绘制进度圆弧
if (maxProgress > 0) {
canvas.drawArc(rectF, START_ANGLE, loopIndex / maxProgress * 270, false, fontArcPaint);
}
//3.绘制底部文案
float chartNameWidth = chartNamePaint.measureText(chartName);
Paint.FontMetrics fontMetrics = chartNamePaint.getFontMetrics();
float chartNameHeight = fontMetrics.descent - fontMetrics.ascent;
canvas.drawText(chartName, centerX - chartNameWidth / 2, (float) (yPosBottomAlign + chartNameHeight * 1.5), chartNamePaint);
//4.绘制中间的当前进度
float hourNumberWidth = currentProgressNumberPaint.measureText(String.valueOf(loopIndex));
float hourNumberHeight = currentProgressNumberPaint.getFontMetrics().bottom - currentProgressNumberPaint.getFontMetrics().top;
//4.1绘制当前进度数字
canvas.drawText(String.valueOf(loopIndex), centerX - hourNumberWidth / 2, centerY + chartNameHeight / 4, currentProgressNumberPaint);
//4.1绘制进度单位
canvas.drawText(progressUnitString, centerX - unitTextWidth / 2, centerY + chartNameHeight / 4 + hourNumberHeight / 2, chartNamePaint);
}
至此,控件的实现过程已基本完成,但为了更方便地修改控件的相关属性,需要暴漏一些公共方法供开发者使用,此控件暴漏的公共方法如下:
/** * 设置当前进度 * * @param hour */
public ArcProgressBar setCurrentProgress(float hour) {
if (hour < 0) {
currentProgress = 0f;
} else if (hour > maxProgress) {
currentProgress = maxProgress;
} else {
currentProgress = hour;
}
return this;
}
/** * 设置图标名称(底部) * * @param chartName */
public ArcProgressBar setProgressUnit(String chartName) {
if (TextUtils.isEmpty(chartName))
return this;
this.chartName = chartName;
return this;
}
/** * 设置最大进度 */
public ArcProgressBar setMaxProgress(float maxHour) {
if (maxHour <= 0) {
return this;
} else if (maxHour < currentProgress) {
this.maxProgress = currentProgress;
} else {
this.maxProgress = maxHour;
}
return this;
}
/** * 刷新界面 * PS:参数设置完成后,务必调用此方法刷新页面 */
public void refresh() {
invalidate();
}
需要注意的是,在设置完相应的参数后,需调用refresh方法才会调用控件的重绘操作,这也是为了避免过多的重复绘制造成系统处理很多不必要的控件重绘过程。
如果开发者需要涉及到动态绘制当前进度的效果,用户可以通过handler进行绘制或者继承SurfaceView进行绘制,不过强烈推荐继承SurfaceView进行绘制,因为在SurfaceView中其实是在子线程中完成的控件绘制过程,这样就很大程度上降低了在UI线程绘制控件造成的性能问题,有关SurfaceView绘制控件的过程大家可以关注相关博文。