Android 自定义控件--圆形进度条

旧城等待, 2022-05-31 05:37 401阅读 0赞

先上效果图:
这里写图片描述
原文地址

前期参考文章

Android自定义View的官方套路
我奶奶都能懂的UI绘制流程(上)!
我奶奶都能懂的UI绘制流程(下)!
53.自定义View练习(一)圆形百分比控件

1.自定义属性

在res/values/ 下创建 attire.xml文件

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3. <declare-styleable name="CirclePercentBar">
  4. <attr name="arcColor" format="color"/>
  5. <attr name="arcWidth" format="dimension"/>
  6. <attr name="centerTextColor" format="color"/>
  7. <attr name="centerTextSize" format="dimension"/>
  8. <attr name="circleRadius" format="dimension"/>
  9. <attr name="arcStartColor" format="color"/>
  10. <attr name="arcEndColor" format="color"/>
  11. </declare-styleable>
  12. </resources>

2.自定义View类

  1. public class CirclePercentBar extends View{
  2. public CirclePercentBar(Context context) {
  3. this(context, null);
  4. }
  5. public CirclePercentBar(Context context, AttributeSet attrs) {
  6. this(context, attrs, 0);
  7. }
  8. public CirclePercentBar(Context context, AttributeSet attrs, int defStyleAttr) {
  9. super(context, attrs, defStyleAttr);
  10. }

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

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

3.获取属性值

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

  1. TypedArray typedArray=context.obtainStyledAttributes(attrs
  2. , R.styleable.CirclePercentBar, defStyleAttr,0);
  3. mArcColor = typedArray.getColor(R.styleable.CirclePercentBar_arcColor
  4. ,0xff0000);
  5. mArcWidth = typedArray.getDimensionPixelSize(R.styleable.CirclePercentBar_arcWidth
  6. , DisplayUtil.dp2px(context, 20));
  7. mCenterTextColor = typedArray.getColor(R.styleable.CirclePercentBar_centerTextColor
  8. , 0x0000ff);
  9. mCenterTextSize = typedArray.getDimensionPixelSize(R.styleable.CirclePercentBar_centerTextSize
  10. , DisplayUtil.dp2px(context, 20));
  11. mCircleRadius = typedArray.getDimensionPixelSize(R.styleable.CirclePercentBar_circleRadius
  12. , DisplayUtil.dp2px(context, 100));
  13. arcStartColor = typedArray.getColor(R.styleable.CirclePercentBar_arcStartColor
  14. , ContextCompat.getColor(mContext, R.color.colorStart));
  15. arcEndColor = typedArray.getColor(R.styleable.CirclePercentBar_arcEndColor
  16. , ContextCompat.getColor(mContext, R.color.colorEnd));
  17. typedArray.recycle();

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

4.初始化画笔

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

  1. private void initPaint() {
  2. startCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  3. startCirclePaint.setStyle(Paint.Style.FILL);
  4. //startCirclePaint.setStrokeWidth(mArcWidth);
  5. startCirclePaint.setColor(arcStartColor);
  6. arcCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  7. arcCirclePaint.setStyle(Paint.Style.STROKE);
  8. arcCirclePaint.setStrokeWidth(mArcWidth);
  9. arcCirclePaint.setColor(ContextCompat.getColor(mContext,R.color.colorCirclebg));
  10. arcCirclePaint.setStrokeCap(Paint.Cap.ROUND);
  11. arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  12. arcPaint.setStyle(Paint.Style.STROKE);
  13. arcPaint.setStrokeWidth(mArcWidth);
  14. arcPaint.setColor(mArcColor);
  15. arcPaint.setStrokeCap(Paint.Cap.ROUND);
  16. centerTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  17. centerTextPaint.setStyle(Paint.Style.STROKE);
  18. centerTextPaint.setColor(mCenterTextColor);
  19. centerTextPaint.setTextSize(mCenterTextSize);
  20. //圆弧的外接矩形
  21. arcRectF = new RectF();
  22. //文字的边界矩形
  23. textBoundRect = new Rect();
  24. }

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

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

5.重写onMeasure

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

  1. @Override
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  4. setMeasuredDimension(measureView(widthMeasureSpec), measureView(heightMeasureSpec));
  5. }
  6. private int measureView(int measureSpec) {
  7. int result;
  8. int specMode=MeasureSpec.getMode(measureSpec);
  9. int specSize=MeasureSpec.getSize(measureSpec);
  10. if(specMode==MeasureSpec.EXACTLY){
  11. result=specSize;
  12. }else{
  13. result=mCircleRadius*2;
  14. if(specMode==MeasureSpec.AT_MOST){
  15. result=Math.min(result,specSize);
  16. }
  17. }
  18. return result;
  19. }

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

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

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

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

6.重写onDraw方法

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

  1. canvas.rotate(-90, getWidth()/ 2, getHeight()/ 2);
  2. arcRectF.set(getWidth()/2-mCircleRadius+mArcWidth/2,getHeight()/2-mCircleRadius+mArcWidth/2
  3. ,getWidth()/2+mCircleRadius-mArcWidth/2,getHeight()/2+mCircleRadius-mArcWidth/2);
  4. canvas.drawArc(arcRectF, 0,360,false,arcCirclePaint);
  5. arcPaint.setShader(new SweepGradient(getWidth()/2,getHeight()/2,arcStartColor,arcEndColor));
  6. canvas.drawArc(arcRectF, 0,360* mCurData /100,false,arcPaint);
  7. canvas.rotate(90, getWidth()/ 2, getHeight()/ 2);
  8. canvas.drawCircle(getWidth()/2,getHeight()/2-mCircleRadius+mArcWidth/2,mArcWidth/2,startCirclePaint);
  9. String data= String.valueOf(mCurData) +"%";
  10. centerTextPaint.getTextBounds(data,0,data.length(),textBoundRect);
  11. canvas.drawText(data,getWidth()/2-textBoundRect.width()/2,getHeight()/2+textBoundRect.height()/2,centerTextPaint);

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

这里写图片描述

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

这里写图片描述

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

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

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

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

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

这里写图片描述

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

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

7.暴露设置百分比的方法

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

  1. public void setPercentData(float data, TimeInterpolator interpolator){
  2. ValueAnimator valueAnimator=ValueAnimator.ofFloat(mCurData,data);
  3. valueAnimator.setDuration((long) (Math.abs(mCurData-data)*30));
  4. valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  5. @Override
  6. public void onAnimationUpdate(ValueAnimator valueAnimator) {
  7. float value= (float) valueAnimator.getAnimatedValue();
  8. mCurData=(float)(Math.round(value*10))/10;
  9. invalidate();
  10. }
  11. });
  12. valueAnimator.setInterpolator(interpolator);
  13. valueAnimator.start();
  14. }

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

github源码

发表评论

表情:
评论列表 (有 0 条评论,401人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Android 定义圆形进度

    在Android开发中,对于进度条想必大家不会陌生。例如,应用在执行一个耗时操作时,会通过展示一个进度条来显示“加载中...”的动画作为友好页面以提高用户体验。对于这样的进度条

    相关 定义圆形进度

    自定义圆形进度条:自己写的一个类 ,使用非常简单,非常适合新手学习。 (因为我就是新手,通过学了这个之后,以后绘制其他的图也是差不多这样,能够做到举一反三!事半功倍!非常值得