一步步做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 实验室

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容