• Android 自定义View之圆形进度条


    前言

    很多场景下都用到这种进度条,有的还带动画效果,
    在这里插入图片描述

    今天我也来写一个。

    写之前先拆解下它的组成:

    • 底层圆形
    • 上层弧形
    • 中间文字

    那我们要做的就是:

    1. 绘制底层圆形;
    2. 在同位置绘制上层弧形,但颜色不同;
    3. 在中心点绘制文本,显示进度。

    按照这个目标,学习下自定义View的流程。

    (本文用到 Canvas 相关的知识点,不熟悉的可以参考文末参考资料。)

    1.基础

    新建一个类,继承 View ,重写构造函数,如,

    package com.test.luodemo.customerview;
    
    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.View;
    
    import androidx.annotation.Nullable;
    
    public class CircleProgressBar extends View {
        public CircleProgressBar(Context context) {
            super(context);
        }
    
        public CircleProgressBar(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在 xml 中使用,LinearLayout 加了背景颜色,方便看出所在位置。

    
                
            
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    此时运行,是没效果的,因为这个View还没有绘制,啥也没有。

    2.绘制底层圆形

    初始化3个图形的画笔 ,底层圆形和上层弧形的画笔宽度一致、颜色不一致,方便区分

    重写 onDraw(Canvas canvas) 方法,用 canvas.drawCircle 绘制底层圆形,

    package com.test.luodemo.customerview;
    
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.util.AttributeSet;
    import android.view.View;
    
    import androidx.annotation.Nullable;
    
    public class CircleProgressBar extends View {
        private Paint paintCircleBottom = new Paint();
        private Paint paintArcTop = new Paint();
        private Paint paintText = new Paint();
        
        public CircleProgressBar(Context context) {
            super(context);
            init();
        }
    
        public CircleProgressBar(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init(){
            //初始化文本的画笔
            paintText.setFlags(Paint.ANTI_ALIAS_FLAG);
            paintText.setColor(Color.BLACK);
            paintText.setTextAlign(Paint.Align.CENTER);
            paintText.setTextSize(80f);
    
            //初始化底层圆形的画笔
            paintCircleBottom.setFlags(Paint.ANTI_ALIAS_FLAG);
            paintCircleBottom.setColor(Color.LTGRAY);
            paintCircleBottom.setStrokeWidth(10f);
            paintCircleBottom.setStrokeCap(Paint.Cap.ROUND);
            paintCircleBottom.setStyle(Paint.Style.STROKE);
    
            //初始化弧形的画笔
            paintArcTop.setFlags(Paint.ANTI_ALIAS_FLAG);
            paintArcTop.setColor(Color.MAGENTA);
            paintArcTop.setStrokeWidth(10f);
            paintArcTop.setStrokeCap(Paint.Cap.ROUND);
            paintArcTop.setStyle(Paint.Style.STROKE);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            //绘制底层圆形
            canvas.drawCircle(300, 300, 200, paintCircleBottom);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    效果,
    在这里插入图片描述

    3.绘制上层弧形

    在之前的基础上绘制上层弧形,弧形的中心和圆心一致。

    canvas.drawArc 绘制弧形。这里直接指定绘制的角度是 90° ,后续会动态指定。

    	@Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            //绘制底层圆形
            canvas.drawCircle( 300, 300, 200, paintCircleBottom);
    
            //绘制上层弧形,从顶部开始,顺时针走90°
            _angle = 90;
            canvas.drawArc(100,100,500,500,270, _angle,false, paintArcTop);
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    效果,
    在这里插入图片描述

    4.绘制文本

    canvas.drawText 绘制文本,

    使用 DecimalFormat 格式化输入,保留小数点后两位,如果小数点后两位都是0则不显示小数点后两位。

    	@Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            //绘制底层圆形
            canvas.drawCircle(300, 300, 200, paintCircleBottom);
    
            //绘制上层弧形,从顶部开始,顺时针走90°
            _angle = 90;
            canvas.drawArc(100,100,500,500,270, _angle,false, paintArcTop);
    
            //绘制文本
            DecimalFormat dt = new DecimalFormat("0.##");
            canvas.drawText(dt.format(100 * _angle/360)+"%", 300 , 300, paintText);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    效果,
    在这里插入图片描述

    可以看到,文本虽然居中,但是文本是显示在中心线上,
    在这里插入图片描述

    期望结果是文本的水平中心线和圆心重合,改为,

    		//绘制文本,文字中心和圆心保持一致
            Paint.FontMetrics fontMetrics = paintText.getFontMetrics();
            float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
            float baseline= 300 + distance;
            canvas.drawText(dt.format(100 * _angle/360)+"%", 300, baseline, paintText);//文字中心和圆心一致        
    
    • 1
    • 2
    • 3
    • 4
    • 5

    效果,符合预期。
    在这里插入图片描述

    5.添加动画

    创建一个设置进度的接口,供外部调用。

    使用 ValueAnimator ,监听动画过程,然后逐渐刷新角度值。使用 AccelerateInterpolator 插值器,动画速度开始慢、逐渐加速。

    	@Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            //绘制底层圆形
            canvas.drawCircle(300, 300, 200, paintCircleBottom);
    
            //绘制上层弧形,从顶部开始,顺时针走90°
            canvas.drawArc(100,100,500,500,270, _angle,false, paintArcTop);
    
            //绘制文本,文字中心和圆心保持一致
            DecimalFormat dt = new DecimalFormat("0.##");
            Paint.FontMetrics fontMetrics = paintText.getFontMetrics();
            float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
            float baseline= 300 + distance;
            canvas.drawText(dt.format(100 * _angle/360)+"%", 300, baseline, paintText);//文字中心和圆心一致
        }
    	
    	/**
         * 设置进度,展现动画
         * */
        public void setProgress(int progress){
            ValueAnimator animator = ValueAnimator.ofFloat(0,100f);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float cur = (float) animation.getAnimatedValue();
                    _angle = cur/100 * 360 * progress/100;
                    invalidate(); //刷新 View
                }
            });
            animator.setDuration(3000);
            animator.setInterpolator(new AccelerateInterpolator());
            animator.start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    注意要去掉 3.绘制上层弧形 中固定90°的逻辑。

    外部调用,

    CircleProgressBar mCircleProgressBar1 = (CircleProgressBar) findViewById(R.id.circle_progress_bar1);
    mCircleProgressBar1.setProgress((int) (100 * Math.random()));
    
    • 1
    • 2

    随机生成一个 0.0 - 0.1 的数值,乘以 100 设置为进度。
    效果,
    在这里插入图片描述
    可以看到动画效果, 虽然 git 丢帧了 ~ 。

    6.调整位置、宽高

    前文我是设定了 View 宽高都是 300dp ,并且绘制图形是随意指定的坐标。

    实际开发时,不可能用这些值,所以要优化下绘制的逻辑。

    实际使用时,可能宽度高度一样,宽度大于高度 ,宽度小于高度,

    采用这个逻辑:

    • 取宽度、高度的最小值,作为圆的直径,除以 2 得到半径。
    • 对角线交汇点作为圆心。

    简言之,以对角线为圆心画最大内切圆。

    重写 onMeasure 方法,重绘 View 的宽高,这部分参考《Android 开发艺术探索》,

    	private int DEFAULT_WIDTH = 100;//默认宽度
        private int DEFAULT_HEIGHT = 100;//默认宽度
        private int DEFAULT_RADIUS = 50;//默认半径
        
    	@Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
            } else if (widthMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(DEFAULT_WIDTH, heightMeasureSpec);
            } else if (heightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(widthMeasureSpec, DEFAULT_HEIGHT);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    修改 onDraw 绘制逻辑 ,

    	@Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            // 圆心坐标是(centerX,centerY)
            int centerX = getWidth()/2;
            int centerY = getHeight()/2;
            //确定半径
            float radius = Math.min(centerX, centerY) - paintCircleBottom.getStrokeWidth();
    
    
            //绘制底层圆形
            canvas.drawCircle(centerX, centerY, radius, paintCircleBottom);
    
    
            //绘制上层弧形,从顶部开始,顺时针走 _angle
            canvas.drawArc(centerX - radius,centerY-radius,centerX + radius,centerY + radius,270, _angle,false, paintArcTop);
    
            //绘制文本,文字中心和圆心保持一致
            Paint.FontMetrics fontMetrics = paintText.getFontMetrics();
            float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
            float baseline= centerY + distance;
            canvas.drawText(dt.format(100 * _angle/360)+"%", centerX, baseline, paintText);//文字中心和圆心一致
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    分别写了 3 个布局,布局依次是 宽度等于高度 、宽度大宇高度、宽度小于高度,效果,
    在这里插入图片描述
    至此,基本是一个还可以的版本了。

    附代码

    贴下当前代码,

    CircleProgressBar.java

    package com.test.luodemo.customerview;
    
    import android.animation.ValueAnimator;
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.View;
    import android.view.animation.AccelerateInterpolator;
    
    import androidx.annotation.Nullable;
    
    import java.text.DecimalFormat;
    
    public class CircleProgressBar extends View {
        private Paint paintCircleBottom = new Paint();
        private Paint paintArcTop = new Paint();
        private Paint paintText = new Paint();
    
        private int DEFAULT_WIDTH = 100;//默认宽度
        private int DEFAULT_HEIGHT = 100;//默认宽度
        private int DEFAULT_RADIUS = 50;//默认半径
    
        private float _angle;//弧形的角度
        
        public CircleProgressBar(Context context) {
            super(context);
            init();
        }
    
        public CircleProgressBar(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init(){
            //初始化文本的画笔
            paintText.setFlags(Paint.ANTI_ALIAS_FLAG);
            paintText.setColor(Color.BLACK);
            paintText.setTextAlign(Paint.Align.CENTER);
            paintText.setTextSize(80f);
    
            //初始化底层圆形的画笔
            paintCircleBottom.setFlags(Paint.ANTI_ALIAS_FLAG);
            paintCircleBottom.setColor(Color.LTGRAY);
            paintCircleBottom.setStrokeWidth(10f);
            paintCircleBottom.setStrokeCap(Paint.Cap.ROUND);
            paintCircleBottom.setStyle(Paint.Style.STROKE);
    
            //初始化弧形的画笔
            paintArcTop.setFlags(Paint.ANTI_ALIAS_FLAG);
            paintArcTop.setColor(Color.MAGENTA);
            paintArcTop.setStrokeWidth(10f);
            paintArcTop.setStrokeCap(Paint.Cap.ROUND);
            paintArcTop.setStyle(Paint.Style.STROKE);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
            } else if (widthMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(DEFAULT_WIDTH, heightMeasureSpec);
            } else if (heightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(widthMeasureSpec, DEFAULT_HEIGHT);
            }
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            // 圆心坐标是(centerX,centerY)
            int centerX = getWidth()/2;
            int centerY = getHeight()/2;
            //确定半径
            float radius = Math.min(centerX, centerY) - paintCircleBottom.getStrokeWidth();
    
    
            //绘制底层圆形
            canvas.drawCircle(centerX, centerY, radius, paintCircleBottom);
    
    
            //绘制上层弧形,从顶部开始,顺时针走90°
            canvas.drawArc(centerX - radius,centerY-radius,centerX + radius,centerY + radius,270, _angle,false, paintArcTop);
    
            //绘制文本,文字中心和圆心保持一致
            DecimalFormat dt = new DecimalFormat("0.##");
            Paint.FontMetrics fontMetrics = paintText.getFontMetrics();
            float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
            float baseline= centerY + distance;
            canvas.drawText(dt.format(100 * _angle/360)+"%", centerX, baseline, paintText);//文字中心和圆心一致
        }
    
        /**
         * 设置进度,展现动画
         * */
        public void setProgress(int progress){
            ValueAnimator animator = ValueAnimator.ofFloat(0,100f);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float cur = (float) animation.getAnimatedValue();
                    _angle = cur/100 * 360 * progress/100;
                    invalidate();
                }
            });
            animator.setDuration(3000);
            animator.setInterpolator(new AccelerateInterpolator());
            animator.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124

    布局文件

    
    
    
        
    
            
    
                
            
    
            
    
                
            
    
            
    
                
            
    
            
        
    
        
    
            

    Activity 调用

    public class CircleProgressBarActivity extends AppCompatActivity {
    
        private CircleProgressBar mCircleProgressBar1 , mCircleProgressBar2 , mCircleProgressBar3;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_circle_progress_bar);
            Objects.requireNonNull(getSupportActionBar()).setTitle("CircleProgressBarActivity");
    
            mCircleProgressBar1 = (CircleProgressBar) findViewById(R.id.circle_progress_bar1);
            mCircleProgressBar2 = (CircleProgressBar) findViewById(R.id.circle_progress_bar2);
            mCircleProgressBar3 = (CircleProgressBar) findViewById(R.id.circle_progress_bar3);
        }
    
        public void onCPBButtonClick(View view) {
            switch (view.getId()) {
                case R.id.button_cpb1:
                    mCircleProgressBar1.setProgress((int) (100 * Math.random()));
                    break;
                case R.id.button_cpb2:
                    mCircleProgressBar2.setProgress((int) (100 * Math.random()));
                    break;
                case R.id.button_cpb3:
                    mCircleProgressBar3.setProgress((int) (100 * Math.random()));
                    break;
                case R.id.button_cpb_all:
                    mCircleProgressBar1.setProgress((int) (100 * Math.random()));
                    mCircleProgressBar2.setProgress((int) (100 * Math.random()));
                    mCircleProgressBar3.setProgress((int) (100 * Math.random()));
                    break;
                default:
                    break;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    7.自定义属性 attr

    需求是不停的,会有这些需求:可指定画笔(宽度、颜色等)、可指定动画时长等。

    这些可以通过在自定义的View中创建 Java 接口来设置,但我要学自定义View,就要用 attr

    7.1 创建 res/values/attrs.xml

    如果已有就不用创建,直接用就行了。

    写入如下内容,

    
    
        
        
             
             
             
            
            
            
            
        
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    中 CircleProgressBar 就是自定义 View 的名字,要保持一致。

    不一致AS会报黄,

    By convention, the custom view (CircleProgressBar) and the declare-styleable (CircleProgressBar111) should have the same name (various editor features rely on this convention)
    
    • 1

    是 CircleProgressBar 的属性,可指定类型

    类型说明
    boolean布尔类型,true 或 false
    color颜色值,如 @android:color/white
    dimensiondp 值,如 20dp
    enum枚举
    flags位或运算,如 app:cus_view_gravity=“top|right”
    fraction百分比,如 30%
    floatfloat 型
    integerint 型
    reference引用资源,如 @drawable/pic
    string字符串

    7.2 使用 TypedArray 获取 attrs

    在构造函数中,通过 TypedArray 获取自定义的属性。基本逻辑就是有设置 attr 就用设置的值,没有就用默认值。

    使用后一定要调用 TypedArray.recycle();

    	public CircleProgressBar(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
    
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar);
            textColor = typedArray.getColor(R.styleable.CircleProgressBar_textColor, Color.BLACK);
            textSize = typedArray.getFloat(R.styleable.CircleProgressBar_textSize, 80f);
            circleColor = typedArray.getColor(R.styleable.CircleProgressBar_circleColor, Color.LTGRAY);
            circleWidth = typedArray.getFloat(R.styleable.CircleProgressBar_circleWidth, 10f);
            arcColor = typedArray.getColor(R.styleable.CircleProgressBar_arcColor, Color.MAGENTA);
            arcWidth = typedArray.getFloat(R.styleable.CircleProgressBar_arcWidth, 10f);
            progress = typedArray.getInt(R.styleable.CircleProgressBar_initProgress, 0);
            typedArray.recycle();
            
            init();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    有两个带 AttributeSet 参数的构造函数,

    • public CircleProgressBar(Context context, @Nullable AttributeSet attrs) {}
    • public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {}

    为什么用前面这个? 因为我们是在 xml 中定义的 CircleProgressBar 。参考源码说明,

    	/**
         * Constructor that is called when inflating a view from XML. This is called
         * when a view is being constructed from an XML file, supplying attributes
         * that were specified in the XML file. This version uses a default style of
         * 0, so the only attribute values applied are those in the Context's Theme
         * and the given AttributeSet.
         *
         * 

    * The method onFinishInflate() will be called after all children have been * added. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. * @see #View(Context, AttributeSet, int) */ public View(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } /** * Perform inflation from XML and apply a class-specific base style from a * theme attribute. This constructor of View allows subclasses to use their * own base style when they are inflating. For example, a Button class's * constructor would call this version of the super class constructor and * supply R.attr.buttonStyle for defStyleAttr; this * allows the theme's button style to modify all of the base view attributes * (in particular its background) as well as the Button class's attributes. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. * @param defStyleAttr An attribute in the current theme that contains a * reference to a style resource that supplies default values for * the view. Can be 0 to not look for defaults. * @see #View(Context, AttributeSet) */ public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    7.3 在 xml 中初始化 attr

    xml 关键代码如下,

    	
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".customerview.CircleProgressBarActivity">
    	
    		
    
    	
    
                	
                    app:circleColor="@android:color/white"
                    app:circleWidth="30"
                    app:arcColor="@color/my_red"
                    app:arcWidth="15"
                    app:textColor="@android:color/holo_orange_dark"
                    app:initProgress="30"
                    
                    />
            
    		
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    注释2处就是初始化 attr ,以 app: 开头是对应注释1处。

    7.4 效果

    左一是自定义 attr 的效果,左二、左三是没有自定义 attr 的效果。
    差异有:底层圆形的颜色、画笔大小;上层弧形的颜色、画笔大小、开始的角度;中间文字的颜色。
    说明自定义 attr 起效了。
    在这里插入图片描述

    附代码V2

    CircleProgressBar.java

    package com.test.luodemo.customerview;
    
    import android.animation.ValueAnimator;
    import android.content.Context;
    import android.content.res.TypedArray;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.View;
    import android.view.animation.AccelerateInterpolator;
    
    import androidx.annotation.Nullable;
    
    import com.test.luodemo.R;
    
    import java.text.DecimalFormat;
    
    public class CircleProgressBar extends View {
        private Paint paintCircleBottom = new Paint();
        private Paint paintArcTop = new Paint();
        private Paint paintText = new Paint();
    
        private int DEFAULT_WIDTH = 100;//默认宽度
        private int DEFAULT_HEIGHT = 100;//默认宽度
        private int DEFAULT_RADIUS = 50;//默认半径
    
        private float _angle;//弧形的角度
    
        /***************************** attr *******************************/
        int textColor;
        float textSize;
        int circleColor ;
        int arcColor;
        float circleWidth;
        float arcWidth;
        int progress;
        /***************************** attr *******************************/
    
        public CircleProgressBar(Context context) {
            super(context);
            init();
        }
    
        public CircleProgressBar(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
    
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar);
            textColor = typedArray.getColor(R.styleable.CircleProgressBar_textColor, Color.BLACK);
            textSize = typedArray.getFloat(R.styleable.CircleProgressBar_textSize, 80f);
            circleColor = typedArray.getColor(R.styleable.CircleProgressBar_circleColor, Color.LTGRAY);
            circleWidth = typedArray.getFloat(R.styleable.CircleProgressBar_circleWidth, 10f);
            arcColor = typedArray.getColor(R.styleable.CircleProgressBar_arcColor, Color.MAGENTA);
            arcWidth = typedArray.getFloat(R.styleable.CircleProgressBar_arcWidth, 10f);
            progress = typedArray.getInt(R.styleable.CircleProgressBar_initProgress, 0);
    
            typedArray.recycle();
            init();
        }
    
        public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init(){
            //初始化文本的画笔
            paintText.setFlags(Paint.ANTI_ALIAS_FLAG);
            paintText.setStyle(Paint.Style.FILL);
            paintText.setColor(textColor);//设置自定义属性值
            paintText.setTextAlign(Paint.Align.CENTER);
            paintText.setTextSize(textSize);
    
            //初始化底层圆形的画笔
            paintCircleBottom.setFlags(Paint.ANTI_ALIAS_FLAG);
            paintCircleBottom.setStrokeCap(Paint.Cap.ROUND);
            paintCircleBottom.setStyle(Paint.Style.STROKE);
            paintCircleBottom.setColor(circleColor);//设置自定义属性值
            paintCircleBottom.setStrokeWidth(circleWidth);//设置自定义属性值
    
            //初始化弧形的画笔
            paintArcTop.setFlags(Paint.ANTI_ALIAS_FLAG);
            paintArcTop.setStrokeCap(Paint.Cap.ROUND);
            paintArcTop.setStyle(Paint.Style.STROKE);
            paintArcTop.setColor(arcColor);//设置自定义属性值
            paintArcTop.setStrokeWidth(arcWidth);//设置自定义属性值
    
            _angle = progress;
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
            } else if (widthMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(DEFAULT_WIDTH, heightMeasureSpec);
            } else if (heightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(widthMeasureSpec, DEFAULT_HEIGHT);
            }
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            // 圆心坐标是(centerX,centerY)
            int centerX = getWidth()/2;
            int centerY = getHeight()/2;
            //确定半径
            float radius = Math.min(centerX, centerY) - paintCircleBottom.getStrokeWidth();
    
            //绘制底层圆形
            canvas.drawCircle(centerX, centerY, radius, paintCircleBottom);
    
            //绘制上层弧形,从顶部开始,顺时针走90°
            canvas.drawArc(centerX - radius,centerY-radius,centerX + radius,centerY + radius,270, _angle,false, paintArcTop);
    
            //绘制文本,文字中心和圆心保持一致
            DecimalFormat dt = new DecimalFormat("0.##");
            Paint.FontMetrics fontMetrics = paintText.getFontMetrics();
            float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
            float baseline= centerY + distance;
            canvas.drawText(dt.format(100 * _angle/360)+"%", centerX, baseline, paintText);//文字中心和圆心一致
        }
    
        /**
         * 设置进度,展现动画
         * */
        public void setProgress(int progress){
            ValueAnimator animator = ValueAnimator.ofFloat(0,100f);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float cur = (float) animation.getAnimatedValue();
                    _angle = cur/100 * 360 * progress/100;
                    invalidate();
                }
            });
            animator.setDuration(3000);
            animator.setInterpolator(new AccelerateInterpolator());
            animator.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149

    布局文件

    
    
    
        
    
            
    
                
            
    
            
    
                
            
    
            
    
                
            
    
        
    
        
    
            

    Activity 调用

    和之前一样。

    attrs

    
    
        
        
             
             
             
            
            
            
            
        
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    8.进度条按照进度变色

    继续整,进度条按照的进度,从红色渐变为黄色再渐变为绿色。

    渐变就考虑用 Shader ,它专注于颜色渐变。

    	@Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            // 圆心坐标是(centerX,centerY)
            int centerX = getWidth()/2;
            int centerY = getHeight()/2;
            //确定半径
            float radius = Math.min(centerX, centerY) - paintCircleBottom.getStrokeWidth();
    
            //绘制底层圆形
            canvas.drawCircle(centerX, centerY, radius, paintCircleBottom);
    
    		//注释1
            canvas.save();//保存画布状态
            canvas.rotate(-90, centerX, centerY);//以 (centerX, centerY) 为中心,逆时针旋转 90°
    
    		//注释2
            SweepGradient sweepGradient = new SweepGradient(centerX,centerY,new int[]{Color.RED,Color.YELLOW, Color.GREEN},null);
            paintArcTop.setShader(sweepGradient);//
            paintArcTop.setStrokeCap(Paint.Cap.BUTT);//设置画笔边缘为切面,默认是半圆状
            
            //绘制上层弧形,从顶部开始,顺时针走90°        
    		// canvas.drawArc(centerX - radius,centerY-radius,centerX + radius,centerY + radius,270, _angle,false, paintArcTop);
    		
    		canvas.drawArc(centerX - radius,centerY-radius,centerX + radius,centerY + radius,0, _angle,false, paintArcTop);//注释3
            canvas.restore();//注释4
    
            //绘制文本,文字中心和圆心保持一致
            DecimalFormat dt = new DecimalFormat("0.##");
            Paint.FontMetrics fontMetrics = paintText.getFontMetrics();
            float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
            float baseline= centerY + distance;
            canvas.drawText(dt.format(100 * _angle/360)+"%", centerX, baseline, paintText);//文字中心和圆心一致
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 注释1处:保存画布状态,已绘制的图形正常显示;以 (centerX, centerY) 为中心,逆时针旋转 90° ,此操作修改了画布的坐标,修改后 x轴和 y 轴的情况是这样的,
      在这里插入图片描述

    • 注释2处:创建 SweepGradient (梯度渐变,也称之为扫描式渐变,其效果有点类似雷达的扫描效果,从 3点钟方向顺时针旋转回到 3 点钟方向),画笔设置 Shader 为 SweepGradient ,设置 Shader 后画笔颜色就失效了,以 Shader 为准;画笔边缘设置为切边状(默认是半圆状),不设置的话颜色交汇处会有重叠。

    • 注释3处:因为画布坐标系变了,所以 drawArc() 方法中的 startAngle 参数变了。

    • 注释4处:还原对画布的修改,本例就是还原旋转画布的操作,坐标系恢复为原始状态。

    效果如图,gif 失真看颜色不清晰了 =.=|
    在这里插入图片描述

    参考资料:

    Android属性动画深入分析:让你成为动画牛人_singwhatiwanna的博客-CSDN博客
    Android Canvas的使用_南国樗里疾的博客-CSDN博客
    Android Canvas的drawText()和文字居中方案 - 简书
    自定义控件其实很简单1/3_AigeStudio的博客-CSDN博客
    关于Android Paint.Cap枚举和Paint.Join枚举的使用_5hand的博客-CSDN博客

  • 相关阅读:
    最新面试题:用友OC,美团三面已挂
    港联证券:股票浮筹比例的指标怎么用?
    《WebGIS快速开发教程第四版》重磅更新
    pytest集成allure报告(allure安装及配置以及如何实现集成)
    Java---多线程04:线程优先级、守护线程、线程同步、死锁
    操作字符串获取文件名字
    数据结构 | 绪论
    【云原生--Kubernetes】kubectl命令详解
    Intel汇编语言程序设计(第7版)第四章编程练习题答案
    时间复杂度与空间复杂度
  • 原文地址:https://blog.csdn.net/weixin_44021334/article/details/132755597