前言
该篇文章根据前面 重学 Android 自定义 View 系列(六):环形进度条 拓展而来。
最终效果如下:
1. 扩展功能
- 支持进度顺时针或逆时针显示
- 在进度条末尾添加自定义指针图片
- 使用线性渐变为进度条添加颜色效果
2. 关键技术点解析
2.1 进度方向控制的实现
通过添加一个 direction 属性,设置角度的正负,决定进度条是顺时针还是逆时针绘制:
public static final int CLOCKWISE = 1;
public static final int COUNTERCLOCKWISE = -1;
private int direction = COUNTERCLOCKWISE; // 默认逆时针// 在 onDraw 方法中计算扫过的角度时,加入方向
float sweepAngle = 360f * progress / maxProgress;
sweepAngle *= direction;// 绘制进度条
canvas.drawArc(rectF, startAngle, sweepAngle, false, progressPaint);
2.2 自定义指针图片的绘制
在环形进度条的末尾,绘制一张 Bitmap 图片作为指针,图片可由你传入,但指针的方向要和demo中的一致,如下:
绘制指针的步骤:
- 调整指针的绘制半径:确保指针贴合圆环外侧,加入一个 outerSize 参数用于控制指针漏出圆环的长度。
- 计算指针位置:使用三角函数计算图片中心点坐标。
- 旋转画布并绘制图片(关键):将画布旋转到指定角度后,再绘制指针图片。
用到的三角函数原理如下,再重温一下学校的知识:),因为在Java中 Math 函数计算三角函数用的是弧度而不是角度,所以代码使用了Math.toRadians
进行了角度转弧度。
核心代码实现:
private void drawPointer(Canvas canvas, float angle) {// 调整半径,使指针图片紧贴圆环外部float adjustedRadius = radius + backgroundPaint.getStrokeWidth() / 2 + outerSize;// 计算指针的中心点位置float rightCenterX = centerX + adjustedRadius * (float) Math.cos(Math.toRadians(angle));float rightCenterY = centerY + adjustedRadius * (float) Math.sin(Math.toRadians(angle));// 计算Bitmap左上角位置float left = rightCenterX - bitmapWidth;float top = rightCenterY - bitmapHeight / 2;// 保存画布状态,旋转画布canvas.save();canvas.rotate(angle, rightCenterX, rightCenterY);// 绘制指针Bitmapcanvas.drawBitmap(pointerBitmap, left, top, null);// 恢复画布状态canvas.restore();
}
重点是角度的计算 angle = startAngle + sweepAngle
,和指针的位移与旋转,结合三角函数计算坐标,并通过旋转画布保持图片对齐,使指针始终指向圆心位置。
2.3 渐变颜色的实现
为进度条添加线性渐变效果使用了 LinearGradient 着色器,实际效果按需求自定义,重点是 计算渐变起点和终点,因为有起始角度的存在,需要用到 圆心坐标、半径和起始角度计算:
private void updateGradient() {// 计算圆上的起点和终点坐标double startAngleRadians = Math.toRadians(startAngle);float startX = centerX + (float) (radius * Math.cos(startAngleRadians));float startY = centerY + (float) (radius * Math.sin(startAngleRadians));float endX = centerX - (float) (radius * Math.cos(startAngleRadians));float endY = centerY - (float) (radius * Math.sin(startAngleRadians));//线性渐变,从一个点渐变到另一个点,因为渐变的距离是圆的直径 所以,TileMode 在这里实际无意义gradientShader = new LinearGradient(startX, startY, endX, endY,progressColors, null,Shader.TileMode.CLAMP);progressPaint.setShader(gradientShader);
}
4. 定义自定义属性
<declare-styleable name="CircularProgressBarEx"><!-- 进度条的最大值 --><attr name="maxProgress" format="integer"/><!-- 当前进度 --><attr name="progress" format="integer"/><!-- 环形进度条的背景色 --><attr name="circleBackgroundColor" format="color"/><!-- 进度条的颜色 --><attr name="progressColor" format="color"/><!-- 进度条的宽度 --><attr name="circleWidth" format="dimension"/><!-- 显示进度文本 --><attr name="showProgressText" format="boolean"/><!-- 进度文本的颜色 --><attr name="progressTextColor" format="color"/><!-- 进度文本的大小 --><attr name="progressTextSize" format="dimension"/><!-- 开始角度 --><attr name="startAngle" format="enum"><enum name="angle0" value="0"/><enum name="angle90" value="90"/><enum name="angle180" value="180"/><enum name="angle270" value="270"/></attr><!-- 进度方向 --><attr name="direction"><enum name="clockwise" value="1"/><enum name="counterclockwise" value="-1"/></attr><!-- 指针漏出的长度 --><attr name="outerSize" format="dimension"/></declare-styleable>
5. 完整代码
public class CircularProgressBarEx extends View {private Paint backgroundPaint;private Paint progressPaint;private Paint textPaint;private RectF rectF;private Bitmap pointerBitmap; // 指针图标private float bitmapWidth; // Bitmap的宽度private float bitmapHeight; // Bitmap的高度private int outerSize = 5; //让指针漏出圆环的长度private int maxProgress = 100;private int progress = 0;private int circleBackgroundColor = Color.GRAY;private int[] progressColors = {Color.GREEN, Color.BLUE}; // 渐变颜色private int circleWidth = 20;private boolean showProgressText = true;private int progressTextColor = Color.BLACK;private int progressTextSize = 50;private int startAngle = 0; // 默认从左边开始private float centerX, centerY;private float radius;public static final int CLOCKWISE = 1;public static final int COUNTERCLOCKWISE = -1;private int direction = COUNTERCLOCKWISE; // 默认顺时针private LinearGradient gradientShader;public CircularProgressBarEx(Context context) {this(context, null);}public CircularProgressBarEx(Context context, AttributeSet attrs) {this(context, attrs, 0);}public CircularProgressBarEx(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);if (attrs != null) {TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CircularProgressBarEx,0, 0);try {maxProgress = typedArray.getInt(R.styleable.CircularProgressBarEx_maxProgress, 100);progress = typedArray.getInt(R.styleable.CircularProgressBarEx_progress, 0);circleBackgroundColor = typedArray.getColor(R.styleable.CircularProgressBarEx_circleBackgroundColor, Color.GRAY);circleWidth = typedArray.getDimensionPixelSize(R.styleable.CircularProgressBarEx_circleWidth, 20);showProgressText = typedArray.getBoolean(R.styleable.CircularProgressBarEx_showProgressText, true);progressTextColor = typedArray.getColor(R.styleable.CircularProgressBarEx_progressTextColor, Color.BLACK);progressTextSize = typedArray.getDimensionPixelSize(R.styleable.CircularProgressBarEx_progressTextSize, 50);startAngle = typedArray.getInt(R.styleable.CircularProgressBarEx_startAngle, 0);direction = typedArray.getInt(R.styleable.CircularProgressBarEx_direction, CLOCKWISE);outerSize = typedArray.getDimensionPixelSize(R.styleable.CircularProgressBarEx_outerSize, 5);} finally {typedArray.recycle();}}backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);backgroundPaint.setColor(circleBackgroundColor);backgroundPaint.setStyle(Paint.Style.STROKE);backgroundPaint.setStrokeWidth(circleWidth);progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);progressPaint.setStyle(Paint.Style.STROKE);progressPaint.setStrokeWidth(circleWidth);//progressPaint.setStrokeCap(Paint.Cap.ROUND);textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);textPaint.setColor(progressTextColor);textPaint.setTextSize(progressTextSize);textPaint.setTextAlign(Paint.Align.CENTER);rectF = new RectF();}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);int padding = circleWidth / 2 + outerSize;rectF.set(padding, padding, w - padding, h - padding);centerX = rectF.centerX();centerY = rectF.centerY();radius = rectF.width() / 2;updateGradient();}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);// 绘制背景圆环//canvas.drawArc(rectF, 0f, 360f, false, backgroundPaint);// 绘制背景圆环canvas.drawCircle(centerX, centerY, radius, backgroundPaint);// 计算进度角度float sweepAngle = 360f * progress / maxProgress;sweepAngle *= direction;// 绘制进度canvas.drawArc(rectF, startAngle, sweepAngle, false, progressPaint);// 绘制进度文本if (showProgressText) {String progressText = progress + "%";float x = getWidth() / 2f;float y = getHeight() / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;canvas.drawText(progressText, x, y, textPaint);}// 绘制指针if (pointerBitmap != null) {drawPointer(canvas, startAngle + sweepAngle);}}/*** 绘制指针** @param angle 指针的角度(画布坐标系下的角度)*/private void drawPointer(Canvas canvas, float angle) {//Log.d("drawPointer", "angle: " + angle);// 计算调整后的半径,使Bitmap边缘紧贴圆环最外部float adjustedRadius = radius + backgroundPaint.getStrokeWidth() / 2 + outerSize;// 确保Bitmap的右边上的中心点在圆上float rightCenterX = centerX + adjustedRadius * (float) Math.cos(Math.toRadians(angle));float rightCenterY = centerY + adjustedRadius * (float) Math.sin(Math.toRadians(angle));// 计算Bitmap左上角的位置,使得右边上的中心点位于计算出的坐标float left = rightCenterX - bitmapWidth;float top = rightCenterY - bitmapHeight / 2;// 保存画布状态canvas.save();// 将画布旋转,使得Bitmap对齐到半径上canvas.rotate(angle, rightCenterX, rightCenterY);// 绘制指针Bitmapcanvas.drawBitmap(pointerBitmap, left, top, null);// 恢复画布状态canvas.restore();}// 更新渐变private void updateGradient() {float centerX = rectF.centerX();float centerY = rectF.centerY();float radius = rectF.width() / 2;double startAngleRadians = Math.toRadians(startAngle);float startX = centerX + (float) (radius * Math.cos(startAngleRadians));float startY = centerY + (float) (radius * Math.sin(startAngleRadians));float endX = centerX - (float) (radius * Math.cos(startAngleRadians));float endY = centerY - (float) (radius * Math.sin(startAngleRadians));//Log.d("updateGradient", "startX: " + startX + ", startY: " + startY + ", endX: " + endX + ", endY: " + endY);//线性渐变,从一个点渐变到另一个点,因为渐变的距离是圆的直径 所以,TileMode 在这里实际无意义gradientShader = new LinearGradient(startX, startY, endX, endY,progressColors, null,Shader.TileMode.CLAMP);progressPaint.setShader(gradientShader);}// 设置进度public void setProgress(int progress) {this.progress = Math.max(0, Math.min(progress, maxProgress));invalidate();}// 设置指针图标public void setPointerBitmap(Bitmap bitmap) {this.pointerBitmap = bitmap;bitmapWidth = bitmap.getWidth();bitmapHeight = bitmap.getHeight();invalidate();}// 设置渐变颜色public void setProgressColors(int[] colors) {this.progressColors = colors;updateGradient();invalidate();}// 设置绘制方向public void setDirection(int direction) {this.direction = direction;invalidate();}// 设置开始角度public void setStartAngle(int angle) {this.startAngle = angle;updateGradient();invalidate();}// 获取当前进度public int getProgress() {return progress;}
}
6. 使用示例
xml:
<com.xaye.diyview.view.progressEx.CircularProgressBarExandroid:id="@+id/circularProgressBar"android:layout_width="140dp"android:layout_height="140dp"app:maxProgress="100"app:circleBackgroundColor="#DDDDDD"app:progressColor="#00B8D4"app:circleWidth="15dp"app:showProgressText="true"app:progressTextColor="#000000"app:progressTextSize="20sp"app:startAngle="angle0"app:direction="clockwise"app:outerSize="10dp" />
Activity:
mBind.circularProgressBar.setPointerBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.point))mBind.circularProgressBar2.setPointerBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.point))mBind.circularProgressBar3.setPointerBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.point))mBind.circularProgressBar4.setPointerBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.point))mBind.btnStartAnimation.clickNoRepeat {val animator = ValueAnimator.ofInt(0, 95)animator.setDuration(2000)animator.interpolator = LinearInterpolator()animator.addUpdateListener { animation ->val value = animation.animatedValue as IntmBind.circularProgressBar.setProgress(value)mBind.circularProgressBar2.progress = valuemBind.circularProgressBar3.progress = valuemBind.circularProgressBar4.progress = value}animator.start()}}
7. 最后
本篇文章由网友 Fas 的评论拓展而来,相比于之前那一篇还是稍稍有点难度的,哈哈。
源码及更多自定义View已上传Github:DiyView
另外给喜欢记笔记的同学安利一款好用的云笔记软件,对比大部分国内的这个算还不错的,免费好用:wolai