一步步做Android自定义圆环百分比控件

马上要校招了,有点儿慌,写个自定义控件压压惊

效果图

本来写了一大串近段时间的感慨,还是觉得废话少说比较不容易被喷,直接上效果图

圆环百分比View.gif

就是这个样子,下面记录一下我的编写经历,撸起袖子就是干

如果不想看实现原理,可以直接拿去用,在我的github上有使用方法,如果能给个star的话,只能说明...你很有前途
github源码

前期准备

说实在的,作为菜鸟一个,每次一听到自定义view依然是觉得写出来的人NB的不行,而自己依然懵逼的不行

1.png

不过看了几篇很好的写view的文章,对自己启发比较大,也推荐给大家有时间可以去看看,不要谢我

Android自定义View的官方套路
我奶奶都能懂的UI绘制流程(上)!
我奶奶都能懂的UI绘制流程(下)!

另外还有一篇,我的这个view也是在他的基础上编写并做了部分修改的,感谢大神
自定义View练习

自定义属性

这里的自定义属性主要就是到时候在xml文件中引入控件时要用到的,在values目录下新建attrs.xml文件,自定义属性如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CirclePercentBar">
        <attr name="arcColor" format="color"/>
        <attr name="arcWidth" format="dimension"/>
        <attr name="centerTextColor" format="color"/>
        <attr name="centerTextSize" format="dimension"/>
        <attr name="circleRadius" format="dimension"/>
        <attr name="arcStartColor" format="color"/>
        <attr name="arcEndColor" format="color"/>
    </declare-styleable>
</resources>

这里面都是比较简单的,具体的是什么意思在这就不啰嗦了,如果有不知道的可以google一下就能知道了

自定义View类

自定义一个CirclePercentBar类继承自View

public class CirclePercentBar extends View{

    public CirclePercentBar(Context context) {
        this(context, null);
    }

    public CirclePercentBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

添加了3个构造函数,这个地方需要注意一下(对于初学者有坑):

如果不需要在xml文件中使用控件,就实现第一个构造函数即可;
如果需要在xml中定义,需要传入属性值的时候,系统将调用第二个构造函数;
第三个系统不会自动去调用,而是用户自己需要的时候去主动调用。
针对这个坑按照习惯(我看都这样写的,所以就叫习惯了...哈哈哈)做了相应的修改,就如上面的代码所示,第一个构造函数去调用第二个,第二个调用第三个,然后把我们初始化的操作都写在第三个构造函数中,别问为什么,我也不知道...以后知道了再来补充,当然就规矩点儿写在第二个里面也应该是不会错的。

获取属性值

既然自定义了属性值,而且用户在xml文件中也做了相应的属性设置,自然要拿到代码中来使用这些属性值了,获取的过程也很简单,都是套路

