您的位置:首页 > 移动开发 > Android开发

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

2016-05-10 11:19 656 查看
Progress Wheel为GitHub热门项目,作者是:Todd-Davies,项目地址:
https://github.com/Todd-Davies/ProgressWheel
前三种实现方式代码出自:
http://stormzhang.com/openandroid/2013/11/15/ href="http://lib.csdn.net/base/15" target=_blank>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——/article/1562126.html】

Android视图状态及重绘流程分析,带你一步步深入了解View——/article/1562123.html】

Android自定义View的实现方法,带你一步步深入了解View——/article/1562122.html】

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

【RotateAnimation详解——http://blog.csdn.net/u012403246/article/details/41415799】

三、Android上的实现方式:

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

1、效果图:


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

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

XML:

[html] view
plain copy

print?

<?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>

[html] view
plain copy

print?

<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:

[html] view
plain copy

print?

<?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="@drawable/loading_360"/>

</rotate>

[html] view
plain copy

print?

<ProgressBar

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:indeterminateDrawable="@drawable/progressbar2"/>

3、效果图:


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

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

代码:

[java] view
plain copy

print?

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四部曲的讲解,讲的挺好的。

代码:

[java] view
plain copy

print?

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 = Math.min(layout_width, layout_height);

// 计算在绘制过程中在x,y方向的偏移量

int xOffset = layout_width - minValue;

int yOffset = layout_height - minValue;

// 间距加上偏移量

paddingTop = this.getPaddingTop() + (yOffset / 2);

paddingBottom = this.getPaddingBottom() + (yOffset / 2);

paddingLeft = this.getPaddingLeft() + (xOffset / 2);

paddingRight = this.getPaddingRight() + (xOffset / 2);

int width = getWidth(); //this.getLayoutParams().width;

int height = getHeight(); //this.getLayoutParams().height;

rectBounds = new RectF(paddingLeft,

paddingTop,

width - paddingRight,

height - paddingBottom);

circleBounds = new RectF(paddingLeft + barWidth,

paddingTop + barWidth,

width - paddingRight - barWidth,

height - paddingBottom - barWidth);

circleInnerContour = new RectF(circleBounds.left + (rimWidth / 2.0f) + (contourSize / 2.0f), circleBounds.top + (rimWidth / 2.0f) + (contourSize / 2.0f), circleBounds.right - (rimWidth / 2.0f) - (contourSize / 2.0f), circleBounds.bottom - (rimWidth / 2.0f) - (contourSize / 2.0f));

circleOuterContour = new RectF(circleBounds.left - (rimWidth / 2.0f) - (contourSize / 2.0f), circleBounds.top - (rimWidth / 2.0f) - (contourSize / 2.0f), circleBounds.right + (rimWidth / 2.0f) + (contourSize / 2.0f), circleBounds.bottom + (rimWidth / 2.0f) + (contourSize / 2.0f));

fullRadius = (width - paddingRight - barWidth) / 2;

circleRadius = (fullRadius - barWidth) + 1;

}

/**

* 从XML中解析控件的属性

*

* @param a the attributes to parse

*/

private void parseAttributes(TypedArray a) {

barWidth = (int) a.getDimension(R.styleable.ProgressWheel_barWidth,

barWidth);

rimWidth = (int) a.getDimension(R.styleable.ProgressWheel_rimWidth,

rimWidth);

spinSpeed = (int) a.getDimension(R.styleable.ProgressWheel_spinSpeed,

spinSpeed);

delayMillis = a.getInteger(R.styleable.ProgressWheel_delayMillis,

delayMillis);

if (delayMillis < 0) {

delayMillis = 0;

}

barColor = a.getColor(R.styleable.ProgressWheel_barColor, barColor);

barLength = (int) a.getDimension(R.styleable.ProgressWheel_barLength,

barLength);

textSize = (int) a.getDimension(R.styleable.ProgressWheel_textSize,

textSize);

textColor = (int) a.getColor(R.styleable.ProgressWheel_textColor,

textColor);

//如果text是空的,就无视它

if (a.hasValue(R.styleable.ProgressWheel_text)) {

setText(a.getString(R.styleable.ProgressWheel_text));

}

rimColor = (int) a.getColor(R.styleable.ProgressWheel_rimColor,

rimColor);

circleColor = (int) a.getColor(R.styleable.ProgressWheel_circleColor,

circleColor);

contourColor = a.getColor(R.styleable.ProgressWheel_contourColor, contourColor);

contourSize = a.getDimension(R.styleable.ProgressWheel_contourSize, contourSize);

// 使用TypedArray获得控件属性时必须要注意:使用结束后必须回收TypedArray的对象

a.recycle();

}

//----------------------------------

//动画

//----------------------------------

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

//绘制内圆

canvas.drawArc(circleBounds, 360, 360, false, circlePaint);

//绘制边界

canvas.drawArc(circleBounds, 360, 360, false, rimPaint);

canvas.drawArc(circleOuterContour, 360, 360, false, contourPaint);

canvas.drawArc(circleInnerContour, 360, 360, false, contourPaint);

//绘制条纹

if (isSpinning) {

canvas.drawArc(circleBounds, progress - 90, barLength, false,

barPaint);

} else {

canvas.drawArc(circleBounds, -90, progress, false, barPaint);

}

