首页 > 代码库 > Android自定义控件:进度条的四种实现方式(Progress Wheel的解析)(源码 + Demo)

Android自定义控件:进度条的四种实现方式(Progress Wheel的解析)(源码 + Demo)

Progress Wheel为GitHub热门项目,作者是:Todd-Davies,项目地址:

https://github.com/Todd-Davies/ProgressWheel


前三种实现方式代码出自:

http://stormzhang.com/openandroid/2013/11/15/android-custom-loading/


(源码在最后)


最近一直在学习自定义控件,搜了许多大牛们Blog里分享的小教程,也上GitHub找了一些类似的控件进行学习。发现读起来都不太好懂,就想写这么一篇东西作为学习笔记吧。


一、控件介绍:

进度条在App中非常常见,例如下载进度、加载图片、打开文章、打开网页等等……都需要这么一个效果让用户知道我们的App正在读取,以构造良好的交互。如果没有这样一个效果的话,用户没法知道东西有没有下载好、图片加载了没有、文章打开了没……会让用户很不爽。基于这样的情景我们的UI设计师们创造了这样一个控件。


二、这篇文章会涉及的知识点:

跟我一样刚入门的Android菜鸟们,我推荐大家先了解一下这些知识点再往下看。这些知识点我也会推荐一些博客给大家看看,更推荐大家看文档里的解释,当然大牛们可以直接无视……


1、ClipDrawable类:能够对一个drawable类进行剪切操作(即只显示某一部分的区域,另一部分隐藏),显示多大的区域由level控制(level取值是0~10000)

【博客:http://blog.csdn.net/lonelyroamer/article/details/8244777】、没文档的可以在这看【http://www.apihome.cn/api/android/ClipDrawable.html】


2、自定义View:guolin大神的深入学习View四部曲

Android LayoutInflater原理分析,带你一步步深入了解View——http://blog.csdn.net/guolin_blog/article/details/12921889】

Android视图绘制流程完全解析,带你一步步深入了解View——http://blog.csdn.net/guolin_blog/article/details/16330267】

Android视图状态及重绘流程分析,带你一步步深入了解View——http://blog.csdn.net/guolin_blog/article/details/17045157】

Android自定义View的实现方法,带你一步步深入了解View——

http://blog.csdn.net/guolin_blog/article/details/17357967】

3、没看过我写的:Android自定义控件——老版优酷三级菜单的话,或许需要看看这个:

【RotateAnimation详解——】

三、Android上的实现方式:

(前三种方法比较简单,第四种方法是GitHub项目的解析,对前三种没兴趣可以直接跳到后边……)


1、效果图:


将进度条的变换过程分解为一帧一帧的图片,将这些一帧一帧的图片连起来构成一个动画。常用于:手机阅读网页、逛社区时,加载图片、文章等不需要清楚知道加载进度,但是需要知道是否进行加载的情景。


这种方法实现可以通过创建一个animation-list的XML文件,然后给系统API提供的ProgressBar的indeterminateDrawable属性就可以了。(这个属性应该是类似于设置一个动画吧……)


XML:

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false" >
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_01"
            android:gravity="left"/>
    </item>
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_02"
            android:gravity="left"/>
    </item>
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_03"
            android:gravity="left"/>
    </item>
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_04"
            android:gravity="left"/>
    </item>
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_05"
            android:gravity="left"/>
    </item>
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_06"
            android:gravity="left"/>
    </item>
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_07"
            android:gravity="left"/>
    </item>
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_08"
            android:gravity="left"/>
    </item>
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_09"
            android:gravity="left"/>
    </item>
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_10"
            android:gravity="left"/>
    </item>
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_11"
            android:gravity="left"/>
    </item>
    <item android:duration="150" >
        <clip 
            android:clipOrientation="horizontal"
            android:drawable="@drawable/loading_12"
            android:gravity="left"/>
    </item>

</animation-list>

    <ProgressBar 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminateDrawable="@drawable/progressbar1"
        />

2、效果图:

