每次看到别人做出炫酷的都会想,这个应该很难吧?这是心理上先入为主的就这么认为了,其实实现很简单,下面一步一步的详细剖析自定义圆形进度条的步骤。

首先看效果图:



圆形进度条swift 圆形进度条变化_Text


篇幅有点长,耐心看完肯定get新技能。

看每一个视图都包含了些什么。

  • 最里层一个蓝色圆形
  • 中间一层显示进度的橙色扇形圆弧
  • 最外层一个红色圆环
  • 显示进度百分比的文字以及下方提示文字

下面来一步一步实现:

  1. 创建一个类继承View,并实现几个构造方法
  2. 定义样式属性,获取属性值
  3. 创建画笔
  4. 重写onDraw()绘制
  5. 应用

直接从第二步开始: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()绘制出一个圆。
如下图:

圆形进度条swift 圆形进度条变化_ci_02

// getWidth为当前View的宽度,即上面设置的100dp
float circleRadius = getWidth() / 2;
// 参数:(圆心X坐标,圆心Y坐标,半径,画笔)
canvas.drawCircle(circleRadius, circleRadius, circleRadius, circlePaint);

这就绘制出了直径为100dp的圆,为灰色矩形的内切圆(矩形加上背景作为对比),如图:



圆形进度条swift 圆形进度条变化_安卓_03

        接着再在圆形外面画一个紧贴着的圆环(圆心应该与蓝色圆保持一致),假设圆环的宽度为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个参数是用来确定圆环四个边的位置的,如下图:


圆形进度条swift 圆形进度条变化_安卓_04

所以在灰色矩形中并且在蓝色圆形外画圆环就可以这样画,确定圆环的四个顶点位置并且将蓝色圆的半径缩小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);


圆形进度条swift 圆形进度条变化_圆形进度条_05


从上图可以看到,圆环是画出来了,也是在蓝色圆形外面,但是感觉好像哪里不对。圆环的宽度有一半好像在矩形外面去了,而圆环与圆之间有空隙,圆环半径应该再缩小圆环宽度/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);



圆形进度条swift 圆形进度条变化_ci_06


嗯,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);


圆形进度条swift 圆形进度条变化_安卓_07


Very Nice!!非常简单

接下来就是绘制文字了,将显示进度百分比的文字绘制在圆形的正中央。
绘制文字当然是用canvas.drawText()了,来看看它的几个参数

drawText(String text, float x, float y, Paint paint)

第一个参数是要绘制的文字,第四个参数是画笔,中间2个参数x,y不知道没关系,我们先将x,y设置成圆心的坐标试试看。



圆形进度条swift 圆形进度条变化_ci_08


我们得到了如图左侧的效果,但是想要的是右侧的效果。对比可知,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);

绘制完进度,接下来该继续绘制提示文字了。

圆形进度条swift 圆形进度条变化_圆形进度条_09


将“当前进度”放在下方圆半径中间的位置,根据以上经验可以轻松的写出代码:

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的值