每次看到别人做出炫酷的都会想,这个应该很难吧?这是心理上先入为主的就这么认为了,其实实现很简单,下面一步一步的详细剖析自定义圆形进度条的步骤。
首先看效果图:
篇幅有点长,耐心看完肯定get新技能。
看每一个视图都包含了些什么。
- 最里层一个蓝色圆形
- 中间一层显示进度的橙色扇形圆弧
- 最外层一个红色圆环
- 显示进度百分比的文字以及下方提示文字
下面来一步一步实现:
- 创建一个类继承View,并实现几个构造方法
- 定义样式属性,获取属性值
- 创建画笔
- 重写onDraw()绘制
- 应用
直接从第二步开始:res->values下创建attrs.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleProgressView">
// 里层实心圆颜色
<attr name="circleColor" format="color" />
// 中间圆环(宽度/颜色)
<attr name="progressWidth" format="dimension" />
<attr name="progressColor" format="color" />
// 外层圆环(宽度/颜色)
<attr name="sectorWidth" format="dimension" />
<attr name="sectorColor" format="color" />
// 中间进度文字(颜色/大小)
<attr name="proTextColor" format="color" />
<attr name="proTextSize" format="dimension" />
// 中间提示文字(颜色/大小/文本内容)
<attr name="tipTextColor" format="color" />
<attr name="tipTextSize" format="dimension" />
<attr name="tipText" format="string" />
// 最大进度
<attr name="max" format="integer" />
// 是否显示最外层的圆环
<attr name="showStoke" format="boolean" />
// 进度圆环是否在圆上
<attr name="isAbove" format="boolean" />
// 进度是否滚动
<attr name="isScroll" format="boolean" />
</declare-styleable>
</resources>
定义完属性后该获取定义的属性了。 注:上方的文字大小和宽度必须用dimension[尺寸],而不能用float 声明需要的变量
private Paint circlePaint; // 最里层实心圆画笔
private int circleColor; // 实心圆颜色
private Paint progressPaint; // 中间显示进度圆环画笔
private float progressWidth; // 进度圆环宽度
private int progressColor; // 进度圆环颜色
private Paint sectorPaint; // 最外层圆环画笔
private float sectorWidth; // 外层圆环宽度
private int sectorColor; // 外层圆环颜色
private Paint proTextPaint;
private float proTextSize;
private int proTextColor;
private Paint tipTextPaint;
private float tipTextSize;
private int tipTextColor;
private String tipText;
private int currProgress; // 当前进度
private int maxProgress; // 最大进度
private boolean isShow;
private boolean isAbove;
private boolean isScroll;
获取属性
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CircleProgressView, 0, 0);
circleColor = typedArray.getColor(R.styleable.CircleProgressView_circleColor, 0x993F51B5);
progressWidth = typedArray.getDimension(R.styleable.CircleProgressView_progressWidth, 300);
progressColor = typedArray.getColor(R.styleable.CircleProgressView_progressColor, 0x3F51B5);
sectorWidth = typedArray.getDimension(R.styleable.CircleProgressView_sectorWidth, 10);
sectorColor = typedArray.getColor(R.styleable.CircleProgressView_sectorColor, 0xFF4081);
proTextSize = typedArray.getDimension(R.styleable.CircleProgressView_proTextSize, 60);
proTextColor = typedArray.getColor(R.styleable.CircleProgressView_proTextColor, 0xFFFFFF);
tipTextSize = typedArray.getDimension(R.styleable.CircleProgressView_tipTextSize, 30);
tipTextColor = typedArray.getColor(R.styleable.CircleProgressView_tipTextColor, 0xFFFFFF);
tipText = typedArray.getString(R.styleable.CircleProgressView_tipText);
maxProgress = typedArray.getInteger(R.styleable.CircleProgressView_max, 100);
isShow = typedArray.getBoolean(R.styleable.CircleProgressView_showStoke, true);
isAbove = typedArray.getBoolean(R.styleable.CircleProgressView_isAbove, false);
isScroll = typedArray.getBoolean(R.styleable.CircleProgressView_isScroll, false);
typedArray.recycle();
}
创建5个画笔(1个圆,2个圆环,2个文本),然后在参数最多的构造器中调用
private void initPaint() {
// 圆形画笔
circlePaint = new Paint();
circlePaint.setAntiAlias(true);
circlePaint.setStyle(Paint.Style.FILL);
// 进度圆环画笔
progressPaint = new Paint();
progressPaint.setAntiAlias(true);
progressPaint.setStyle(Paint.Style.STROKE);
// 最外层圆环画笔
sectorPaint = new Paint();
sectorPaint.setAntiAlias(true);
sectorPaint.setStyle(Paint.Style.STROKE);
// 进度文字画笔
proTextPaint = new Paint();
proTextPaint.setAntiAlias(true);
proTextPaint.setStyle(Paint.Style.FILL);
// 提示文字画笔
tipTextPaint = new Paint();
tipTextPaint.setAntiAlias(true);
tipTextPaint.setStyle(Paint.Style.FILL);
}
下面就是最重要的绘制过程了
1、首先将获取到的自定义属性值设置给每个画笔,在onDraw()方法中调用
2、绘制最里面的蓝色圆形,确定圆心和半径。假设在xml布局中引用了这个View并设置width和height各位100dp,那么圆心就应该在视图的中心位置(50,50),半径则是宽度或者高度的一半(50dp),知道了圆形和半径就可以用canvas.drawCicle()绘制出一个圆。
如下图:
// getWidth为当前View的宽度,即上面设置的100dp
float circleRadius = getWidth() / 2;
// 参数:(圆心X坐标,圆心Y坐标,半径,画笔)
canvas.drawCircle(circleRadius, circleRadius, circleRadius, circlePaint);
这就绘制出了直径为100dp的圆,为灰色矩形的内切圆(矩形加上背景作为对比),如图:
接着再在圆形外面画一个紧贴着的圆环(圆心应该与蓝色圆保持一致),假设圆环的宽度为sectorWidth = 5,想要圆环绘制在灰色矩形内,那么蓝色圆形的半径就应该要缩小,那么需要缩小多少呢?先缩小圆环的宽度来试试。
绘制圆环用的是
canvas.drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
需要传入5个参数,oval是一个矩形,startAngle为圆环开始的角度,3点钟的方向为0度,sweepAngle为扫过的角度(如360为一周),useCenter为false时绘制的为圆环,为true时绘制的是扇形,paint为画笔。
第一个参数RectF又有几个参数
RectF(float left, float top, float right, float bottom)
当此时我们要在灰色矩形中画圆环,则将灰色矩形左上角的坐标为(0,0),那么这4个参数是用来确定圆环四个边的位置的,如下图:
所以在灰色矩形中并且在蓝色圆形外画圆环就可以这样画,确定圆环的四个顶点位置并且将蓝色圆的半径缩小5,即:
RectF sector = new RectF(0, // left
0, // top
2 * circleRadius, // right
2 * circleRadius); // bottom
// 圆形
canvas.drawCircle(circleRadius, circleRadius, circleRadius - sectorWidth, circlePaint);
// 圆环
canvas.drawArc(sector, 0, 360, false, sectorPaint);
从上图可以看到,圆环是画出来了,也是在蓝色圆形外面,但是感觉好像哪里不对。圆环的宽度有一半好像在矩形外面去了,而圆环与圆之间有空隙,圆环半径应该再缩小圆环宽度/2就刚好填满了空隙,由此我们可以知道 绘制圆环的半径是(蓝色圆的半径+圆环宽度/2),当绘制的圆环有宽度时,圆环的外层要与矩形相切,因此蓝色圆形的半径还需要再缩小圆环宽度/2。
修改以上代码:
RectF sector = new RectF(sectorWidth/2, // left
sectorWidth/2, // top
2 * circleRadius - sectorWidth/2, // right
2 * circleRadius - sectorWidth/2); // bottom
// 圆形
canvas.drawCircle(circleRadius, circleRadius, circleRadius - sectorWidth/2, circlePaint);
// 圆环
canvas.drawArc(sector, 0, 360, false, sectorPaint);
嗯,Perfect!
接下来在圆与圆环之间再画出一个表示进度的圆弧。
思路很简单,最外层的圆环不用动,将圆形半径缩小圆弧宽度/2即可。绘制圆环和圆弧是一致的,只是扫过的角度不一致而已。
// 几个顶点分别离X,Y轴的距离,progressWidth是进度圆弧的宽度
RectF progressRectF = new RectF(sectorWidth + progressWidth / 2,
sectorWidth + progressWidth / 2,
2 * circleRadius - sectorWidth - progressWidth / 2,
2 * circleRadius - sectorWidth - progressWidth / 2);
// -90度从圆的上顶点开始,扫描90度
canvas.drawArc(progressRectF, -90, 90, false, progressPaint);
如果动态的设置进度。
设: progress // 当前进度
max // 最大进度百分比(100%则max = 100)
swapAngel // 扫过的角度
则:swapAngel = (float)progress/max * 360;
canvas.drawArc(progressRectF, -90, swapAngel, false, progressPaint);
Very Nice!!非常简单
接下来就是绘制文字了,将显示进度百分比的文字绘制在圆形的正中央。
绘制文字当然是用canvas.drawText()了,来看看它的几个参数
drawText(String text, float x, float y, Paint paint)
第一个参数是要绘制的文字,第四个参数是画笔,中间2个参数x,y不知道没关系,我们先将x,y设置成圆心的坐标试试看。
我们得到了如图左侧的效果,但是想要的是右侧的效果。对比可知,drawText()中的x,y参数分别指绘制文字左下角的横纵坐标。因此我们需要获取到文字的宽高,
外层圆环半径 - 文字宽度/2,外层圆环半径 + 文字高度/2 就可以将文字移动到最中央。
// 获取文字宽度,proText为绘制的进度文字
int width= proTextPaint.measureText(proText);
// 获取文字高度
Rect rect = new Rect();
proTextPaint.getTextBounds(proText, 0, proText.length(), rect);
int height = rect.height();
获取到宽高之后就可以用drawText绘制出进度文字了。
canvas.drawText(proText, circleRadius - width / 2,
circleRadius + height / 2, proTextPaint);
绘制完进度,接下来该继续绘制提示文字了。
将“当前进度”放在下方圆半径中间的位置,根据以上经验可以轻松的写出代码:
Rect tipRect = new Rect();
tipTextPaint.getTextBounds(tipText, 0, tipText.length(), tipRect);
int tipHeight = tipRect.height();
canvas.drawText(tipText, // 绘制的文字
circleRadius - tipTextPaint.measureText(tipText) / 2,
3 * circleRadius / 2 + tipHeight / 2,
tipTextPaint);
至于动态效果是在一个线程中用了一个临时变量 temp 从0 ~ 设置的进度做循环逐渐增加,然后一次一次的绘制出来,不过感觉这样很消耗性能,有更好的办法欢迎联系我交流交流。具体代码就不展示了,欢迎下载Demo看看。
应用
XML :
在XML根元素中声明命名空间(hcc可换)
xmlns:hcc="http://schemas.android.com/apk/res-auto"
<com.cc.customview.progress.CircleProgressView
android:id="@+id/pv"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
hcc:isAbove="true"
hcc:isScroll="true"
hcc:proTextColor="#FFFFFF"
hcc:proTextSize="30sp"
hcc:progressColor="@color/colorOrange"
hcc:progressWidth="5dp"
hcc:sectorColor="@color/colorAccent"
hcc:showStoke="true"
hcc:tipText="当前进度"
hcc:max="100" // 默认100,可填其他
hcc:tipTextColor="#FFFFFF" />
Java :
pv.setCircleColor(getResources().getColor(R.color.colorPrimary));
pv.setAbove(false);
pv.setScroll(true);
pv.setShow(true);
pv.setProgressColor(getResources().getColor(R.color.colorOrange));
pv.setProTextColor(getResources().getColor(R.color.colorWhite));
pv.setTipTextColor(getResources().getColor(R.color.colorPrimary));
pv.setSectorColor(getResources().getColor(R.color.colorAccent));
pv.setTipText("当前进度");
pv.setTipTextColor(getResources().getColor(R.color.colorWhite));
pv.setProgressWidth(8);
pv.setSectorWidth(5);
pv.setMaxProgress(100); // 默认100,可填其他
CircleProgressView pv= (CircleProgressView) findViewById(R.id.pv);
pv.setProgress(80); // 任意整形大于0的值