        TypedArray typedArray=context.obtainStyledAttributes(attrs
                , R.styleable.CirclePercentBar, defStyleAttr,0);
        mArcColor = typedArray.getColor(R.styleable.CirclePercentBar_arcColor
                ,0xff0000);
        mArcWidth = typedArray.getDimensionPixelSize(R.styleable.CirclePercentBar_arcWidth
                , DisplayUtil.dp2px(context, 20));
        mCenterTextColor = typedArray.getColor(R.styleable.CirclePercentBar_centerTextColor
                , 0x0000ff);
        mCenterTextSize = typedArray.getDimensionPixelSize(R.styleable.CirclePercentBar_centerTextSize
                , DisplayUtil.dp2px(context, 20));
        mCircleRadius = typedArray.getDimensionPixelSize(R.styleable.CirclePercentBar_circleRadius
                , DisplayUtil.dp2px(context, 100));
        arcStartColor = typedArray.getColor(R.styleable.CirclePercentBar_arcStartColor
                , ContextCompat.getColor(mContext, R.color.colorStart));
        arcEndColor = typedArray.getColor(R.styleable.CirclePercentBar_arcEndColor
                , ContextCompat.getColor(mContext, R.color.colorEnd));
        typedArray.recycle();

利用TypeArray对象来获取属性值,根据自定义属性的类型,定义的是颜色属性,就用它的getcolor方法,获取的是尺寸属性,就用getDimensionPixelSize属性,这里尺寸方法需要注意第二个默认值参数需要把dp转为px

初始化画笔

因为draw方法会被调用很多次,肯定不能在画的时候才初始化画笔,这样会很消耗内存,影响性能,所以选择在构造函数中初始化。

private void initPaint() {

        startCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        startCirclePaint.setStyle(Paint.Style.FILL);
        //startCirclePaint.setStrokeWidth(mArcWidth);
        startCirclePaint.setColor(arcStartColor);

        arcCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        arcCirclePaint.setStyle(Paint.Style.STROKE);
        arcCirclePaint.setStrokeWidth(mArcWidth);
        arcCirclePaint.setColor(ContextCompat.getColor(mContext,R.color.colorCirclebg));
        arcCirclePaint.setStrokeCap(Paint.Cap.ROUND);

        arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        arcPaint.setStyle(Paint.Style.STROKE);
        arcPaint.setStrokeWidth(mArcWidth);
        arcPaint.setColor(mArcColor);
        arcPaint.setStrokeCap(Paint.Cap.ROUND);

        centerTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        centerTextPaint.setStyle(Paint.Style.STROKE);
        centerTextPaint.setColor(mCenterTextColor);
        centerTextPaint.setTextSize(mCenterTextSize);

        //圆弧的外接矩形
        arcRectF = new RectF();

        //文字的边界矩形
        textBoundRect = new Rect();

    }

这里初始化了4个画笔,第一个画笔是用来画圆环顶部的一个圆的,这个到后面再说为什么我选择画了一个圆;第二个画笔是用来画未填充状态的背景圆环的;第三个画笔用来画百分比占据的彩色弧形环的,也就是效果图中动起来的圆弧部分,类型采用了描边,圆角;第四个画笔用来写中间的百分比文字。

另外还在这里初始化了一个定位圆弧的外界矩形和定位文字的边界矩形

重写onMeasure

到这构造函数需要做的初始化工作就基本做好了,接下来的工作就是自定义控件必要的重写onMeasure方法

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(measureView(widthMeasureSpec), measureView(heightMeasureSpec));
    }

    private int measureView(int measureSpec) {
        int result;
        int specMode=MeasureSpec.getMode(measureSpec);
        int specSize=MeasureSpec.getSize(measureSpec);
        if(specMode==MeasureSpec.EXACTLY){
            result=specSize;
        }else{
            result=mCircleRadius*2;
            if(specMode==MeasureSpec.AT_MOST){
                result=Math.min(result,specSize);
            }
        }
        return result;
    }

关于为什么要重写onMeasure方法,还有测量都是些什么鬼,我相信很多初学者都会感到持续懵b,如果你想详细了解下可以看看这篇文章
为什么重写onMeasure()以及怎么重写

不想细看的可以看看我下面简单的解释

测量就是为了告诉系统我这个控件该画多大,如果是给了确定的值比如设置的是Match_parent或者特定的dp值就很简单了,即按照measureSpec给出的大小返回就行,如果设置的是wrap_content,系统本身是不知道你的控件内部元素到底有多大的,所以就需要计算出一个最小值告诉给系统

如上述代码所示,如果判断得到设置的模式是MeasureSpec.EXACTLY,就把MeasureSpec中的尺寸值返回就行,如果判断得到设置的模式是MeasureSpec.AT_MOST,也就是代码中设置的 wrap_content,就比较圆环的直径和MeasureSpec中给出的尺寸值,取最小的一个返回,最后调用setMeasuredDimension方法,传入处理后的长宽值。

重写onDraw方法

