Android自定义进度条的实现

一、需求及效果图

实现的效果如下图:

Android 自定义圆形进度View android圆环进度条渐变_android动态圆环进度条

如上图的效果。自定义的圆环进度条可以根据设置的进度来以动画的形式一点点的画出需要显示的进度圆弧(这里是静态图片,实际效果是可以动态的画蓝色的圆弧)。


二、自定义View的代码

直接上该项目的主要代码:


RoundProgressBar.class:


package com.example.projecttest;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.View;
public class RoundProgressBar extends View {
	private Paint paint;//画笔
	private int roundColor;//底层圆环的颜色
	private int roundProgressColor;//圆环进度的颜色
	private float roundWidth;//圆环的宽度
	private int textColor;//圆环中心显示的文字的颜色
	private float textSize;//圆环中心显示的文字的大小
	private int max;//最大进度
	private int progress;//设置的需要显示的进度
	private boolean textIsDisplayable;//是否显示中间的进度
	private int style;//进度的风格,实心或者空心
	public static final int STROKE = 0;
	public static final int FILL = 1;

	public RoundProgressBar(Context context) {
		this(context, null);
	}

	public RoundProgressBar(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public RoundProgressBar(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		paint = new Paint();
		TypedArray mTypedArray = context.obtainStyledAttributes(attrs,
				R.styleable.RoundProgressBar);

		// 获取自定义属性和默认值
		roundColor = mTypedArray.getColor(
				R.styleable.RoundProgressBar_roundColor, Color.RED);//圆圈底色
		roundProgressColor = mTypedArray.getColor(
				R.styleable.RoundProgressBar_roundProgressColor, Color.GREEN);//圆圈的进度颜色
		textColor = mTypedArray.getColor(
				R.styleable.RoundProgressBar_textColor, Color.GREEN);//字体的颜色
		textSize = mTypedArray.getDimension(
				R.styleable.RoundProgressBar_textSize, 15);//字体的大小
		roundWidth = mTypedArray.getDimension(
				R.styleable.RoundProgressBar_roundWidth, 10);//圆圈的宽度
		max = mTypedArray.getInteger(R.styleable.RoundProgressBar_max, 100);//一圈代表的最大值
		textIsDisplayable = mTypedArray.getBoolean(
				R.styleable.RoundProgressBar_textIsDisplayable, true);
		style = mTypedArray.getInt(R.styleable.RoundProgressBar_style, 0);//圆圈的样式0-STROKE  1-FILL

		/**
		 * 回收TypedArray,以便后面重用。在调用这个函数后,你就不能再使用这个TypedArray。
		 * 在TypedArray后调用recycle主要是为了缓存。当recycle被调用后,
		 * 这就说明这个对象从现在可以被重用了。TypedArray 内部持有部分数组,
		 * 它们缓存在Resources类中的静态字段中,这样就不用每次使用前都需要分配内存
		 */
		mTypedArray.recycle();
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		/**
		 * 画最外层的大圆环
		 */
		int centre = getWidth() / 2; // 获取圆心的x坐标
		int radius = (int) (centre - roundWidth); // 圆环的半径
		paint.setColor(roundColor); // 设置圆环的颜色
		paint.setStyle(Paint.Style.STROKE); // 设置空心
		paint.setStrokeWidth(roundWidth); // 设置圆环的宽度
		paint.setAntiAlias(true); // 消除锯齿
		paint.setAlpha(180);
		RectF oval2 = new RectF(centre - radius, centre - radius, centre
				+ radius, centre + radius);//绘制弧线
		canvas.drawArc(oval2, 0, 360, false, paint); // 根据进度画圆弧。这里也可以直接画圆
		/**
		 * 画进度百分比文字
		 */
		paint.setStrokeWidth(0);
		paint.setAlpha(255);
		paint.setColor(textColor);
		paint.setTextSize(80);
		paint.setTypeface(Typeface.DEFAULT_BOLD); // 设置字体
		float textWidth = paint.measureText(progress + "%"); // 测量字体宽度,我们需要根据字体的宽度设置在圆环中间
		if (textIsDisplayable && style == STROKE) {
			canvas.drawText(progress + "%", centre - textWidth / 2, centre
					+ textSize / 2, paint); // 画出进度百分比
		}
		/**
		 * 画圆弧 ,画圆环的进度
		 */
		paint.setStrokeWidth(roundWidth); // 设置圆环的宽度
		paint.setStrokeCap(Paint.Cap.ROUND);//设置画笔的始末端是圆角
		paint.setColor(roundProgressColor); // 设置进度的颜色
		RectF oval = new RectF(centre - radius, centre - radius, centre
				+ radius, centre + radius); // 用于定义的圆弧的形状和大小的界限
		switch (style) {
		case STROKE: {
			paint.setStyle(Paint.Style.STROKE);
			drawArc(canvas, oval);
			break;
		}
		case FILL: {
			paint.setStyle(Paint.Style.FILL_AND_STROKE);
			drawArc(canvas, oval);
			break;
		}
		}

	}

	private void drawArc(Canvas canvas, RectF oval) {
		if(curDegree <= goalDegree){
			canvas.drawArc(oval, -90, curDegree, false, paint); // 根据进度画圆弧
			/**
			 * 这里如果不想使用handler机制,
			 * 也可以在这里调用Invalidate方法。
			 * 因为Invalidate方法会导致onDraw方法的调用,
			 * 也可以实现相同的效果
			 */
			handler.sendEmptyMessage(88);
		}else{
			canvas.drawArc(oval, -90, goalDegree, false, paint); // 根据进度画圆弧
		}
	}
	private float goalDegree = 0;//设置的要显示的最终度数
	private float curDegree = 0;//当前显示的度数
	private Handler handler = new Handler(){
		public void handleMessage(Message msg) {
			curDegree += 1;
			postInvalidate();//这里可以直接使用invalidate();方法。因为是通过Handler发的消息在UI线程中
		};
	};
	
	public synchronized int getMax() {
		return max;
	}

	/**
	 * 设置进度的最大值
	 * @param max
	 */
	public synchronized void setMax(int max) {
		if (max < 0) {
			throw new IllegalArgumentException("max not less than 0");
		}
		this.max = max;
	}

	/**
	 * 获取进度.需要同步
	 * @return
	 */
	public synchronized int getProgress() {
		return progress;
	}

	/**
	 * 设置进度,此为线程安全控件,由于考虑多线的问题,需要同步 刷新界面调用postInvalidate()能在非UI线程刷新
	 * @param progress
	 */
	public synchronized void setProgress(int progress) {
		if (progress < 0) {
			throw new IllegalArgumentException("progress not less than 0");
		}
		if (progress > max) {
			progress = max;
		}
		if (progress <= max) {
			this.progress = progress;
			goalDegree = 360 * progress / max;
			curDegree = 0;
			postInvalidate();
		}

	}

	public int getCricleColor() {
		return roundColor;
	}

	public void setCricleColor(int cricleColor) {
		this.roundColor = cricleColor;
	}

	public int getCricleProgressColor() {
		return roundProgressColor;
	}

	public void setCricleProgressColor(int cricleProgressColor) {
		this.roundProgressColor = cricleProgressColor;
	}

	public int getTextColor() {
		return textColor;
	}

	public void setTextColor(int textColor) {
		this.textColor = textColor;
	}

	public float getTextSize() {
		return textSize;
	}

	public void setTextSize(float textSize) {
		this.textSize = textSize;
	}

	public float getRoundWidth() {
		return roundWidth;
	}

	public void setRoundWidth(float roundWidth) {
		this.roundWidth = roundWidth;
	}

	public boolean isTextIsDisplayable() {
		return textIsDisplayable;
	}

	public void setTextIsDisplayable(boolean textIsDisplayable) {
		this.textIsDisplayable = textIsDisplayable;
	}

}




MainActivity.class:


package com.example.projecttest;

import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;

public class MainActivity extends Activity {

	private EditText et;
	private RoundProgressBar rpb;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		et = (EditText) findViewById(R.id.et);
		rpb = (RoundProgressBar) findViewById(R.id.rpb);
	}

	public void click(View v){
		String etStr = et.getText().toString().trim();
		if(TextUtils.isEmpty(etStr)){
			etStr = "50";
		}
		int progress;
		try {
			progress = Integer.parseInt(etStr);
		} catch (NumberFormatException e) {
			e.printStackTrace();
			progress = 50;
		}
		rpb.setProgress(progress);
	}
	
	
}




activity_layout.xml:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/com.example.projecttest"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <EditText
        android:id="@+id/et"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="number" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="click"
        android:text="输入百分比0-100" />

    <com.example.projecttest.RoundProgressBar
        android:id="@+id/rpb"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_gravity="center"
        app:roundColor="#ff0000"
        app:roundProgressColor="#32BFF9"
        app:roundWidth="20dp" />


</LinearLayout>



attrs.xml:


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RoundProgressBar">
        <attr name="roundColor" format="color"/>
        <attr name="roundProgressColor" format="color"/>
        <attr name="roundWidth" format="dimension"></attr>
        <attr name="textColor" format="color"/>
        <attr name="textSize" format="dimension"/>
        <attr name="max" format="integer"></attr>
        <attr name="textIsDisplayable" format="boolean"></attr>
        <attr name="style">
            <enum name="STROKE" value="0"></enum>
            <enum name="FILL" value="1"></enum>
        </attr>
    </declare-styleable>
    
