服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Android - Android自定义View实现圆环带数字百分比进度条

Android自定义View实现圆环带数字百分比进度条

2022-09-08 16:32热木星 Android

这篇文章主要为大家详细介绍了Android自定义View实现圆环带数字百分比进度条,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

分享一个自己制作的Android自定义View。是一个圆环形状的反映真实进度的进度条,百分比的进度文字跟随已完成进度的圆弧转动。以下是效果图:

Android自定义View实现圆环带数字百分比进度条

Android自定义View实现圆环带数字百分比进度条

这个自定义View可以根据需要设定圆环的宽度和百分比文字的大小。

先说一下思路:这个View一共分为三部分:第一部分也就是灰色的圆环部分,代表未完成的进度;第二部分是蓝色的圆弧部分,代表已经完成的进度;第三部分是红色的百分比的数字百分比文本,显示当前确切的完成进度。

下面是View的编写思路:

①:定义三个画笔,分别画灰色圆环,蓝色圆弧,红色文字;
②:构造器中初始化数据,设置三个画笔的属性;
③:重写View的onMeasure方法,得到本View的宽度,高度,计算出中心点的坐标;
④:由于这个View是一个圆环形状,所以定义本View宽高中较小者的一半为整个圆环部分(包括圆环和文字)最外侧的半径,这样使用者可以任意指定本View的宽高,圆环可以恰好嵌入其中,不会超出空间,也不会浪费空间;
⑤:绘制圆环需要一个RectF对象,创建一个RectF对象,指定它的左上右下边界均距离View中心为整个圆环部分(包括圆环和文字)最外侧的半径减去圆环画笔宽度和文字高度较大者的一半,这样,整个圆环部分(包括圆环和文字)的边界恰好会与View的边界重合;
⑥:绘制紧贴着圆环的文字,需要一个Path对象来指定文字的路径。给这个Path对象添加一小段圆弧的轨迹,然后在圆弧上面绘制文字。这段圆弧也需要一个RectF对象,但是这个RectF要比上一个RectF小一些,来保证文字恰好在圆环轨迹上;
⑦:然后是最关键的重绘部分,重写onDraw方法。首先画出灰色圆环,这个比较简单,它不需要随进度变化而变化,只是一个圆环;
⑧:其次画出蓝色的圆弧。设定圆弧开始的角度是-90度,也就是圆环顶部。扫过的角度是当前百分比乘以360度一整圈的角度,并且随着progress增加而不断增加,产生动画的效果;
⑨:最难的部分,画文字。在路径上面画文字并不难,但是要精确确定文字的位置,使文字的中央恰好处于蓝色进度条的最前端。在这里我使用了Paint的一个方法:getTextWidths,这个方法可以根据当前需要绘制的文字,返回所有单个字符的宽度组成的一个float型的数组,然后根据这个数组可以得到要绘制文字所占的宽度,进而可以得到需要的Path对象需要的圆弧轨迹的长度,然后根据圆弧长度,得到圆弧角度(width = sweepAngle * 2 * π * R / 360 →sweepAngle = width * 360 / 2 / π / R),然后就可以用这个Path对象去画文字了。
⑩:这里我们给View添加了 一个方法,setProgress,参数为int型的进度,这样外界使用者就可以根据实际进度来指定View的进度来显示当前实际工作完成的百分比。

下面是代码View代码:

?
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
public class CircleNumberProgress extends View {
 
  /** 进度条画笔的宽度(dp) */
  private int paintProgressWidth = 3;
 
  /** 文字百分比的字体大小(sp) */
  private int paintTextSize = 20;
 
  /** 未完成进度条的颜色 */
  private int paintUndoneColor = 0xffaaaaaa;
 
  /** 已完成进度条的颜色 */
  private int paintDoneColor = 0xff67aae4;
 
  /** 百分比文字的颜色 */
  private int paintTextColor = 0xffff0077;
 
  /** 设置进度条画笔的宽度(px) */
  private int paintProgressWidthPx;
 
  /** 文字画笔的尺寸(px) */
  private int paintTextSizePx;
  /** Context上下文环境 */
  private Context context;
 
  /** 调用者设置的进程 0 - 100 */
  private int progress;
 
  /** 画未完成进度圆弧的画笔 */
  private Paint paintUndone = new Paint();
 
