Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南

马肤

温馨提示:这篇文章已超过440天没有更新,请注意相关的内容是否还可用!

摘要:,,本文介绍了如何在Android中自定义View,实现圆形进度条。通过自定义View,可以灵活控制进度条的样式、颜色、大小等属性,以满足不同需求。圆形进度条作为一种常见的UI元素,可以用于展示任务进度、加载状态等。本文详细阐述了自定义View的实现过程,包括绘制圆形进度条的关键步骤和注意事项。

目录

  • 前言
  • 1.基础
  • 2.绘制底层圆形
  • 3.绘制上层弧形
  • 4.绘制文本
  • 5.添加动画
  • 6.调整位置、宽高
  • 附代码
    • CircleProgressBar.java
    • 布局文件
    • Activity 调用
    • 7.自定义属性 attr
      • 7.1 创建 res/values/attrs.xml
      • 7.2 使用 TypedArray 获取 attrs
      • 7.3 在 xml 中初始化 attr
      • 7.4 效果
      • 附代码V2
        • CircleProgressBar.java
        • 布局文件
        • Activity 调用
        • attrs
        • 8.进度条按照进度变色
        • 参考资料:

          前言

          很多场景下都用到这种进度条,有的还带动画效果,

          Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南 第1张

          今天我也来写一个。

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

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

            那我们要做的就是:

            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);
                }
            }
            

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

                        
                    
            

            此时运行,是没效果的,因为这个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);
                }
            }
            

            效果,

            Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南 第2张

            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);
                }
            

            效果,

            Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南 第3张

            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);
                }
            

            效果,

            Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南 第4张

            可以看到,文本虽然居中,但是文本是显示在中心线上,

            Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南 第5张

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

            		//绘制文本,文字中心和圆心保持一致
                    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);//文字中心和圆心一致        
            

            效果,符合预期。

            Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南 第6张

            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();
                }
            

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

            外部调用,

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

            随机生成一个 0.0 - 0.1 的数值,乘以 100 设置为进度。

            效果,

            Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南 第7张

            可以看到动画效果, 虽然 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);
                      }
                  }
              

              修改 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);//文字中心和圆心一致
                  }
              

              分别写了 3 个布局,布局依次是 宽度等于高度 、宽度大宇高度、宽度小于高度,效果,

              Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南 第8张

              至此,基本是一个还可以的版本了。

              附代码

              贴下当前代码,

              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();
                  }
              }
              

              布局文件

              
                  
                      
                          
                      
                      
                          
                      
                      
                          
                      
                      
                  
                  
                      
                      
                      
                      
                  
              
              

              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;
                      }
                  }
              }
              

              7.自定义属性 attr

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

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

              7.1 创建 res/values/attrs.xml

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

              写入如下内容,

              
                  
                  
                       
                       
                       
                      
                      
                      
                      
                  
              
              

              中 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)
              

              是 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();
                  }
              

              有两个带 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); }

                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"
                                
                                /
                        
                		
                
                

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

                7.4 效果

                左一是自定义 attr 的效果,左二、左三是没有自定义 attr 的效果。

                差异有:底层圆形的颜色、画笔大小;上层弧形的颜色、画笔大小、开始的角度;中间文字的颜色。

                说明自定义 attr 起效了。

                Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南 第9张

                附代码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();
                    }
                }
                

                布局文件

                
                    
                        
                            
                        
                        
                            
                        
                        
                            
                        
                    
                    
                        
                        
                        
                        
                    
                
                

                Activity 调用

                和之前一样。

                attrs

                
                    
                    
                         
                         
                         
                        
                        
                        
                        
                    
                
                

                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处:保存画布状态,已绘制的图形正常显示;以 (centerX, centerY) 为中心,逆时针旋转 90° ,此操作修改了画布的坐标,修改后 x轴和 y 轴的情况是这样的,

                  Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南 第10张

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

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

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

                  效果如图,gif 失真看颜色不清晰了 =.=|

                  Android 自定义View之圆形进度条,Android 自定义圆形进度条View的实现指南 第11张

                  参考资料:

                  Android属性动画深入分析:让你成为动画牛人_singwhatiwanna的博客-CSDN博客

                  Android Canvas的使用_南国樗里疾的博客-CSDN博客

                  Android Canvas的drawText()和文字居中方案 - 简书

                  自定义控件其实很简单1/3_AigeStudio的博客-CSDN博客

                  关于Android Paint.Cap枚举和Paint.Join枚举的使用_5hand的博客-CSDN博客


0
收藏0
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。

相关阅读

  • 【研发日记】Matlab/Simulink自动生成代码(二)——五种选择结构实现方法,Matlab/Simulink自动生成代码的五种选择结构实现方法(二),Matlab/Simulink自动生成代码的五种选择结构实现方法详解(二)
  • 超级好用的C++实用库之跨平台实用方法,跨平台实用方法的C++实用库超好用指南,C++跨平台实用库使用指南,超好用实用方法集合,C++跨平台实用库超好用指南,方法与技巧集合
  • 【动态规划】斐波那契数列模型(C++),斐波那契数列模型(C++实现与动态规划解析),斐波那契数列模型解析与C++实现(动态规划)
  • 【C++】,string类底层的模拟实现,C++中string类的模拟底层实现探究
  • uniapp 小程序实现微信授权登录(前端和后端),Uniapp小程序实现微信授权登录全流程(前端后端全攻略),Uniapp小程序微信授权登录全流程攻略,前端后端全指南
  • Vue脚手架的安装(保姆级教程),Vue脚手架保姆级安装教程,Vue脚手架保姆级安装指南,Vue脚手架保姆级安装指南,从零开始教你如何安装Vue脚手架
  • 如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问,树莓派上本地部署Web站点及无公网IP远程访问指南,树莓派部署Web站点及无公网IP远程访问指南,本地部署与远程访问实践,树莓派部署Web站点及无公网IP远程访问实践指南,树莓派部署Web站点及无公网IP远程访问实践指南,本地部署与远程访问详解,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南。
  • vue2技术栈实现AI问答机器人功能(流式与非流式两种接口方法),Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法探究,Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法详解
  • 发表评论

    快捷回复:表情:
    评论列表 (暂无评论,0人围观)

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

    目录[+]

    取消
    微信二维码
    微信二维码
    支付宝二维码