Android自定义控件实现颜色渐变式圆形进度

王春蕾

先睹为快,自定义控件实现的效果如下图所示:

这种控件的效果还是很常见的,但限于Android系统并没有提供这种效果的控件,所以只能自己动手丰衣足食啦!

Android项目中有些因为产品需求或者UI效果图的要求,需要实现一些Android系统源生控件没有提供的效果时,就需要自定义控件实现相关效果了,因此自定义控件也是作为Android开发的一项基本功。本文就基于继承于View并使用Canvas绘制图形的方式介绍自定义控件的相关流程。

1.自定义CircleProgressBar,继承View,并实现响应的构造函数

代码如下:

/**
 * Created by WangChunLei
 */
public class GradientProgressBar extends View {  
     public GradientProgressBar(Context context) {
        super(context);
        init();
    }

    public GradientProgressBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public GradientProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
}

其中init方法是对相关画笔进行初始化的方法,init方法代码如下:

    private void init() {
        backCirclePaint = new Paint();
        backCirclePaint.setStyle(Paint.Style.STROKE);
        backCirclePaint.setAntiAlias(true);
        backCirclePaint.setColor(Color.LTGRAY);
        backCirclePaint.setStrokeWidth(circleBorderWidth);
//        backCirclePaint.setMaskFilter(new BlurMaskFilter(20, BlurMaskFilter.Blur.OUTER));

        gradientCirclePaint = new Paint();
        gradientCirclePaint.setStyle(Paint.Style.STROKE);
        gradientCirclePaint.setAntiAlias(true);
        gradientCirclePaint.setColor(Color.LTGRAY);
        gradientCirclePaint.setStrokeWidth(circleBorderWidth);

        linePaint = new Paint();
        linePaint.setColor(Color.WHITE);
        linePaint.setStrokeWidth(5);

        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(textSize);
        textPaint.setColor(Color.BLACK);
    }

2.测量控件的宽高-onMeasure

onMeasure是自定义控件的第一步,目的就是测量得到该控件应该占有的宽高尺寸。其中onMeasure方法的代码如下:

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(Math.min(measureWidth, measureHeight), Math.min(measureWidth, measureHeight));
    }

贴上onMeasure的代码后,大家估计是很少见过测量过程这么简单的onMeasure,不要介意,有兴趣的同僚们可以细化一下这个测量过程,对不同的测量模式分别进行处理和测量,让控件适配效果更好更完善! onMeasure方法中,分别获取期望的宽度和高度,并取其中较小的尺寸作为该控件的宽和高。

3.依次绘制不同的控件组成部分。

因为控件是直接继承自View,所以不需要再处理onLayout方法,这也是自定义View与自定义ViewGroup的其中一个区别,但继承ViewGroup也并不一定要重写onMeasure。

要实现如图所示的效果,需要分以下步骤依次实现
    (1)绘制灰色空心圆环
    (2)绘制颜色渐变的圆环
    (3)绘制圆环上分割的白色线条
    (4)绘制百分比文字等。

绘制过程过,后绘制的内容如果与之前绘制的内容存在交集,则后绘制的内容会覆盖掉之前绘制的内容。 按照上述步骤依次介绍 在绘制过程中,会产生以下成员变量,下文中会用到:

/*圆弧线宽*/
    private float circleBorderWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics());
    /*内边距*/
    private float circlePadding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics());
    /*字体大小*/
    private float textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 50, getResources().getDisplayMetrics());
    /*绘制圆周的画笔*/
    private Paint backCirclePaint;
    /*绘制圆周白色分割线的画笔*/
    private Paint linePaint;
    /*绘制文字的画笔*/
    private Paint textPaint;
    /*百分比*/
    private int percent = 0;
    /*渐变圆周颜色数组*/
    private int[] gradientColorArray = new int[]{Color.GREEN, Color.parseColor("#fe751a"), Color.parseColor("#13be23"), Color.GREEN};
    private Paint gradientCirclePaint;

3.1绘制灰色空心圆环

代码如下:

//1.绘制灰色背景圆环
        canvas.drawArc(
                new RectF(circlePadding * 2, circlePadding * 2,
                        getMeasuredWidth() - circlePadding * 2, getMeasuredHeight() - circlePadding * 2), -90, 360, false, backCirclePaint);

其中,-90为绘制圆弧的起始角度,360是圆弧绘制的角度,即sweepAngle.

3.2绘制颜色渐变的圆环

//2.绘制颜色渐变圆环
        LinearGradient linearGradient = new LinearGradient(circlePadding, circlePadding,
                getMeasuredWidth() - circlePadding,
                getMeasuredHeight() - circlePadding,
                gradientColorArray, null, Shader.TileMode.MIRROR);
        gradientCirclePaint.setShader(linearGradient);
        gradientCirclePaint.setShadowLayer(10, 10, 10, Color.RED);
        canvas.drawArc(
                new RectF(circlePadding * 2, circlePadding * 2,
                        getMeasuredWidth() - circlePadding * 2, getMeasuredHeight() - circlePadding * 2), -90, (float) (percent / 100.0) * 360, false, gradientCirclePaint);

其中,linearGradient是Paint的shadow,是为了圆弧的颜色渐变效果的而需要设置的,日常开发中应用频率不高,但的确是可以实现非常理想的颜色渐变效果。

3.3绘制圆环上分割的白色线条

绘制圆弧上的白色线条时,需要进行一些简单的运算,比如线条的起始坐标startX,startY和线条的终止坐标stopX,stopY等,利用简单的三角函数还是很容易去计算出来的。 效果中,将圆弧使用白色线条平分成100分,每一个的阶级为1,可以满足int类型的百分比与效果图比例的一致。

        //半径
        float radius = (getMeasuredWidth() - circlePadding * 3) / 2;
        //X轴中点坐标
        int centerX = getMeasuredWidth() / 2;

        //3.绘制100份线段,切分空心圆弧
        for (float i = 0; i < 360; i += 3.6) {
            double rad = i * Math.PI / 180;
            float startX = (float) (centerX + (radius - circleBorderWidth) * Math.sin(rad));
            float startY = (float) (centerX + (radius - circleBorderWidth) * Math.cos(rad));

            float stopX = (float) (centerX + radius * Math.sin(rad) + 1);
            float stopY = (float) (centerX + radius * Math.cos(rad) + 1);

            canvas.drawLine(startX, startY, stopX, stopY, linePaint);
        }

3.4绘制百分比文字等 最后绘制百分比文字。 绘制文字时,为了保持文字的中心点和圆弧的原点一致,需要先测量得到要显示文字的宽度和高度,然后再进行一些简单的运算,原理不再赘述,相信大家数学一定都比我好。

        //4.绘制文字
        float textWidth = textPaint.measureText(percent + "%");
        int textHeight = (int) (Math.ceil(textPaint.getFontMetrics().descent - textPaint.getFontMetrics().ascent) + 2);
        canvas.drawText(percent + "%", centerX - textWidth / 2, centerX + textHeight / 4, textPaint);

最后,暴漏一个公共的方法供改变显示的百分比,代码如下:

/**
     * 设置百分比
     *
     * @param percent
     */
    public void setPercent(int percent) {
        if (percent < 0) {
            percent = 0;
        } else if (percent > 100) {
            percent = 100;
        }

        this.percent = percent;
        invalidate();
    }

至此,所有绘制过程简述完毕,130行代码就能实现很炫酷的效果有木有? 最后,贴上项目完整代码,供懒得看实现过程的同学们使用,O(∩_∩)O哈哈~

package com.example.myview;

import android.content.Context;  
import android.graphics.*;  
import android.util.AttributeSet;  
import android.util.TypedValue;  
import android.view.View;

/**
 * Created by WangChunLei
 */