这里就到了核心的地方了,画我们要展示的view了,先上代码:

        canvas.rotate(-90, getWidth()/ 2, getHeight()/ 2);

        arcRectF.set(getWidth()/2-mCircleRadius+mArcWidth/2,getHeight()/2-mCircleRadius+mArcWidth/2
                ,getWidth()/2+mCircleRadius-mArcWidth/2,getHeight()/2+mCircleRadius-mArcWidth/2);
        canvas.drawArc(arcRectF, 0,360,false,arcCirclePaint);

        arcPaint.setShader(new SweepGradient(getWidth()/2,getHeight()/2,arcStartColor,arcEndColor));
        canvas.drawArc(arcRectF, 0,360* mCurData /100,false,arcPaint);

        canvas.rotate(90, getWidth()/ 2, getHeight()/ 2);
        canvas.drawCircle(getWidth()/2,getHeight()/2-mCircleRadius+mArcWidth/2,mArcWidth/2,startCirclePaint);

        String data= String.valueOf(mCurData) +"%";
        centerTextPaint.getTextBounds(data,0,data.length(),textBoundRect);
        canvas.drawText(data,getWidth()/2-textBoundRect.width()/2,getHeight()/2+textBoundRect.height()/2,centerTextPaint);

1、首先将画布绕中心点逆时针旋转了90度,做这个是因为在后面画渐变色的圆弧时,drawArc和SweepGradient这两个类的起始点0度不是在我们习惯的圆环最上面那个点,而是从圆环最右边那个点开始,所以逆时针旋转90度就能让它从最上面的点开始

旋转90度.png

2、接下来首先要画未填充状态的圆环,这个很简单,用drawArc方法从0度到360度就好了,这个地方的坑点在于调用之前给圆环定位的外接矩形,这个地方稍微有点儿绕,看图说话

外界矩形.png

如图所示,红色的矩形就是该圆环的外接矩形,而圆环的边是有宽度的,所以这样就有一问题了

需要发挥一点儿想象力,想一下假设我们设置的该控件的大小是wrap或者特定的大小,反正就是当控件的长宽小于2r+width这个长度,而这个时候我们一般会把外接矩形长宽直接设置为2r,觉得正好,然而,当运行出来的时候就傻眼了,圆环上下左右四个地方缺少了一块儿,因为圆环的边是有宽度的,半径相当于多了width/2的宽度

所以为了避免这种情况,我们在设置外接矩形的时候,提前把我们要画的圆环半径缩小width/2就好了,所以就有了代码中设置外接矩形时参数里包括width/2的设置

3、紧接着要画的就是百分比填充的彩色圆环,外接矩形就可以用上面设置好的,唯一的不同就是画笔需要设置一个渐变色的渲染效果,利用setShader方法

4、然后在圆环的顶部起始位置又画了一个实心圆。
因为画圆弧的画笔是圆头类型的,在起始地方0度偏左还会有一个半圆,但是我们又采用了渐变色渲染,所以圆头部分就变成了结束的颜色值,就是这样

1501210841(1).png

这效果简直不能忍,我也尝试了补偿起始角度来改变这种情况,但效果都不理想,所以最后找了一个最暴力的解决方式,用一个圆直接覆盖起始部分就好了...

5、最后就是添加中间的百分比文字就,这里没啥说的,定位到文字起始的位置,用drawText就好

暴露设置百分比的方法

这么一个控件,自然要暴露给用户一个动态设置百分比的方法

public void setPercentData(float data, TimeInterpolator interpolator){
        ValueAnimator valueAnimator=ValueAnimator.ofFloat(mCurData,data);
        valueAnimator.setDuration((long) (Math.abs(mCurData-data)*30));
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                float value= (float) valueAnimator.getAnimatedValue();
                mCurData=(float)(Math.round(value*10))/10;
                invalidate();
            }
        });
        valueAnimator.setInterpolator(interpolator);
        valueAnimator.start();
    }

这个地方用到了ValueAnimator,然后监听valueAnimaer数值的更新,在回调中设置相应百分比参数,调用invalidate,重绘view,这样就达到了动画改变的效果了

总结

到这,这个自定义的圆环百分比控件就完成了,回想一下确实也挺简单的
感觉就是和大神们说的一样,自定义View看起来很高端,我们学习的时候如果就从基础的各种measure之类的基础理论看起的话,很枯燥,也很抽象,不如就拿一个自定义view上手写,先写简单的,领悟其中的原理,不懂得地方再去查相应的知识点,这样学起来效率会高很多,然后循序渐进,相信慢慢就会有明显的进步的

菜鸟一个,如果有什么不对的地方,希望能指正提意见...

2017.7.28 12:05
三教 806 实验室

推荐阅读更多精彩内容