//绘制我们想要设置的文字 (并让它显示在圆水平和垂直方向的中心处)

float textHeight = textPaint.descent() - textPaint.ascent();

float verticalTextOffset = (textHeight / 2) - textPaint.descent();

for (String s : splitText) {

float horizontalTextOffset = textPaint.measureText(s) / 2;

canvas.drawText(s, this.getWidth() / 2 - horizontalTextOffset,

this.getHeight() / 2 + verticalTextOffset, textPaint);

}

if (isSpinning) {

scheduleRedraw();

}

}

private void scheduleRedraw() {

progress += spinSpeed;

if (progress > 360) {

progress = 0;

}

postInvalidateDelayed(delayMillis);

}

/**

* 判断wheel是否在旋转

*/

public boolean isSpinning() {

if(isSpinning){

return true;

} else {

return false;

}

}

/**

* 重设进度条的值

*/

public void resetCount() {

progress = 0;

setText("0%");

invalidate();

}

/**

* 停止进度条的旋转

*/

public void stopSpinning() {

isSpinning = false;

progress = 0;

postInvalidate();

}

/**

* 让进度条开启旋转模式

*/

public void spin() {

isSpinning = true;

postInvalidate();

}

/**

* 让进度条每次增加1(最大值为360)

*/

public void incrementProgress() {

isSpinning = false;

progress++;

if (progress > 360)

progress = 0;

setText(Math.round(((float) progress / 360) * 100) + "%");

postInvalidate();

}

/**

* 设置进度条为一个确切的数值

*/

public void setProgress(int i) {

isSpinning = false;

progress = i;

postInvalidate();

}

//----------------------------------

//get和set方法

//----------------------------------

/**

* 设置progress bar的文字并不需要刷新View

*

* @param text the text to show ('\n' constitutes a new line)

*/

public void setText(String text) {

this.text = text;

splitText = this.text.split("\n");

}

public int getCircleRadius() {

return circleRadius;

}

public void setCircleRadius(int circleRadius) {

this.circleRadius = circleRadius;

}

public int getBarLength() {

return barLength;

}

public void setBarLength(int barLength) {

this.barLength = barLength;

}

public int getBarWidth() {

return barWidth;

}

public void setBarWidth(int barWidth) {

this.barWidth = barWidth;

if ( this.barPaint != null ) {

this.barPaint.setStrokeWidth( this.barWidth );

}

}

public int getTextSize() {

return textSize;

}

public void setTextSize(int textSize) {

this.textSize = textSize;

if ( this.textPaint != null ) {

this.textPaint.setTextSize( this.textSize );

}

}

public int getPaddingTop() {

return paddingTop;

}

public void setPaddingTop(int paddingTop) {

this.paddingTop = paddingTop;

}

public int getPaddingBottom() {

return paddingBottom;

}

public void setPaddingBottom(int paddingBottom) {

this.paddingBottom = paddingBottom;

}

public int getPaddingLeft() {

return paddingLeft;

}

public void setPaddingLeft(int paddingLeft) {

this.paddingLeft = paddingLeft;

}

public int getPaddingRight() {

return paddingRight;

}

public void setPaddingRight(int paddingRight) {

this.paddingRight = paddingRight;

}

public int getBarColor() {

return barColor;

}

public void setBarColor(int barColor) {

this.barColor = barColor;

if ( this.barPaint != null ) {

this.barPaint.setColor( this.barColor );

}

}

public int getCircleColor() {

return circleColor;

}

public void setCircleColor(int circleColor) {

this.circleColor = circleColor;

if ( this.circlePaint != null ) {

this.circlePaint.setColor( this.circleColor);

}

}

public int getRimColor() {

return rimColor;

}

public void setRimColor(int rimColor) {

this.rimColor = rimColor;

if ( this.rimPaint != null ) {

this.rimPaint.setColor( this.rimColor );

}

}

public Shader getRimShader() {

return rimPaint.getShader();

}

public void setRimShader(Shader shader) {

this.rimPaint.setShader(shader);

}

public int getTextColor() {

return textColor;

}

public void setTextColor(int textColor) {

this.textColor = textColor;

if ( this.textPaint != null ) {

this.textPaint.setColor( this.textColor );

}

}

public int getSpinSpeed() {

return spinSpeed;

}

public void setSpinSpeed(int spinSpeed) {

this.spinSpeed = spinSpeed;

}

public int getRimWidth() {

return rimWidth;

}

public void setRimWidth(int rimWidth) {

this.rimWidth = rimWidth;

if ( this.rimPaint != null ) {

this.rimPaint.setStrokeWidth( this.rimWidth );

}

}

public int getDelayMillis() {

return delayMillis;

}

public void setDelayMillis(int delayMillis) {

this.delayMillis = delayMillis;

}

public int getContourColor() {

return contourColor;

}

public void setContourColor(int contourColor) {

this.contourColor = contourColor;

if ( contourPaint != null ) {

this.contourPaint.setColor( this.contourColor );

}

}

public float getContourSize() {

return this.contourSize;

}

public void setContourSize(float contourSize) {

this.contourSize = contourSize;

if ( contourPaint != null ) {

this.contourPaint.setStrokeWidth( this.contourSize );

}

}

}

源码下载

转自:http://blog.csdn.net/u012403246/article/details/41477427?utm_source=tuicool&utm_medium=referral
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: