关于过渡色环形圆角进图条的实现

进度条是平时开发中再常用不过的效果了,本篇旨在记录总结此类进度条的几种实现方式。

示例图

使用shape实现

在正常情况下,能用shape来实现的效果,我们是不会采用自定义View的,毕竟自定义view需要实现组件的测量/布局/绘制等流程,而使用shape则不用如此繁琐。

shape中最常用的是类型是矩形rectangle,配合圆角来实现组件边框类的场景效果。 而相对冷门的ring则鲜有使用,因为环形边框的效果通过rectangle配合corners和stroke也能实现,但是像本次要实现的这种过渡色环形,则刚好适合ring来实现。

ring类型的shape使用起来非常简单

        <shape
            android:shape="ring"
            android:innerRadius="60dp"  内边框半径
            android:thickness="15dp"  边框宽度
            android:useLevel="false">
            <gradient
                android:endColor="#00ff0000"
                android:startColor="#F00"
                android:type="sweep" />环形过渡色
        </shape>

如此即可画出一个简单环形过渡色的效果。 但是这个效果离我们的预期还有一定差距,我们想要颜色最重的一端是圆弧形状的,这里就要用到另外一个标签layer-list,来进行叠加绘制。

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!--绘制环形shape-->
    <item>
        <shape
            android:innerRadius="60dp"
            android:shape="ring"
            android:thickness="15dp"
            android:useLevel="false">
            <gradient
                android:endColor="#00ff0000"
                android:startColor="#F00"
                android:type="sweep" />
        </shape>
    </item>
<!--在叠加绘制一个小的圆形-->
    <item
        android:left="135dp"
        android:top="67.5dp"
        android:bottom="67.5dp">
        <shape
            >
            <size android:height="15dp" android:width="15dp"/>
            <corners android:radius="7.5dp"/>
            <solid android:color="#F00"/>
        </shape>
    </item>
</layer-list>

在布局文件中将这个shape设为background并设宽高为150dp,完美。 想要这个view旋转起来,只需要我定义一个ObjectAnimator来控制即可。

        ObjectAnimator rotation1 = ObjectAnimator.ofFloat(ringView, "rotation", -360);
        rotation1.setInterpolator(new LinearInterpolator());
        rotation1.setRepeatCount(ValueAnimator.INFINITE);
        rotation1.setDuration(1500);
        rotation1.start();

自定义View实现环形效果

在某些情况下,需要在程序运行期间需要动态的设置上述效果的颜色或边框宽度等属性的话,单纯的使用shape就满足不了我们的需求了。这时候我们需要我自定义View来复刻这个效果。 首先是自定义View的三件套

  • 构造方法调用init初始化笔刷等资源
  • 复写onMeasure拿到宽高
  • 复写onDraw进行绘制
    Paint paintRing = new Paint();//环形笔刷
    Paint paintEnds = new Paint();//终点笔刷
    int[] ringColors = new int[]{
            Color.RED,
            Color.parseColor("#00FF0000")
    };
    private void init() {
        paintRing.setColor(Color.RED);
        paintRing.setStrokeWidth(ringWidth);
        paintRing.setStyle(Paint.Style.STROKE);
        paintRing.setColor(Color.WHITE);
        paintRing.setAntiAlias(true);
        paintEnds.setAntiAlias(true);
        paintEnds.setStyle(Paint.Style.FILL);
        paintEnds.setColor(Color.RED);
    }

这里初始化了两个笔刷,分别用于绘制过渡色的环形和终点的弧形效果。 颜色则定义了一个int型的数组用于实现过渡色,当然这里可以根据特殊需求放很多个颜色,这样是自定义View的优势。 绘制

    @Override
    protected void onDraw(Canvas canvas) {
        int offsetX = Math.abs(width - height) / 2;
        int offsetY = Math.abs(height - width) / 2;
        int left = (width > height ? offsetX : 0) + ringWidth / 2;
        int top = (height > width ? offsetY : 0) + ringWidth / 2;
        int right = width - (width > height ? offsetX : 0) - ringWidth / 2;
        int bottom = height - (height > width ? offsetY : 0) - ringWidth / 2;
        RectF rectF = new RectF(left, top, right, bottom);

        paintRing.setShader(new SweepGradient(width / 2, height / 2, ringColors, null));

        canvas.drawArc(rectF, 0, 360, false, paintRing);
        canvas.drawCircle(width - ringWidth / 2, 1f * height / 2, ringWidth / 2, paintEnds);
    }

在onDraw方法中计算了圆环居中情况的边界Rect,并根据颜色数组创建了过渡色的shader,将shader赋值给笔刷绘制出过渡色的圆环,最后在最右测画出一个圆形,达到终端圆弧的效果。 当然,这个view依然是一个静态的,想让他转起来的话使用ObjectAnimator,和上面方法一致。

不封闭的环形

在开篇中展示的不封闭的环形效果比上面实现的完整圆形稍微复杂一些,需要额外处理一些细节。

  • 背景的白色环形
  • 背景和彩色进度两端的弧形处理
  • 旋转画布让过渡色的起始结束位置调整到底部中心

依然是自定义View的三件套

  • 构造方法调用init初始化笔刷等资源
  • 复写onMeasure拿到宽高
  • 复写onDraw进行绘制

