自定义圆形ProgressBar

这篇博客介绍了如何在Android中自定义一个圆形的ProgressBar,作者在实现过程中增加了额外的效果,使其更华丽。文章详细讲解了实现步骤,包括定义attr属性、初始化、绘制圆弧以及添加动画处理。附带了完整的RoundProgressBar.java代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

公司的项目已经完成了几个了,无一例外,都有计步的功能,我的天哪,能不能有点创意,好吧,既然还要套代码,那么就把一些刁钻的UI给封装好,以后就可以直接使用提高效率了。说是刁钻,其实也并没有很夸张,只是原生控件实现不了而已。

这次的自定义View就是RoundProgressBar,顾名思义,圆形的ProgressBar而已,网上其实也有相关的博客,但我这里并不是简单的在View上面画个圆而已,我会稍微加点效果上去,尽量让它华丽点。

先上效果图:



好吧,步骤依然还是那几步:

1. 定义好attr属性,并在构造函数里将其初始化。

属性定义:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RoundProgressBar">
        <attr name="max" format="integer"/>
        <attr name="progress" format="integer"/>
        <attr name="count" format="integer"/>
        <attr name="countWidth" format="dimension"/>
        <attr name="countAngle" format="float"/>
        <attr name="countColor" format="color"/>
        <attr name="secondCountColor" format="color"/>
        <attr name="startAngle" format="float"/>
        <attr name="spacingFromAngle" format="float"/>
        <attr name="spacingToAngle" format="float"/>
        <attr name="isSpacing" format="boolean"/>
        <attr name="animation" format="boolean"/>
        <attr name="animationDuration" format="integer"/>
    </declare-styleable>
</resources>
属性注释:

	//进度
	private int progress;
	//最大进度
	private int max;
	//矩形小块的个数
	private int count;
	//小块的宽度,实际应该叫高度才对,这个width实际是画弧线的strokeWidth而已
	private int countWidth;
	//小块所占的角度
	private float countAngle;
	//小块的颜色,相当于progressColor
	private int countColor;
	//小块的第二颜色,即进度颜色,相当于secondProgressColor
	private int secondCountColor;
	//进度的起始角度,即从progress从0开始的角度
	private float startAngle;
	//是否挖空部分进度
	private boolean isSpacing;
	//挖空的开始角度
	private float spacingFromAngle;
	//挖空的结束角度
	private float spacingToAngle;
	//是否使用动画更新progress
	private boolean animation;
	//动画的持续时间
	private long animationDuration;

2. 重写onMeasure(),处理好宽高和默认宽高。

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// TODO Auto-generated method stub
		
		//主要是使view的宽高一样,以及当wrap_content时默认为100dp
		int widthMode=MeasureSpec.getMode(widthMeasureSpec);
		int width=MeasureSpec.getSize(widthMeasureSpec);
		if(widthMode==MeasureSpec.AT_MOST){
			width=dp2px(100);
		}
		
		int heightMode=MeasureSpec.getMode(heightMeasureSpec);
		int height=MeasureSpec.getSize(heightMeasureSpec);
		if(heightMode==MeasureSpec.AT_MOST){
			height=dp2px(100);
		}
		
		setMeasuredDimension(Math.min(width, height), Math.min(width, height));
	}

这里有点必须注意,使用LinearLayout作为父容器且值为指定值时上面代码测量高度是不会触发AT_MOST情况的,但使用RelativeLayout作为父容器时,即使是指定值或match_parent都会触发AT_MOST,是不是Bug有待进一步研究,先暂时用着LinearLayout先。


3. 重写onDraw(), 将块状ProgressBar画出来。
	@Override
	protected void onDraw(Canvas canvas) {
		// TODO Auto-generated method stub
		//每个小块之间的间隔角度
		float countSplitAngle=0;
		
		if(isSpacing){
			//360度减去所有小块所占角度和挖空的总角度,并除以count-1,减1是因为间隔只有count-1个,这样首尾小块位置才对称
			countSplitAngle=(360-count*countAngle-(spacingToAngle-spacingFromAngle))/(count-1);
		}else{
			//360度减去所有小块所占角度,并除以count,以整个360度作为进度
			countSplitAngle=(360-count*countAngle)/count;
		}
		
		//这是画弧线需要的参数,即在该矩形内画弧线
		RectF oval=new RectF(0+countWidth/2, 0+countWidth/2, getWidth()-countWidth/2, getHeight()-countWidth/2);
		
		//设置小块的高度,即弧线的strokeWidth
		paint.setStrokeWidth(countWidth);
		paint.setStyle(Style.STROKE);
		
		//先画进度,利用循环画弧把逐个小块画好,要把进度0点的起始角度加上
		paint.setColor(secondCountColor);				
		for(int i=0; i<count*progress/max; i++){
			float sweepStartAngle=startAngle+(countAngle+countSplitAngle)*i;	
			canvas.drawArc(oval, sweepStartAngle, countAngle, false, paint);
		}
		
		//换一种颜色把剩下的非进度小块画完
		paint.setColor(countColor);
		for(int i=count*progress/max; i<count; i++){
			float sweepStartAngle=startAngle+(countAngle+countSplitAngle)*i;
			canvas.drawArc(oval, sweepStartAngle, countAngle, false, paint);
		}
		
		//画个进度指针,这里随便画了一红色指针
		paint.setColor(Color.RED);
		if(isSpacing){
			canvas.drawArc(oval, startAngle+(360-(spacingToAngle-spacingFromAngle))*progress/max, countAngle, false, paint);
		}else{
			canvas.drawArc(oval, startAngle+360*progress/max, countAngle, false, paint);
		}
	}

这里需要注意的是,若需要挖空处理,那么起始角度应该设置到挖空末尾位置的角度,这里默认是不挖空,其实角度是90度。

另一点需要注意的是,画弧函数的参数里涉及到度数,其度数的0度位于右边,和平常我们学过的几何坐标一样,但android里顺时针是正,和我们学过的几何坐标相反。


4. 在setProgress()里添加动画处理。

	public void setProgress(int progress){
		//若使用动画,则利用ValueAnimator进行产生一个进度加载的缓冲效果
		if(animation){
			ValueAnimator valueAnimator=ValueAnimator.ofInt(this.progress, progress);
			valueAnimator.setDuration(animationDuration);			
			valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
				
				@Override
				public void onAnimationUpdate(ValueAnimator animation) {
					// TODO Auto-generated method stub
					RoundProgressBar.this.progress=Integer.valueOf(animation.getAnimatedValue().toString());
					//利用每一动画每帧改变progress/max,然后更新UI
					invalidate();
				}
			});
			valueAnimator.start();
		}else{
			this.progress=progress;
			invalidate();
		}		
	}

5. 布局文件添加自定义属性命名空间后使用该View。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res/com.example.roundprogressbar"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.example.roundprogressbar.RoundProgressBar 
        android:id="@+id/progressBar"
        android:layout_width="200dp"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="50dp"
        custom:countWidth="10dp"
        custom:count="100"
        custom:animation="true"
        custom:isSpacing="true"
        custom:startAngle="135"
        custom:animationDuration="2000"/>
    
    <com.example.roundprogressbar.RoundProgressBar 
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="20dp"
        custom:progress="40"
        custom:countWidth="15dp"
        custom:countAngle="2"
        custom:count="80"
        custom:countColor="#33ffffff"
        custom:secondCountColor="#ffffffff"
        android:background="#6495ED"/>

</LinearLayout>

贴上完整代码(布局文件在上面了):

RoundProgressBar.java

public class RoundProgressBar extends View {
		
	//进度
	private int progress;
	//最大进度
	private int max;
	//矩形小块的个数
	private int count;
	//小块的宽度,实际应该叫高度才对,这个width实际是画弧线的strokeWidth而已
	private int countWidth;
	//小块所占的角度
	private float countAngle;
	//小块的颜色,相当于progressColor
	private int countColor;
	//小块的第二颜色,即进度颜色,相当于secondProgressColor
	private int secondCountColor;
	//进度的起始角度,即从progress从0开始的角度
	private float startAngle;
	//是否挖空部分进度
	private boolean isSpacing;
	//挖空的开始角度
	private float spacingFromAngle;
	//挖空的结束角度
	private float spacingToAngle;
	//是否使用动画更新progress
	private boolean animation;
	//动画的持续时间
	private long animationDuration;
	
	private Paint paint;

	public RoundProgressBar(Context context) {
		this(context, null);
		// TODO Auto-generated constructor stub
	}

	public RoundProgressBar(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
		// TODO Auto-generated constructor stub
	}

	public RoundProgressBar(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		// TODO Auto-generated constructor stub
		TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.RoundProgressBar);
		progress=array.getInt(R.styleable.RoundProgressBar_progress, 0);
		max=array.getInt(R.styleable.RoundProgressBar_max, 100);
		count=array.getInt(R.styleable.RoundProgressBar_count, 100);
		countWidth=array.getDimensionPixelSize(R.styleable.RoundProgressBar_countWidth, dp2px(10));
		countAngle=array.getFloat(R.styleable.RoundProgressBar_countAngle, 1f);
		countColor=array.getColor(R.styleable.RoundProgressBar_countColor, Color.GRAY);
		secondCountColor=array.getColor(R.styleable.RoundProgressBar_secondCountColor, Color.GREEN);
		startAngle=array.getFloat(R.styleable.RoundProgressBar_startAngle, 90f); 
		isSpacing=array.getBoolean(R.styleable.RoundProgressBar_isSpacing, false);
		spacingFromAngle=array.getFloat(R.styleable.RoundProgressBar_spacingFromAngle, 45f);
		spacingToAngle=array.getFloat(R.styleable.RoundProgressBar_spacingToAngle, 135f);
		animation=array.getBoolean(R.styleable.RoundProgressBar_animation, false);
		animationDuration=array.getInteger(R.styleable.RoundProgressBar_animationDuration, 2000);
				
		array.recycle();
		
		paint=new Paint();
		paint.setAntiAlias(true);
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// TODO Auto-generated method stub
		
		//主要是使view的宽高一样,以及当wrap_content时默认为100dp
		
		int widthMode=MeasureSpec.getMode(widthMeasureSpec);
		int width=MeasureSpec.getSize(widthMeasureSpec);Log.d("zz", "111:"+width);
		if(widthMode==MeasureSpec.AT_MOST){
			width=dp2px(100);Log.d("zz", "222:"+width);
		}
		
		int heightMode=MeasureSpec.getMode(heightMeasureSpec);
		int height=MeasureSpec.getSize(heightMeasureSpec);Log.d("zz", "333:"+height);
		if(heightMode==MeasureSpec.AT_MOST){
			height=dp2px(100);Log.d("zz", "444:"+height);
		}
		
		setMeasuredDimension(Math.min(width, height), Math.min(width, height));
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		// TODO Auto-generated method stub
		//每个小块之间的间隔角度
		float countSplitAngle=0;
		
		if(isSpacing){
			//360度减去所有小块所占角度和挖空的总角度,并除以count-1,减1是因为间隔只有count-1个,这样首尾小块位置才对称
			countSplitAngle=(360-count*countAngle-(spacingToAngle-spacingFromAngle))/(count-1);
		}else{
			//360度减去所有小块所占角度,并除以count,以整个360度作为进度
			countSplitAngle=(360-count*countAngle)/count;
		}
		
		//这是画弧线需要的参数,即在该矩形内画弧线
		RectF oval=new RectF(0+countWidth/2, 0+countWidth/2, getWidth()-countWidth/2, getHeight()-countWidth/2);
		
		//设置小块的高度,即弧线的strokeWidth
		paint.setStrokeWidth(countWidth);
		paint.setStyle(Style.STROKE);
		
		//先画进度,利用循环画弧把逐个小块画好,要把进度0点的起始角度加上
		paint.setColor(secondCountColor);				
		for(int i=0; i<count*progress/max; i++){
			float sweepStartAngle=startAngle+(countAngle+countSplitAngle)*i;	
			canvas.drawArc(oval, sweepStartAngle, countAngle, false, paint);
		}
		
		//换一种颜色把剩下的非进度小块画完
		paint.setColor(countColor);
		for(int i=count*progress/max; i<count; i++){
			float sweepStartAngle=startAngle+(countAngle+countSplitAngle)*i;
			canvas.drawArc(oval, sweepStartAngle, countAngle, false, paint);
		}
		
		//画个进度指针,这里随便画了一红色指针
		paint.setColor(Color.RED);
		if(isSpacing){
			canvas.drawArc(oval, startAngle+(360-(spacingToAngle-spacingFromAngle))*progress/max, countAngle, false, paint);
		}else{
			canvas.drawArc(oval, startAngle+360*progress/max, countAngle, false, paint);
		}
	}
	

	public void setProgress(int progress){
		//若使用动画,则利用ValueAnimator进行产生一个进度加载的缓冲效果
		if(animation){
			ValueAnimator valueAnimator=ValueAnimator.ofInt(this.progress, progress);
			valueAnimator.setDuration(animationDuration);			
			valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
				
				@Override
				public void onAnimationUpdate(ValueAnimator animation) {
					// TODO Auto-generated method stub
					RoundProgressBar.this.progress=Integer.valueOf(animation.getAnimatedValue().toString());
					//利用每一动画每帧改变progress/max,然后更新UI
					invalidate();
				}
			});
			valueAnimator.start();
		}else{
			this.progress=progress;
			invalidate();
		}		
	}

	//dp转px单位
	private int dp2px(int dp){
		return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
	}
}

MainActivity.java

public class MainActivity extends Activity {
	
	private RoundProgressBar progressBar;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		progressBar=(RoundProgressBar)findViewById(R.id.progressBar);
		progressBar.post(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				progressBar.setProgress(80);
			}
		});
	}
}

代码下载: github



---------------------------------程序员小记----------------------------------

昨天爬山,今天又在健身房苦练,好累好累,睡觉先,明天继续,加油~






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值