自定義圓形ProgressBar

公司的項目已經完成了幾個了,無一例外,都有計步的功能,我的天哪,能不能有點創意,好吧,既然還要套代碼,那麼就把一些刁鑽的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



---------------------------------程序員小記----------------------------------

昨天爬山,今天又在健身房苦練,好累好累,睡覺先,明天繼續,加油~






發佈了33 篇原創文章 · 獲贊 50 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章