初始化笔刷

    private void init() {
        //白色背景弧线
        paintBg.setColor(Color.WHITE);
        paintBg.setStrokeWidth(strokeWidth);
        paintBg.setStyle(Paint.Style.STROKE);
        paintBg.setAntiAlias(true);
        //白色背景两个端点
        paintBgEnds.setColor(Color.WHITE);
        paintBgEnds.setAntiAlias(true);
        //彩色进度弧线
        paintProgress.setColor(Color.RED);
        paintProgress.setStrokeWidth(strokeWidth);
        paintProgress.setStyle(Paint.Style.STROKE);
        paintProgress.setAntiAlias(true);
        //彩色进度端点
        getPaintProgressEnds.setColor(Color.RED);
        getPaintProgressEnds.setAntiAlias(true);
    }

在onMeasure中测量得到能刚好在view中绘制弧形的尺寸

    float circleSize = 0;
    private int height;
    private int width;
    RectF rect = new RectF();
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        height = getMeasuredHeight();
        width = getMeasuredWidth();
        int size = Math.min(width, height);
        rect.left = strokeWidth / 2f;
        rect.top = strokeWidth / 2f;
        rect.right = size - strokeWidth / 2f;
        rect.bottom = size - strokeWidth / 2f;
        circleSize = (width - strokeWidth) / 2f;
    }

从宽高中取最小值作为圆形的外环半径 减去环形宽度的一半作为绘制圆形的真实尺寸 进行绘制

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int centerX = width / 2;
        int centerY = height / 2;
        //白色背景左圆端点
        float startX = (float) (circleSize * Math.sin(300 * Math.PI / 180));
        float startY = (float) (circleSize * Math.cos(300 * Math.PI / 180));
        paintBgEnds.setColor(Color.WHITE);
        canvas.drawCircle(centerX + startX, centerY + startY, strokeWidth / 2f, paintBgEnds);
        //白色背景右圆端点
        float endX = (float) (circleSize * Math.sin(60 * Math.PI / 180));
        float endY = (float) (circleSize * Math.cos(60 * Math.PI / 180));
        paintBgEnds.setColor(Color.WHITE);
        canvas.drawCircle(centerX + endX, centerY + endY, strokeWidth / 2f, paintBgEnds);
        //白色弧形背景
        canvas.drawArc(rect, 150, 240, false, paintBg);
        //彩色进度左圆端点
        int currentLastColor = getCurrentColor(60f / 360, colors);
        paintBgEnds.setColor(currentLastColor);
        canvas.drawCircle(centerX + startX, centerY + startY, strokeWidth / 2f, paintBgEnds);
        //彩色进度右圆端点
        float currentX = (float) (circleSize * Math.sin((60 + 240 * (1 - progress)) * Math.PI / 180));
        float currentY = (float) (circleSize * Math.cos((60 + 240 * (1 - progress)) * Math.PI / 180));
        currentLastColor = getCurrentColor((300f - 240 * (1 - progress)) / 360, colors);
        paintBgEnds.setColor(currentLastColor);
        canvas.drawCircle(centerX + currentX, centerY + currentY, strokeWidth / 2f, paintBgEnds);
        //绘制彩色弧形进度
        paintProgress.setShader(new SweepGradient(width / 2f, height / 2f, colors, null));
        canvas.rotate(90, width / 2f, height / 2);
        canvas.drawArc(rect, 60, 240 * progress, false, paintProgress);//进度
    }

在上面的onDraw方法中依次进行了如下绘制过程

  • 计算出最大范围的起止点,并使绘制出圆形白点。
  • 绘制出白色的弧形,将两个圆点连接起来作为背景。
  • 根据progress进度计算出进度终点的实时位置,并画点。
  • 根据progress计算弧度的长度进行绘制。

这个绘制中有一个细节,在绘制进度终点的时候获取了实时的颜色,颜色的获取是通过算法计算出来的。

public static int getCurrentColor(float percent, int[] colors) {
        float[][] f = new float[colors.length][3];
        for (int i = 0; i < colors.length; i++) {
            f[i][0] = (colors[i] & 0xff0000) >> 16;
            f[i][1] = (colors[i] & 0x00ff00) >> 8;
            f[i][2] = (colors[i] & 0x0000ff);
        }
        float[] result = new float[3];
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < f.length; j++) {
                if (f.length == 1 || percent == j / (f.length - 1f)) {
                    result = f[j];
                } else {
                    if (percent > j / (f.length - 1f) && percent < (j + 1f) / (f.length - 1)) {
                        result[i] = f[j][i] - (f[j][i] - f[j + 1][i]) * (percent - j / (f.length - 1f)) * (f.length - 1f);
                    }
                }
            }
        }
        return Color.rgb((int) result[0], (int) result[1], (int) result[2]);
    }

至于如何让进度条的进度动起来,将progress通过get/set方法开放出去,在set方法中写入postInvalidate,在外部通过ObjectAnimator控制即可。

小结

使用shape定义环形虽然方便,但是可定制化非常有限。 而使用自定义View来实现虽然繁琐,但是可玩性非常高,能够实现各种酷炫的效果,以满足真实的开发场景。

文章目录
,