public class GradientProgressBar extends View {  
    /*圆弧线宽*/
    private float circleBorderWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics());
    /*内边距*/
    private float circlePadding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics());
    /*字体大小*/
    private float textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 50, getResources().getDisplayMetrics());
    /*绘制圆周的画笔*/
    private Paint backCirclePaint;
    /*绘制圆周白色分割线的画笔*/
    private Paint linePaint;
    /*绘制文字的画笔*/
    private Paint textPaint;
    /*百分比*/
    private int percent = 0;
    /*渐变圆周颜色数组*/
    private int[] gradientColorArray = new int[]{Color.GREEN, Color.parseColor("#fe751a"), Color.parseColor("#13be23"), Color.GREEN};
    private Paint gradientCirclePaint;

    public GradientProgressBar(Context context) {
        super(context);
        init();
    }

    public GradientProgressBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public GradientProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        backCirclePaint = new Paint();
        backCirclePaint.setStyle(Paint.Style.STROKE);
        backCirclePaint.setAntiAlias(true);
        backCirclePaint.setColor(Color.LTGRAY);
        backCirclePaint.setStrokeWidth(circleBorderWidth);
//        backCirclePaint.setMaskFilter(new BlurMaskFilter(20, BlurMaskFilter.Blur.OUTER));

        gradientCirclePaint = new Paint();
        gradientCirclePaint.setStyle(Paint.Style.STROKE);
        gradientCirclePaint.setAntiAlias(true);
        gradientCirclePaint.setColor(Color.LTGRAY);
        gradientCirclePaint.setStrokeWidth(circleBorderWidth);

        linePaint = new Paint();
        linePaint.setColor(Color.WHITE);
        linePaint.setStrokeWidth(5);

        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(textSize);
        textPaint.setColor(Color.BLACK);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(Math.min(measureWidth, measureHeight), Math.min(measureWidth, measureHeight));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //1.绘制灰色背景圆环
        canvas.drawArc(
                new RectF(circlePadding * 2, circlePadding * 2,
                        getMeasuredWidth() - circlePadding * 2, getMeasuredHeight() - circlePadding * 2), -90, 360, false, backCirclePaint);
        //2.绘制颜色渐变圆环
        LinearGradient linearGradient = new LinearGradient(circlePadding, circlePadding,
                getMeasuredWidth() - circlePadding,
                getMeasuredHeight() - circlePadding,
                gradientColorArray, null, Shader.TileMode.MIRROR);
        gradientCirclePaint.setShader(linearGradient);
        gradientCirclePaint.setShadowLayer(10, 10, 10, Color.RED);
        canvas.drawArc(
                new RectF(circlePadding * 2, circlePadding * 2,
                        getMeasuredWidth() - circlePadding * 2, getMeasuredHeight() - circlePadding * 2), -90, (float) (percent / 100.0) * 360, false, gradientCirclePaint);

        //半径
        float radius = (getMeasuredWidth() - circlePadding * 3) / 2;
        //X轴中点坐标
        int centerX = getMeasuredWidth() / 2;

        //3.绘制100份线段,切分空心圆弧
        for (float i = 0; i < 360; i += 3.6) {
            double rad = i * Math.PI / 180;
            float startX = (float) (centerX + (radius - circleBorderWidth) * Math.sin(rad));
            float startY = (float) (centerX + (radius - circleBorderWidth) * Math.cos(rad));

            float stopX = (float) (centerX + radius * Math.sin(rad) + 1);
            float stopY = (float) (centerX + radius * Math.cos(rad) + 1);

            canvas.drawLine(startX, startY, stopX, stopY, linePaint);
        }

        //4.绘制文字
        float textWidth = textPaint.measureText(percent + "%");
        int textHeight = (int) (Math.ceil(textPaint.getFontMetrics().descent - textPaint.getFontMetrics().ascent) + 2);
        canvas.drawText(percent + "%", centerX - textWidth / 2, centerX + textHeight / 4, textPaint);
    }

    /**
     * 设置百分比
     *
     * @param percent
     */
    public void setPercent(int percent) {
        if (percent < 0) {
            percent = 0;
        } else if (percent > 100) {
            percent = 100;
        }

        this.percent = percent;
        invalidate();
    }
}