  /** 画已经完成进度条的画笔 */
  private Paint paintDone = new Paint();
 
  /** 画文字的画笔 */
  private Paint paintText = new Paint();
 
  /** 包围进度条圆弧的矩形 */
  private RectF rectF = new RectF();
 
  /** 包围文字所在路径圆弧的矩形,比上一个矩形略小 */
  private RectF rectF2 = new RectF();
 
  /** 进度文字所在的路径 */
  private Path path = new Path();
 
  /** 文字所在路径圆弧的半径 */
  private int radiusText;
 
  /** 是否进行过了测量 */
  private boolean isMeasured = false;
 
  public CircleNumberProgress(Context context, AttributeSet attrs) {
    super(context, attrs);
    this.context = context;
    // 构造器中初始化数据
    initData();
  }
 
  /** 初始化数据 */
  private void initData() {
 
    // 设置进度条画笔的宽度
    paintProgressWidthPx = Utils.dip2px(context, paintProgressWidth);
 
    // 设置文字画笔的尺寸(px)
    paintTextSizePx = Utils.sp2px(context, paintTextSize);
 
    // 未完成进度圆环的画笔的属性
    paintUndone.setColor(paintUndoneColor);
    paintUndone.setStrokeWidth(paintProgressWidthPx);
    paintUndone.setAntiAlias(true);
    paintUndone.setStyle(Paint.Style.STROKE);
 
    // 已经完成进度条的画笔的属性
    paintDone.setColor(paintDoneColor);
    paintDone.setStrokeWidth(paintProgressWidthPx);
    paintDone.setAntiAlias(true);
    paintDone.setStyle(Paint.Style.STROKE);
 
    // 文字的画笔的属性
    paintText.setColor(paintTextColor);
    paintText.setTextSize(paintTextSizePx);
    paintText.setAntiAlias(true);
    paintText.setStyle(Paint.Style.STROKE);
    paintText.setTypeface(Typeface.DEFAULT_BOLD);
 
  }
 
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (!isMeasured) {
      getWidthAndHeight();
      isMeasured = true;
    }
  }
 
  /** 得到视图等的高度宽度尺寸数据 */
  private void getWidthAndHeight() {
 
    // 得到自定义视图的高度
    int viewHeight;
 
    // 得到自定义视图的宽度
    int viewWidth;
 
    // 得到自定义视图的X轴中心点
    int viewCenterX;
 
    // 得到自定义视图的Y轴中心点
    int viewCenterY;
 
    viewHeight = getMeasuredHeight();
    viewWidth = getMeasuredWidth();
    viewCenterX = viewWidth / 2;
    viewCenterY = viewHeight / 2;
 
    // 取本View长宽较小者的一半为整个圆环部分(包括圆环和文字)最外侧的半径
    int minLenth = viewHeight > viewWidth ? viewWidth / 2 : viewHeight / 2;
 
    // 比较文字高度和圆环宽度,如果文字高度较大,那么文字将突破圆环,否则,圆环会把文字包裹在内部
    Rect rect = new Rect();
    paintText.getTextBounds("100%", 0, "100%".length(), rect);
    int textHeight = rect.height();
 
    // 得到圆环的中间半径(外径和内径平均值)
    int radiusArc = minLenth - (paintProgressWidthPx > textHeight ? paintProgressWidthPx / 2 : textHeight / 2);
    rectF.left = viewCenterX - radiusArc;
    rectF.top = viewCenterY - radiusArc;
    rectF.right = viewCenterX + radiusArc;
    rectF.bottom = viewCenterY + radiusArc;
 
    // 文字所依赖路径圆弧的半径
    radiusText = radiusArc - textHeight / 2;
    rectF2.left = viewCenterX - radiusText;
    rectF2.top = viewCenterY - radiusText;
    rectF2.right = viewCenterX + radiusText;
    rectF2.bottom = viewCenterY + radiusText;
 
  }
 
  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
 
    // 画未完成进度的圆环
    canvas.drawArc(rectF, 0, 360, false, paintUndone);
 
    // 画已经完成进度的圆弧 从-90度开始,即从圆环顶部开始
    canvas.drawArc(rectF, -90, progress / 100.0f * 360, false, paintDone);
 
    // 为文字所在路径添加一段圆弧轨迹,进度为0%-9%时应该最短,进度为10%-99%时应该边长,进度为100%时应该最长
    // 这样才能保证文字和圆弧的进度一致,不会出现超前或者滞后的情况
 
    // 要画的文字
    String text = progress + "%";
 
    // 存储字符所有字符所占宽度的数组
    float[] widths = new float[text.length()];
 
    // 得到所有字符所占的宽度
    paintText.getTextWidths(text, 0, text.length(), widths);
 
    // 所有字符所占宽度之和
    float textWidth = 0;
    for (float f : widths) {
      textWidth += f;
    }
 
    // 根据长度得到路径对应的扫过的角度
    // width = sweepAngle * 2 * π * R / 360 ; sweepAngle = width * 360 / 2 /
    // π / R
    float sweepAngle = (float) (textWidth * 360 / 2 / Math.PI / radiusText);
 
    // 添加路径
    path.addArc(rectF2, progress * 3.6f - 90.0f - sweepAngle / 2.0f, sweepAngle);
 
    // 绘制进度的文字
    canvas.drawTextOnPath(text, path, 0, 0, paintText);
 
    // 重置路径
    path.reset();
  }
 
  /**
   * @param progress 外部传进来的当前进度,强制重绘
   */
  public void setProgress(int progress) {
    this.progress = progress;
    invalidate();
  }
}