</resources>



三、代码简介

实现该效果主要是通过不断的重复调用onDraw方法来每次绘制一段圆弧来实现的动态的效果。非常简单的小例子,可以帮助我们学习自定义的View。其中用到的知识有自定义属性的使用,这个在以前的博客中有使用的具体步骤,还有一些与Canvas类相关的方法以及与Paint类相关的方法。这些类中的方法以后会有总结。目前代码中使用到的方法代码中都有详细的解释。下边说一下容易混淆的点:

1、invalidate();和postInvalidate();的区别

Android提供了Invalidate方法实现界面刷新,但是Invalidate不能直接在线程中调用,因为他是违背了单线程模型:Android UI操作并不是线程安全的,并且这些操作必须在UI线程中调用。  

   invalidate()是用来刷新View的,必须是在UI线程中进行工作。比如在修改某个view的显示时,调用invalidate()才能看到重新绘制的界面。invalidate()的调用是把之前的旧的view从主UI线程队列中pop掉。 一个Android 程序默认情况下也只有一个进程,但一个进程下却可以有许多个线程。在这么多线程当中,把主要是负责控制UI界面的显示、更新和控件交互的线程称为UI线程,由于onCreate()方法是由UI线程执行的,所以也可以把UI线程理解为主线程。其余的线程可以理解为工作者线程。invalidate()得在UI线程中被调动,在工作者线程中可以通过Handler来通知UI线程进行界面更新。

   postInvalidate()在工作者线程中被调用。


2、为什么需要在TypedArray后调用recycle?

当我们没有在使用TypedArray后调用recycle,编译器会提示“This TypedArray should be recycled after use with #recycle()”。

    官方的解释是:回收TypedArray,以便后面重用。在调用这个函数后,你就不能再使用这个TypedArray。

在TypedArray后调用recycle主要是为了缓存。当recycle被调用后,这就说明这个对象从现在可以被重用了。TypedArray 内部持有部分数组,它们缓存在Resources类中的静态字段中,这样就不用每次使用前都需要分配内存。你可以看看TypedArray.recycle()中的代码:

/**
 * Give back a previously retrieved StyledAttributes, for later re-use.
 */
public void recycle() {
    synchronized (mResources.mTmpValue) {
        TypedArray cached = mResources.mCachedStyledAttributes;
        if (cached == null || cached.mData.length < mData.length) {
            mXml = null;
            mResources.mCachedStyledAttributes = this;
        }
    }
}

注意:写代码时要注意多线程同步的问题。

四、源码下载

有需要下载代码的可以直接点击下边的链接免费下载:

点击打开链接