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

一、控件介绍:

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

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

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

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


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

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

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

【RotateAnimation详解——】

三、Android上的实现方式:

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

1、效果图:


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

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

XML:

android:oneshot="false">
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_01"
android:gravity="left"/>
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_02"
android:gravity="left"/>
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_03"
android:gravity="left"/>
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_04"
android:gravity="left"/>
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_05"
android:gravity="left"/>
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_06"
android:gravity="left"/>
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_07"
android:gravity="left"/>
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_08"
android:gravity="left"/>
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_09"
android:gravity="left"/>
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_10"
android:gravity="left"/>
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_11"
android:gravity="left"/>
android:clipOrientation="horizontal"
android:drawable="@drawable/loading_12"
android:gravity="left"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminateDrawable="@drawable/progressbar1"
/>

2、效果图:


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

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

XML:
android:pivotX="50%"
android:pivotY="50%"
android:fromDegrees="0"
android:toDegrees="360"
android:interpolator="@android:anim/accelerate_decelerate_interpolator">
android:antialias="true"
android:filter="true"
android:src="@drawable/loading_360"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminateDrawable="@drawable/progressbar2"/>

3、效果图:


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

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

代码:
publicclassMyProgressBarextendsFrameLayout{
privatebooleanrunning;
privateintprogress =0;
privatestaticfinalintMAX_PROGRESS =10000;
privateClipDrawable clip;
privateHandler handler =newHandler(){
@Override
publicvoidhandleMessage(android.os.Message msg) {
if(msg.what ==0x123)
clip.setLevel(progress);
}
};
publicMyProgressBar(Context context){
this(context,null,0);
}
publicMyProgressBar(Context context,AttributeSet attrs){
this(context,null,0);
}
publicMyProgressBar(Context context, AttributeSet attrs,intdefStyle) {
super(context, attrs, defStyle);
Init(context);
}
publicvoidInit(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 = newThread(newRunnable() {
@Override
publicvoidrun() {
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();
}
publicvoidstop(){
progress = 0;
running = false;
}
}

通过代码我们可以看到,逻辑非常简单,关键就在于ClipDrawable的setLevel()方法,这个是设置剪裁效果的。

4、效果图:


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

代码:

publicclassProgressWheelextendsView {
//绘制View用到的各种长、宽带大小
privateintlayout_height =0;
privateintlayout_width =0;
privateintfullRadius =100;
privateintcircleRadius =80;
privateintbarLength =60;
privateintbarWidth =20;
privateintrimWidth =20;
privateinttextSize =20;
privatefloatcontourSize =0;
//与页边的间距
privateintpaddingTop =5;
privateintpaddingBottom =5;
privateintpaddingLeft =5;
privateintpaddingRight =5;
//View要绘制的颜色
privateintbarColor =0xAA000000;
privateintcontourColor =0xAA000000;
privateintcircleColor =0x00000000;
privateintrimColor =0xAADDDDDD;
privateinttextColor =0xFF000000;
//绘制要用的画笔
privatePaint barPaint =newPaint();
privatePaint circlePaint =newPaint();
privatePaint rimPaint =newPaint();
privatePaint textPaint =newPaint();
privatePaint contourPaint =newPaint();
//绘制要用的矩形
@SuppressWarnings("unused")
privateRectF rectBounds =newRectF();
privateRectF circleBounds =newRectF();
privateRectF circleOuterContour =newRectF();
privateRectF circleInnerContour =newRectF();
//动画
//每次绘制要移动的像素数目
privateintspinSpeed =2;
//绘制过程的时间间隔
privateintdelayMillis =0;
intprogress =0;
booleanisSpinning =false;
//其他
privateString text ="";
privateString[] splitText = {};
/**
* ProgressWheel的构造方法
*
* @param context
* @param attrs
*/
publicProgressWheel(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
protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec) {
// 首先我们要调用超类的onMeasure借口
// 原因是我们自己去实现一个方法获得长度、宽度太麻烦了
// 使用超类的的方法非常方便而且让复杂的细节可控
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 在这里我们不能使用getWidth()和getHeight()。
// 因为这两个方法只能在View的布局完成后才能使用,而一个View的绘制过程是先绘制元素,再绘制Layout
// 所以我们必须使用getMeasuredWidth()和getMeasuredHeight()
intsize =0;
intwidth = getMeasuredWidth();
intheight = getMeasuredHeight();
intwidthWithoutPadding = width - getPaddingLeft() - getPaddingRight();
intheigthWithoutPadding = 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
protectedvoidonSizeChanged(intw,inth,intoldw,intoldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Share the dimensions
layout_width = w;
layout_height = h;
setupBounds();
setupPaints();
invalidate();
}
/**
* 设置我们想要绘制的progress wheel的颜色
*/
privatevoidsetupPaints() {
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);
}
/**
* 设置元素的边界
*/
privatevoidsetupBounds() {
// 为了保持宽度和长度的一致,我们要获得layout_width和layout_height中较小的一个,从而绘制一个圆
intminValue = Math.min(layout_width, layout_height);
// 计算在绘制过程中在x,y方向的偏移量
intxOffset = layout_width - minValue;
intyOffset = layout_height - minValue;
// 间距加上偏移量
paddingTop = this.getPaddingTop() + (yOffset /2);
paddingBottom = this.getPaddingBottom() + (yOffset /2);
paddingLeft = this.getPaddingLeft() + (xOffset /2);
paddingRight = this.getPaddingRight() + (xOffset /2);
intwidth = getWidth();//this.getLayoutParams().width;
intheight = getHeight();//this.getLayoutParams().height;
rectBounds = newRectF(paddingLeft,
paddingTop,
width - paddingRight,
height - paddingBottom);
circleBounds = newRectF(paddingLeft + barWidth,
paddingTop + barWidth,
width - paddingRight - barWidth,
height - paddingBottom - barWidth);
circleInnerContour = newRectF(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 = newRectF(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
*/
privatevoidparseAttributes(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();
}
//----------------------------------
//动画
//----------------------------------
protectedvoidonDraw(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);
}
//绘制我们想要设置的文字 (并让它显示在圆水平和垂直方向的中心处)
floattextHeight = textPaint.descent() - textPaint.ascent();
floatverticalTextOffset = (textHeight /2) - textPaint.descent();
for(String s : splitText) {
floathorizontalTextOffset = textPaint.measureText(s) /2;
canvas.drawText(s, this.getWidth() /2- horizontalTextOffset,
this.getHeight() /2+ verticalTextOffset, textPaint);
}
if(isSpinning) {
scheduleRedraw();
}
}
privatevoidscheduleRedraw() {
progress += spinSpeed;
if(progress >360) {
progress = 0;
}
postInvalidateDelayed(delayMillis);
}
/**
*   判断wheel是否在旋转
*/
publicbooleanisSpinning() {
if(isSpinning){
returntrue;
} else{
returnfalse;
}
}
/**
* 重设进度条的值
*/
publicvoidresetCount() {
progress = 0;
setText("0%");
invalidate();
}
/**
* 停止进度条的旋转
*/
publicvoidstopSpinning() {
isSpinning = false;
progress = 0;
postInvalidate();
}
/**
* 让进度条开启旋转模式
*/
publicvoidspin() {
isSpinning = true;
postInvalidate();
}
/**
* 让进度条每次增加1(最大值为360)
*/
publicvoidincrementProgress() {
isSpinning = false;
progress++;
if(progress >360)
progress = 0;
setText(Math.round(((float) progress /360) *100) +"%");
postInvalidate();
}
/**
* 设置进度条为一个确切的数值
*/
publicvoidsetProgress(inti) {
isSpinning = false;
progress = i;
postInvalidate();
}
//----------------------------------
//get和set方法
//----------------------------------
/**
* 设置progress bar的文字并不需要刷新View
*
* @param text the text to show ('\n' constitutes a new line)
*/
publicvoidsetText(String text) {
this.text = text;
splitText = this.text.split("\n");
}
publicintgetCircleRadius() {
returncircleRadius;
}
publicvoidsetCircleRadius(intcircleRadius) {
this.circleRadius = circleRadius;
}
publicintgetBarLength() {
returnbarLength;
}
publicvoidsetBarLength(intbarLength) {
this.barLength = barLength;
}
publicintgetBarWidth() {
returnbarWidth;
}
publicvoidsetBarWidth(intbarWidth) {
this.barWidth = barWidth;
if(this.barPaint !=null) {
this.barPaint.setStrokeWidth(this.barWidth );
}
}
publicintgetTextSize() {
returntextSize;
}
publicvoidsetTextSize(inttextSize) {
this.textSize = textSize;
if(this.textPaint !=null) {
this.textPaint.setTextSize(this.textSize );
}
}
publicintgetPaddingTop() {
returnpaddingTop;
}
publicvoidsetPaddingTop(intpaddingTop) {
this.paddingTop = paddingTop;
}
publicintgetPaddingBottom() {
returnpaddingBottom;
}
publicvoidsetPaddingBottom(intpaddingBottom) {
this.paddingBottom = paddingBottom;
}
publicintgetPaddingLeft() {
returnpaddingLeft;
}
publicvoidsetPaddingLeft(intpaddingLeft) {
this.paddingLeft = paddingLeft;
}
publicintgetPaddingRight() {
returnpaddingRight;
}
publicvoidsetPaddingRight(intpaddingRight) {
this.paddingRight = paddingRight;
}
publicintgetBarColor() {
returnbarColor;
}
publicvoidsetBarColor(intbarColor) {
this.barColor = barColor;
if(this.barPaint !=null) {
this.barPaint.setColor(this.barColor );
}
}
publicintgetCircleColor() {
returncircleColor;
}
publicvoidsetCircleColor(intcircleColor) {
this.circleColor = circleColor;
if(this.circlePaint !=null) {
this.circlePaint.setColor(this.circleColor);
}
}
publicintgetRimColor() {
returnrimColor;
}
publicvoidsetRimColor(intrimColor) {
this.rimColor = rimColor;
if(this.rimPaint !=null) {
this.rimPaint.setColor(this.rimColor );
}
}
publicShader getRimShader() {
returnrimPaint.getShader();
}
publicvoidsetRimShader(Shader shader) {
this.rimPaint.setShader(shader);
}
publicintgetTextColor() {
returntextColor;
}
publicvoidsetTextColor(inttextColor) {
this.textColor = textColor;
if(this.textPaint !=null) {
this.textPaint.setColor(this.textColor );
}
}
publicintgetSpinSpeed() {
returnspinSpeed;
}
publicvoidsetSpinSpeed(intspinSpeed) {
this.spinSpeed = spinSpeed;
}
publicintgetRimWidth() {
returnrimWidth;
}
publicvoidsetRimWidth(intrimWidth) {
this.rimWidth = rimWidth;
if(this.rimPaint !=null) {
this.rimPaint.setStrokeWidth(this.rimWidth );
}
}
publicintgetDelayMillis() {
returndelayMillis;
}
publicvoidsetDelayMillis(intdelayMillis) {
this.delayMillis = delayMillis;
}
publicintgetContourColor() {
returncontourColor;
}
publicvoidsetContourColor(intcontourColor) {
this.contourColor = contourColor;
if( contourPaint !=null) {
this.contourPaint.setColor(this.contourColor );
}
}
publicfloatgetContourSize() {
returnthis.contourSize;
}
publicvoidsetContourSize(floatcontourSize) {
this.contourSize = contourSize;
if( contourPaint !=null) {
this.contourPaint.setStrokeWidth(this.contourSize );
}
}
}