重学 Android 自定义 View 系列(六):环形进度条
目标
自定义一个环形进度条,可以自定义其最大值、当前进度、背景色、进度色,宽度等信息。
最终效果如下(GIF展示纯色有点问题):
1. 结构分析
- 背景圆环:表示进度条的背景。
- 进度圆环:表示当前进度,根据进度值动态绘制圆环。
- 进度值文本:在圆环中间展示进度。
2. 实现思路
- 定义自定义属性:在 res/values/attrs.xml 中定义自定义属性,以便通过 XML 配置自定义的视图样式。
- 初始化视图元素:在构造函数中,根据传入的属性初始化各种画笔、尺寸和视图元素。
- 绘制视图内容:重写 onDraw 方法,使用画笔绘制背景圆环、进度圆环和文本。
- 支持动态更新进度:提供 setProgress 方法,允许外部动态设置进度,触发视图重绘。
3. 关键技术点解析
我们首先要知道,画圆的基础是有个正方形,或者说有一个正方形的坐标,还要考虑到画笔的宽度,所以我们首先就要确定矩形的大小:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int padding = circleWidth / 2;
rectF.set(padding, padding, w - padding, h - padding);
}
circleWidth
为圆环的宽,也为圆环画笔的宽。
canvas.drawArc
是用于绘制弧形(圆弧)的一种方法,具体参数在前几篇文章已介绍,再次不做赘述。这里主要自定义了进度开始的角度,即 startAngle
属性。
textPaint.setTextAlign(Paint.Align.CENTER)
重点关注下文本画笔的这个属性,在前几篇文章中都是通过计算基线的方式确保文本水平居中的,本篇文章的实现方式不同,先来看下setTextAlign
的作用:
textPaint.setTextAlign(Paint.Align.CENTER)
是 Paint 类中的一个设置文本对齐方式的方法。它用于控制文本在指定位置绘制时的对齐方式,具体来说,它影响了绘制文本时文本的起始点(x,y 坐标)如何被确定。
作用:
Paint.Align
是一个枚举,定义了文本绘制时如何对齐相对于给定的 x
和 y
坐标。常用的对齐方式有三个选项:
-
Paint.Align.LEFT
:- 文本绘制时,文本的起始位置在
x
坐标上。 - 也就是说,文本的左边缘会对齐到
x
坐标。
- 文本绘制时,文本的起始位置在
-
Paint.Align.CENTER
:- 文本绘制时,文本的中心位置与
x
坐标对齐。 - 这意味着文本的中点(水平中心)会与
x
坐标对齐,从而实现水平居中。
- 文本绘制时,文本的中心位置与
-
Paint.Align.RIGHT
:- 文本绘制时,文本的右边缘会对齐到
x
坐标。 - 也就是说,文本的右边缘会对齐到指定的
x
坐标,x
坐标即为文本的终点。
- 文本绘制时,文本的右边缘会对齐到
所以说现在我们不用计算基线,而是给drawText
一个中心的 x
坐标,文字就能做到水平居中了!
4. 定义自定义属性
<declare-styleable name="CircularProgressBar">
<!-- 进度条的最大值 -->
<attr name="maxProgress" format="integer"/>
<!-- 当前进度 -->
<attr name="progress" format="integer"/>
<!-- 环形进度条的背景色 -->
<attr name="circleBackgroundColor" format="color"/>
<!-- 进度条的颜色 -->
<attr name="progressColor" format="color"/>
<!-- 进度条的宽度 -->
<attr name="circleWidth" format="dimension"/>
<!-- 显示进度文本 -->
<attr name="showProgressText" format="boolean"/>
<!-- 进度文本的颜色 -->
<attr name="progressTextColor" format="color"/>
<!-- 进度文本的大小 -->
<attr name="progressTextSize" format="dimension"/>
<!-- 开始角度 -->
<attr name="startAngle" format="enum">
<enum name="angle0" value="0"/>
<enum name="angle90" value="90"/>
<enum name="angle180" value="180"/>
<enum name="angle270" value="270"/>
</attr>
</declare-styleable>
5. 完整代码
public class CircularProgressBar extends View {
private Paint backgroundPaint;
private Paint progressPaint;
private Paint textPaint;
private RectF rectF;
private int maxProgress = 100;
private int progress = 0;
private int circleBackgroundColor = Color.GRAY;
private int progressColor = Color.GREEN;
private int circleWidth = 20;
private boolean showProgressText = true;
private int progressTextColor = Color.BLACK;
private int progressTextSize = 50;
private int startAngle = 0; // 起始角度,默认从0开始
public CircularProgressBar(Context context) {
this(context, null);
}
public CircularProgressBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircularProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 初始化自定义属性
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.CircularProgressBar,
0, 0
);
try {
maxProgress = typedArray.getInt(R.styleable.CircularProgressBar_maxProgress, 100);
progress = typedArray.getInt(R.styleable.CircularProgressBar_progress, 0);
circleBackgroundColor = typedArray.getColor(R.styleable.CircularProgressBar_circleBackgroundColor, Color.GRAY);
progressColor = typedArray.getColor(R.styleable.CircularProgressBar_progressColor, Color.GREEN);
circleWidth = typedArray.getDimensionPixelSize(R.styleable.CircularProgressBar_circleWidth, 20);
showProgressText = typedArray.getBoolean(R.styleable.CircularProgressBar_showProgressText, true);
progressTextColor = typedArray.getColor(R.styleable.CircularProgressBar_progressTextColor, Color.BLACK);
progressTextSize = typedArray.getDimensionPixelSize(R.styleable.CircularProgressBar_progressTextSize, 50);
// 获取自定义的起始角度
int angleValue = typedArray.getInt(R.styleable.CircularProgressBar_startAngle, 0);
switch (angleValue) {
case 90:
startAngle = 90;
break;
case 180:
startAngle = 180;
break;
case 270:
startAngle = 270;
break;
default:
startAngle = 0;
break;
}
} finally {
typedArray.recycle();
}
}
// 设置画笔属性
backgroundPaint = new Paint();
backgroundPaint.setColor(circleBackgroundColor);
backgroundPaint.setStyle(Paint.Style.STROKE);
backgroundPaint.setStrokeWidth(circleWidth);
backgroundPaint.setAntiAlias(true);
progressPaint = new Paint();
progressPaint.setColor(progressColor);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeWidth(circleWidth);
progressPaint.setStrokeCap(Paint.Cap.ROUND);//圆角
progressPaint.setAntiAlias(true);
textPaint = new Paint();
textPaint.setColor(progressTextColor);
textPaint.setTextSize(progressTextSize);
textPaint.setTextAlign(Paint.Align.CENTER);//文本对齐方式
textPaint.setAntiAlias(true);
rectF = new RectF();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int padding = circleWidth / 2;
rectF.set(padding, padding, w - padding, h - padding);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制背景圆环
canvas.drawArc(rectF, 0f, 360f, false, backgroundPaint);
// 绘制进度圆环
float sweepAngle = 360f * progress / (float) maxProgress;
// 根据起始角度调整起始位置
float adjustedStartAngle = 0f;
switch (startAngle) {
case 90:
adjustedStartAngle = 0f;
break;
case 180:
adjustedStartAngle = 90f;
break;
case 270:
adjustedStartAngle = 180f;
break;
default:
adjustedStartAngle = -90f; // 默认从0度(顶部)开始
break;
}
canvas.drawArc(rectF, adjustedStartAngle, sweepAngle, false, progressPaint);
// 绘制进度文本
if (showProgressText) {
String progressText = progress + "%";
// 使用视图的中心来计算x和y坐标
float x = getWidth() / 2f;
float y = getHeight() / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;
// 绘制文本
canvas.drawText(progressText, x, y, textPaint);
}
}
// 设置进度
public void setProgress(int progress) {
if (progress > maxProgress) {
this.progress = maxProgress;
} else {
this.progress = Math.max(progress, 0);
}
invalidate(); // 重新绘制
}
// 获取当前进度
public int getProgress() {
return progress;
}
}
6. 使用示例
xml:
<com.xaye.example.CircularProgressBar
android:id="@+id/circularProgressBar"
android:layout_width="140dp"
android:layout_height="140dp"
app:maxProgress="100"
app:circleBackgroundColor="#DDDDDD"
app:progressColor="#00B8D4"
app:circleWidth="15dp"
app:showProgressText="true"
app:progressTextColor="#000000"
app:progressTextSize="30sp"
app:startAngle="angle0"/>
Activity:
val animator = ValueAnimator.ofInt(0, 80)
animator.setDuration(2000)
animator.interpolator = LinearInterpolator()
animator.addUpdateListener { animation ->
val value = animation.animatedValue as Int
mBind.circularProgressBar.setProgress(value)
}
animator.start()
7. 最后
本篇没什么难度,主要是介绍点新的东西,再熟悉熟悉手感,再会。
另外给喜欢记笔记的同学安利一款好用的云笔记软件,对比大部分国内的这个算还不错的,免费好用:wolai