在上一篇有关自定义控件的博客里我们使用了一个RotateAnimation类来实现旋转效果 (http://blog.csdn.net/u012403246/article/details/41309161),其实,我们在这里也可以把一张图片,通过旋转,达到我们要的效果。本质上和上一种方法没多大区别。


我们只需要创建一个rotate的XML,对其属性进行一些简单的设置,然后加入我们要用的图片就可以了。


XML:

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:pivotX="50%"
    android:pivotY="50%"
    android:fromDegrees="0"
    android:toDegrees="360"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator" >
    <bitmap 
        android:antialias="true"
        android:filter="true"
        android:src=http://www.mamicode.com/"@drawable/loading_360"/>>
	<ProgressBar 
	    android:layout_width="wrap_content"
	    android:layout_height="wrap_content"
	    android:indeterminateDrawable="@drawable/progressbar2"/>

3、效果图:

我们可以弄两张照片,第一张是纯黑色的,然后把这张照片中心挖一个圆出来,圆区域弄成白色,挖出来的圆弄成第二张照片。我们不妨叠加显示两张照片,刚开始把第二张完全“遮住”,随着加载进度的增加,我们减少遮住的区域把第二张照片慢慢的显示出来。


Android上刚好就有这么一个ClipDrawable类,能够实现剪裁的过程。我们来看看怎么通过这样的方式自定义一个进度条控件。


代码:

public class MyProgressBar extends FrameLayout{
	private boolean running;
	private int progress = 0;
	private static final int MAX_PROGRESS = 10000;
	
	private ClipDrawable clip;
	
	private Handler handler = new Handler(){
		@Override
		public void handleMessage(android.os.Message msg) {
			if(msg.what == 0x123)
				clip.setLevel(progress);
		}
	};
	
	public MyProgressBar(Context context){
		this(context,null,0);
	}

	public MyProgressBar(Context context,AttributeSet attrs){
		this(context,null,0);
	}
	
	public MyProgressBar(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		Init(context);
	}
	
	public void Init(Context context){
		View view = LayoutInflater.from(context).inflate(R.layout.view, null);
		
		ImageView iv = (ImageView)view.findViewById(R.id.progress_img);
		
		addView(view);
		clip = (ClipDrawable)iv.getDrawable();
		
		Thread thread = new Thread(new Runnable() {
			
			@Override
			public void run() {
				running = true;
				while(running){
					handler.sendEmptyMessage(0x123);
					if(progress == MAX_PROGRESS)
						progress = 0;
					progress += 100;
					try {
						Thread.sleep(18);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		});
		thread.start();
	}

	public void stop(){
		progress = 0;
		running = false;
	}
}
通过代码我们可以看到,逻辑非常简单,关键就在于ClipDrawable的setLevel()方法,这个是设置剪裁效果的。


4、效果图:

实现一个View的子类——Progress Wheel类,实现进度条效果。具体的内容我都写在了注释上,如果不了解自定义控件的知识,可以去阅读guolin博客里自定义View四部曲的讲解,讲的挺好的。


代码

public class ProgressWheel extends View {

    //绘制View用到的各种长、宽带大小
    private int layout_height = 0;
    private int layout_width = 0;
    private int fullRadius = 100;
    private int circleRadius = 80;
    private int barLength = 60;
    private int barWidth = 20;
    private int rimWidth = 20;
    private int textSize = 20;
    private float contourSize = 0;

    //与页边的间距
    private int paddingTop = 5;
    private int paddingBottom = 5;
    private int paddingLeft = 5;
    private int paddingRight = 5;

    //View要绘制的颜色
    private int barColor = 0xAA000000;
    private int contourColor = 0xAA000000;
    private int circleColor = 0x00000000;
    private int rimColor = 0xAADDDDDD;
    private int textColor = 0xFF000000;

    //绘制要用的画笔
    private Paint barPaint = new Paint();
    private Paint circlePaint = new Paint();
    private Paint rimPaint = new Paint();
    private Paint textPaint = new Paint();
    private Paint contourPaint = new Paint();

    //绘制要用的矩形
    @SuppressWarnings("unused")
    private RectF rectBounds = new RectF();
    private RectF circleBounds = new RectF();
    private RectF circleOuterContour = new RectF();
    private RectF circleInnerContour = new RectF();

    //动画
    //每次绘制要移动的像素数目
    private int spinSpeed = 2;
    //绘制过程的时间间隔
    private int delayMillis = 0;
    int progress = 0;
    boolean isSpinning = false;

    //其他
    private String text = "";
    private String[] splitText = {};

    /**
     * ProgressWheel的构造方法
     *
     * @param context
     * @param attrs
     */
    public ProgressWheel(Context context, AttributeSet attrs) {
        super(context, attrs);

        parseAttributes(context.obtainStyledAttributes(attrs,
                R.styleable.ProgressWheel));
    }

    //----------------------------------
    //初始化一些元素
    //----------------------------------

    /*
     * 调用这个方法时,使View绘制为方形
     * From: http://www.jayway.com/2012/12/12/creating-custom-android-views-part-4-measuring-and-how-to-force-a-view-to-be-square/
     * 
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    	// 首先我们要调用超类的onMeasure借口 
    	// 原因是我们自己去实现一个方法获得长度、宽度太麻烦了
    	// 使用超类的的方法非常方便而且让复杂的细节可控
    	super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    	// 在这里我们不能使用getWidth()和getHeight()。 
    	// 因为这两个方法只能在View的布局完成后才能使用,而一个View的绘制过程是先绘制元素,再绘制Layout
    	// 所以我们必须使用getMeasuredWidth()和getMeasuredHeight()
        int size = 0;
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        int widthWithoutPadding = width - getPaddingLeft() - getPaddingRight();
        int heigthWithoutPadding = height - getPaddingTop() - getPaddingBottom();
        
        // 最后我们用一些简单的逻辑去计算View的大小并调用setMeasuredDimension()去设置View的大小
        // 在比较View的长宽前我们不考虑间距,但当我们设置View所需要绘制的面积时,我们要考虑它
        // 不考虑间距的View(View内的实际画面)此时就应该是方形的,但是由于间距的存在,最终View所占的面积可能不是方形的 
        if (widthWithoutPadding > heigthWithoutPadding) {
            size = heigthWithoutPadding;
        } else {
            size = widthWithoutPadding;
        }
        
        // 如果你重写了onMeasure()方法,你必须调用setMeasuredDimension()方法 
        // 这是你设置View大小的唯一途径  
        // 如果你不调用setMeasuredDimension()方法,父控件会抛出异常,并且程序会崩溃
        // 如果我们使用了超类的onMeasure()方法,我们就不是那么需要setMeasuredDimension()方法
        // 然而,重写onMeasure()方法是为了改变既有的绘制流程,所以我们必须调用setMeasuredDimension()方法以达到我们的目的
        setMeasuredDimension(size + getPaddingLeft() + getPaddingRight(), size + getPaddingTop() + getPaddingBottom());
    }

    /**
     * 使用onSizeChanged方法代替onAttachedToWindow获得View的面积
     * 因为这个方法会在测量了MATCH_PARENT和WRAP_CONTENT后马上被调用
     * 使用获得的面积设置View
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // Share the dimensions
        layout_width = w;
        layout_height = h;

        setupBounds();
        setupPaints();
        invalidate();
    }

    /**
     * 设置我们想要绘制的progress wheel的颜色
     */
    private void setupPaints() {
        barPaint.setColor(barColor);
        barPaint.setAntiAlias(true);
        barPaint.setStyle(Style.STROKE);
        barPaint.setStrokeWidth(barWidth);

        rimPaint.setColor(rimColor);
        rimPaint.setAntiAlias(true);
        rimPaint.setStyle(Style.STROKE);
        rimPaint.setStrokeWidth(rimWidth);

        circlePaint.setColor(circleColor);
        circlePaint.setAntiAlias(true);
        circlePaint.setStyle(Style.FILL);

        textPaint.setColor(textColor);
        textPaint.setStyle(Style.FILL);
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(textSize);

        contourPaint.setColor(contourColor);
        contourPaint.setAntiAlias(true);
        contourPaint.setStyle(Style.STROKE);
        contourPaint.setStrokeWidth(contourSize);
    }

    /**
     * 设置元素的边界
     */
    private void setupBounds() {
        // 为了保持宽度和长度的一致,我们要获得layout_width和layout_height中较小的一个,从而绘制一个圆
        int minValue = http://www.mamicode.com/Math.min(layout_width, layout_height);>
源码下载

Android自定义控件:进度条的四种实现方式(Progress Wheel的解析)(源码 + Demo)