调用者Activity代码:

?
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
public class NumberProgressBarActivity extends Activity {
 
  /** handler消息标识 */
  protected static final int WHAT_INCREASE = 1;
 
  /** 圆形带数字的进度条 */
  private CircleNumberProgress cnp_citcleNumberProgress;
 
  /** 指定给进度条的进程 */
  private int progress;
 
  private Handler handler = new Handler() {
    public void handleMessage(android.os.Message msg) {
      progress++;
      cnp_citcleNumberProgress.setProgress(progress);
      handler.sendEmptyMessageDelayed(WHAT_INCREASE, getRadomNumber(50, 300));
      if (progress >= 100) {
        handler.removeMessages(WHAT_INCREASE);
      }
    }
 
  };
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_number_progress_bar);
    cnp_citcleNumberProgress = (CircleNumberProgress) findViewById(R.id.cnp_citcleNumberProgress);
    Button btn_numberProgressBar = (Button) findViewById(R.id.btn_numberProgressBar);
    btn_numberProgressBar.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        increase();
      }
    });
  }
 
  private void increase() {
    progress = 0;
    handler.removeMessages(WHAT_INCREASE);
    handler.sendEmptyMessage(WHAT_INCREASE);
  }
 
  /**
   * 得到两个整数之间的一个随机数
   *
   * @param start 较小的数
   * @param end  较大的数
   * @return 随机整数
   */
  public int getRadomNumber(int start, int end) {
    return (int) (start + Math.random() * (end - start));
  }
}

布局:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical">
 
  <com.example.viewdemo.view.CircleNumberProgress
    android:id="@+id/cnp_citcleNumberProgress"
    android:layout_width="wrap_content"
    android:layout_height="300dp"
    android:layout_margin="20dp"
    android:background="#33897500"/>
 
  <Button
    android:id="@+id/btn_numberProgressBar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="开始"/>
 
</LinearLayout>

还有几个方法是dp,px,sp相互转换的也一并贴出来:

?
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
/**
 * 将px值转换为dip或dp值,保证尺寸大小不变
 */
public static int px2dip(Context context, float pxValue) {
  final float scale = context.getResources().getDisplayMetrics().density;
  return (int) (pxValue / scale + 0.5f);
}
 
/**
 * 将dip或dp值转换为px值,保证尺寸大小不变
 */
public static int dip2px(Context context, float dipValue) {
  final float scale = context.getResources().getDisplayMetrics().density;
  return (int) (dipValue * scale + 0.5f);
}
 
/**
 * 将px值转换为sp值,保证文字大小不变
 */
public static int px2sp(Context context, float pxValue) {
  final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
  return (int) (pxValue / fontScale + 0.5f);
}
 
/**
 * 将sp值转换为px值,保证文字大小不变
 */
public static int sp2px(Context context, float spValue) {
  final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
  return (int) (spValue * fontScale + 0.5f);
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://blog.csdn.net/qq_27102463/article/details/51592210

延伸 · 阅读

精彩推荐