FEATURES
inderteminate and determinate mode supported.
many attributes supported.
USAGE
just like ProgreeBar in android SDK
xmlns:app="http://schemas.android.com/apk/res-auto"
<com.taobao.library.MaterialProgressBar
app:bar_color="#ff0000"
app:rim_color="#33ff0000"
app:bar_rimshown="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
tips:
you can use layout_width/layout_height to control progressbar's width.
only in determinate mode that you can call setProgress().change mode use setMode() function.
ATTRIBUTES
<declare-styleable name="MaterialProgressBar">
<attr name="bar_color" format="color"/>
<attr name="bar_mode" format="enum">
<enum name="INDETERMINATE" value="0"/>
<enum name="DETERMINATE" value="1"/>
</attr>
<attr name="bar_width" format="dimension"/>
<attr name="bar_progress" format="float"/>
<attr name="rim_width" format="dimension"/>
<attr name="rim_color" format="color"/>
<attr name="bar_rimshown" format="boolean"/>
</declare-styleable>
more samples
<com.taobao.library.MaterialProgressBar
android:layout_marginLeft="10dp"
android:layout_width="60dp"
android:layout_height="70dp" />
<com.taobao.library.MaterialProgressBar
android:layout_width="70dp"
android:layout_height="70dp"
app:bar_rimshown="true"
app:bar_width="5dp"
app:rim_width="6dp"/>
<com.taobao.library.MaterialProgressBar
android:id="@+id/pb"
app:bar_mode="DETERMINATE"
app:bar_progress="0.6"
android:layout_centerHorizontal="true"
android:layout_below="@id/container"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
分析下难点:
1. 动画的实现;
2. 边界的控制;
3. 状态保存与恢复;
4. 两种状态的实现,loading状态(不停旋转)、progress状态。
分别来看下。
1. 动画如何实现:
将动画进行拆解,可以发现它其实是一个弧不断变长变短的一个过程+弧本身在绕圆形转动两部分组成。
所以可以分开来处理,弧度变长变短可以通过canvas.drawArc的参数startAngle/SweeepAngle控制,只要改变这两个值即可实现效果。怎么改变?有几种方案,1是通过hander+thread;2是通过View.post();3是通过PropertyAnimation.
弧本身绕圆心运动可以通过Canvas.rotate实现。
privatestaticfinal float delta = 6f;
private float temp = 0;
classAnimRunnableimplementsRunnable{
@Override
publicvoid run() {
if (mStartAngle == temp) {
mSweepAngle += delta;
}
if (mSweepAngle >= 280 || mStartAngle > temp) {
mStartAngle += delta;
if(mSweepAngle > 20) {
mSweepAngle -= delta;
}
}
if (mStartAngle > temp + 280) {
temp = mStartAngle;
mStartAngle = temp;
mSweepAngle = 20;
}
postInvalidate();
postDelayed(this,mSpinSpeed);
}
}
2.边界的控制:
需要在onMeasure中控制。在onSizeChanged方法中可以拿到最终的width、height,通过width/height就可以控制progressbar的边界了。
需要注意的是,边界需要是正方形的,所以得考虑宽高不相等的情况以及四个方向padding的大小。
@OverrideprotectedvoidonMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//计算自己需要的宽度和高度int width = mCircleRadius*2;
int height = mCircleRadius*2;
//考虑父容器的测量规则
setMeasuredDimension(getResolvedSize(width, widthMeasureSpec), getResolvedSize(height, heightMeasureSpec));
}
@OverrideprotectedvoidonSizeChanged(int w, int h, int oldw, int oldh) {
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
//简化处理,以最大的padding作为paddingint padding = Math.max(Math.max(paddingLeft, paddingRight), Math.max(paddingTop, paddingBottom));
int diameter;
//保证bounds是一个正方形if(w >= h){
diameter = h;
mBounds = new RectF(padding+mBarWidth+(w-h)/2,padding+mBarWidth,diameter-padding-mBarWidth+(w-h)/2,diameter-padding-mBarWidth);
}elseif(w < h){
diameter = w;
mBounds = new RectF(padding+mBarWidth,padding+mBarWidth+(h-w)/2,diameter-padding-mBarWidth,diameter-padding-mBarWidth+(h-w)/2);
}
}
3.状态的保存与恢复:
progressbar的状态不能因为横竖屏切换等问题丢失,所以需要通过重写onSaveInstanceState/onRestoreInstanceState来保存/恢复状态.
@Override
protected void onRestoreInstanceState(Parcelable state) {
if(! (state instanceof SavedState)){
super.onRestoreInstanceState(state);
return;
}
//先恢复父类的状态
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
//在恢复自己的状态
this.mCurMode = savedState.mCurMode == 0 ? Mode.INDETERMINATE : Mode.DETERMINATE;
this.mRimWidth = savedState.mRimWidth;
this.mRimColor = savedState.mRimColor;
this.mBarColor = savedState.mBarColor;
this.mBarWidth = savedState.mBarWidth;
this.showRim = savedState.showRim;
this.isAnimStart = savedState.isAnimStart;
this.mProgress = savedState.mProgress;
}
@Override
protected Parcelable onSaveInstanceState() {
//相当于是做了一层包装
//先保存父类的状态,然后包装,再保存自己的状态
Parcelable parcelable = super.onSaveInstanceState();
SavedState savedState = new SavedState(parcelable);
savedState.mCurMode = (this.mCurMode == Mode.INDETERMINATE) ? 0 : 1;
savedState.mRimWidth = this.mRimWidth;
savedState.mRimColor = this.mRimColor;
savedState.mBarColor = this.mBarColor;
savedState.mBarWidth = this.mBarWidth;
savedState.showRim = this.showRim;
savedState.isAnimStart = this.isAnimStart;
savedState.mProgress = this.mProgress;
return savedState;
}
4.两种状态的实现:
自然是通过一个变量记录当前模式,在onDraw中通过判断模式进行不同的绘制操作。
package com.taobao.library;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
/**
* Created by rowandjj on 15/8/6.
* <p>
* <p>
* 一个material效果的progressbar
* 支持indeterminate与determinate两种模式<br>
* <p>
* 通过setMode()指定模式<br/>
* <p>
* <p>
* 注:<br>
* 调整progressbar大小直接通过layout_width/layout_height指定即可
* wrap_content代表默认大小<br>
* 在indeterminate模式下设置progress无效,必须先更改模式
*/
public class MaterialProgressBar extends View {
/**
* 轮子宽度 注意不是半径
*/
private int mBarWidth = (int) dp2px(3);
/**
* 轮子颜色
*/
private int mBarColor = 0xFF5588FF;
/**
* 轮子半径
*/
private int mCircleRadius = (int) dp2px(25);
/**
* 转速
*/
private int mSpinSpeed = 2;
/**
* 当前进度 0~1.0f
*/
private float mProgress = 0.0f;
private Mode mCurMode = Mode.INDETERMINATE;
public enum Mode {
INDETERMINATE/*转动模式*/, DETERMINATE/*普通模式,可以设置progress*/
}
private RectF mBounds;
//主画笔
private Paint mPaint;
private float mStartAngle = 0;
private float mRotateAngle;
private float mSweepAngle = 0;
//通过线程不断更新样式,也就是进度条的位置
private Runnable mAnimRunnable;
private boolean isAnimStart = false;
private boolean showRim = false;
//底盘画笔
private Paint mRimPaint;
private int mRimColor = 0xEEC0C0C0;
private int mRimWidth = (int) dp2px(3);
/* 将动画进行拆解,可以发现它其实是一个弧不断变长变短的一个过程+弧本身在绕圆形转动两部分组成。所以可以分开来处理.
弧度变长变短可以通过canvas.drawArc的参数startAngle/SweeepAngle控制,只要改变这两个值即可实现效果。
怎么改变?有几种方案,1是通过hander+thread;2是通过View.post();3是通过PropertyAnimation.
弧本身绕圆心运动可以通过Canvas.rotate实现。*/
private static final float delta = 6f;
private float temp = 0;
class AnimRunnable implements Runnable {
@Override
public void run() {
if (mStartAngle == temp) {
mSweepAngle += delta;
}
if (mSweepAngle >= 280 || mStartAngle > temp) {
mStartAngle += delta;
if (mSweepAngle > 20) {
mSweepAngle -= delta;
}
}
if (mStartAngle > temp + 280) {
temp = mStartAngle;
mStartAngle = temp;
mSweepAngle = 20;
}
//android中实现view的更新有两组方法,一组是invalidate,另一组是postInvalidate,其中前者是在UI线程自身中使用,而后者在非UI线程中使用。
postInvalidate();
//每个2ms,调用下自身。 自己不断调用自己,实现进度条的不断更新
postDelayed(this, mSpinSpeed);
}
}
//没有属性限制
public MaterialProgressBar(Context context) {
super(context);
init();
}
//该view在xml中自定义了属性
public MaterialProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
parseAttributes(context, attrs);
init();
}
private void parseAttributes(Context context, AttributeSet attrs) {
//在 Android 自定义 View 的时候,需要使用 TypedArray 来获取 XML layout 中的属性值,使用完之后,需要调用 recyle() 方法将 TypedArray 回收
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MaterialProgressBar);
//自定义的当前进度
mProgress = array.getFloat(R.styleable.MaterialProgressBar_bar_progress, 0.0f);
//自定义的轮子颜色
mBarColor = array.getColor(R.styleable.MaterialProgressBar_bar_color, mBarColor);
//自定义的轮子宽度 注意不是半径
mBarWidth = array.getDimensionPixelSize(R.styleable.MaterialProgressBar_bar_width, mBarWidth);
//自定义的模式
int mode = array.getInt(R.styleable.MaterialProgressBar_bar_mode, 0);
switch (mode) {
case 0:
mCurMode = Mode.INDETERMINATE;
break;
case 1:
mCurMode = Mode.DETERMINATE;
break;
}
//底盘的颜色
mRimColor = array.getColor(R.styleable.MaterialProgressBar_rim_color, mRimColor);
//底盘的宽度 注意不是半径
mRimWidth = array.getDimensionPixelSize(R.styleable.MaterialProgressBar_rim_width, mRimWidth);
//是否显示底盘
showRim = array.getBoolean(R.styleable.MaterialProgressBar_bar_rimshown, showRim);
//单例模式对象,从池中获取,此时回收对象
array.recycle();
}
private void init() {
mPaint = new Paint();
//抗锯齿功能
mPaint.setAntiAlias(true);
//防抖动
mPaint.setDither(true);
// 设置填充样式 FILL填充内部 FILL_AND_STROKE填充内部和描边 STROKE仅描边
mPaint.setStyle(Paint.Style.STROKE);
//设置画笔宽度
mPaint.setStrokeWidth(mBarWidth);
//画笔颜色
mPaint.setColor(mBarColor);
mRimPaint = new Paint();
mRimPaint.setAntiAlias(true);
mRimPaint.setStrokeWidth(mRimWidth);
mRimPaint.setColor(mRimColor);
mRimPaint.setDither(true);
mRimPaint.setStyle(Paint.Style.STROKE);
if (mCurMode == Mode.INDETERMINATE) {
startAnim();
isAnimStart = true;
}
}
private void startAnim() {
if (mAnimRunnable == null)
mAnimRunnable = new AnimRunnable();//创建自定义的线程对象
post(mAnimRunnable);//提交线程到线线程池
}
//自带函数中,第一个被执行的 当view的大小发生变化时触发
//需要在onMeasure中控制。在onSizeChanged方法中可以拿到最终的width、height,通过width/height就可以控制progressbar的边界了。
//需要注意的是,边界需要是正方形的,所以得考虑宽高不相等的情况以及四个方向padding的大小
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
//系统显示控件时,布局会自动的加入一定或自定义的padding,需要在重绘制时加入
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
//简化处理,以最大的padding作为padding
int padding = Math.max(Math.max(paddingLeft, paddingRight), Math.max(paddingTop, paddingBottom));
int diameter;
//保证bounds是一个正方形
if (w >= h) {
diameter = h;
//四点绘制一个正方形 距离左 上 右 下边距的距离
mBounds = new RectF(padding + mBarWidth + (w - h) / 2, padding + mBarWidth, diameter - padding - mBarWidth + (w - h) / 2, diameter - padding - mBarWidth);
} else if (w < h) {
diameter = w;
mBounds = new RectF(padding + mBarWidth, padding + mBarWidth + (h - w) / 2, diameter - padding - mBarWidth, diameter - padding - mBarWidth + (h - w) / 2);
}
}
//自带函数中,第2个执行的 确定所有子元素的大小
//widthMeasureSpec为控件元素的实际宽和高
//该处主要应对android:layout_width="wrap_content"的属性,不能让控件全屏,需要自己设定空间的宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//计算自己需要的宽度和高度,自己说需要的控件宽度
int width = mCircleRadius * 2;
int height = mCircleRadius * 2;
//考虑父容器的测量规则
setMeasuredDimension(getResolvedSize(width, widthMeasureSpec), getResolvedSize(height, heightMeasureSpec));
}
//自带函数中,第三个被执行的 view渲染内容的细节
@Override
protected void onDraw(Canvas canvas) {
mPaint.setStrokeWidth(mBarWidth);
mPaint.setColor(mBarColor);
mRimPaint.setStrokeWidth(mRimWidth);
mRimPaint.setColor(mRimColor);
if (mCurMode == Mode.INDETERMINATE) {//不确定的,进度条一直转,无所谓数值
canvas.save();//保存画布
if (isInEditMode()) {//使用isInEditMode解决可视化编辑器无法识别自定义控件的问题
mStartAngle = 0;
mSweepAngle = 270;
}
//显示底层
if (showRim) {
//mBounds在之前的sizechanged中实例过了
//以矩形的中心为圆心,绘制底层圆形
canvas.drawCircle(mBounds.centerX(), mBounds.centerY(), mBounds.width() / 2, mRimPaint);
}
//控制自身旋转,动画2,也就是弧开始位置的变化
canvas.rotate(mRotateAngle += 4, mBounds.centerX(), mBounds.centerY());
//根据bounds画弧,动画1,弧的长短
canvas.drawArc(mBounds, mStartAngle, mSweepAngle, false, mPaint);
canvas.restore();
if (!isAnimStart) {
isAnimStart = true;
startAnim();
}
} else {
if (isInEditMode()) {
mProgress = 0.6f;
}
isAnimStart = false;
//clear动画
getHandler().removeCallbacks(mAnimRunnable);
//draw progress
canvas.rotate(-90, mBounds.centerX(), mBounds.centerY());
if (showRim) {
canvas.drawCircle(mBounds.centerX(), mBounds.centerY(), mBounds.width() / 2, mRimPaint);
}
canvas.drawArc(mBounds, 0, mProgress * 360, false, mPaint);
}
}
public void setProgress(float progress) {
if (progress > 1.0f || progress < 0f) {
return;
}
if (this.mCurMode == Mode.INDETERMINATE) {
return;
}
this.mProgress = progress;
postInvalidate();
}
public float getProgress() {
return this.mProgress;
}
public void setMode(Mode mode) {
this.mCurMode = mode;
postInvalidate();
}
public Mode getMode() {
return this.mCurMode;
}
public int getBarWidth() {
return mBarWidth;
}
public void setBarWidth(int mBarWidth) {
this.mBarWidth = mBarWidth;
postInvalidate();
}
public int getBarColor() {
return mBarColor;
}
public void setBarColor(int mBarColor) {
this.mBarColor = mBarColor;
postInvalidate();
}
public int getRimColor() {
return mRimColor;
}
public void setRimColor(int mRimColor) {
this.mRimColor = mRimColor;
postInvalidate();
}
public int getRimWidth() {
return mRimWidth;
}
public void setRimWidth(int mRimWidth) {
this.mRimWidth = mRimWidth;
postInvalidate();
}
public boolean isShowRim() {
return showRim;
}
public void setShowRim(boolean showRim) {
this.showRim = showRim;
postInvalidate();
}
//更新控件大小,以满足android:layout_width="wrap_content"属性的配置
private int getResolvedSize(int desiredSize, int measureSpec) {
int mode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
int size;
if (mode == MeasureSpec.EXACTLY) {//如果在布局文件中,设定了固定的值,则按照固定的值,如android:layout_width="100dip"
size = specSize;
} else {//否则。就按照代码中,默认的控件值
size = desiredSize;
if (mode == MeasureSpec.AT_MOST) {//表示子布局限制在一个最大值内,一般为WARP_CONTENT
size = Math.min(desiredSize, specSize);//取较小值为最大值
}
}
return size;
}
private float dp2px(int dip) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, getResources().getDisplayMetrics());
}
//--- 状态保存与恢复 -----
// progressbar的状态不能因为横竖屏切换等问题丢失,所以需要通过重写onSaveInstanceState/onRestoreInstanceState来保存/恢复状态.
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
//先恢复父类的状态
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
//在恢复自己的状态
this.mCurMode = savedState.mCurMode == 0 ? Mode.INDETERMINATE : Mode.DETERMINATE;
this.mRimWidth = savedState.mRimWidth;
this.mRimColor = savedState.mRimColor;
this.mBarColor = savedState.mBarColor;
this.mBarWidth = savedState.mBarWidth;
this.showRim = savedState.showRim;
this.isAnimStart = savedState.isAnimStart;
this.mProgress = savedState.mProgress;
}
@Override
protected Parcelable onSaveInstanceState() {
//相当于是做了一层包装
//先保存父类的状态,然后包装,再保存自己的状态
Parcelable parcelable = super.onSaveInstanceState();
SavedState savedState = new SavedState(parcelable);
savedState.mCurMode = (this.mCurMode == Mode.INDETERMINATE) ? 0 : 1;
savedState.mRimWidth = this.mRimWidth;
savedState.mRimColor = this.mRimColor;
savedState.mBarColor = this.mBarColor;
savedState.mBarWidth = this.mBarWidth;
savedState.showRim = this.showRim;
savedState.isAnimStart = this.isAnimStart;
savedState.mProgress = this.mProgress;
return savedState;
}
static class SavedState extends BaseSavedState {
float mProgress;
int mBarWidth;
int mBarColor;
int mRimColor;
int mRimWidth;
boolean showRim;
boolean isAnimStart;
int mCurMode;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel source) {
super(source);
this.mProgress = source.readFloat();
this.mBarWidth = source.readInt();
this.mBarColor = source.readInt();
this.mRimColor = source.readInt();
this.mRimWidth = source.readInt();
this.showRim = source.readByte() != 0;
this.isAnimStart = source.readByte() != 0;
this.mCurMode = source.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeFloat(this.mProgress);
dest.writeInt(this.mBarWidth);
dest.writeInt(this.mBarColor);
dest.writeInt(this.mRimColor);
dest.writeInt(this.mRimWidth);
dest.writeByte((byte) (this.showRim ? 1 : 0));
dest.writeByte((byte) (this.isAnimStart ? 1 : 0));
dest.writeInt(this.mCurMode);
}
//required field that makes Parcelables from a Parcel
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
//onDetachedFromWindow在退出视频播放,销毁资源(既销毁view)之后调用。
//一般用于注销广播或线程对象
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//一定要removecallback
if (mAnimRunnable != null)
removeCallbacks(mAnimRunnable);//Handler的使用看来不熟悉啊!将一个线程加入到Handler队列,会被Looper监测到,并调用执行线程,removeCallbacks(run)后,只是把run对象的引用从队列里拿出来,这样,他就不会执行了,但 run 没有销毁,当然还在在拉
}
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MaterialProgressBar">
<attr name="bar_color" format="color"/>
<attr name="bar_mode" format="enum">
<enum name="INDETERMINATE" value="0"/>
<enum name="DETERMINATE" value="1"/>
</attr>
<attr name="bar_width" format="dimension"/>
<attr name="bar_progress" format="float"/>
<attr name="rim_width" format="dimension"/>
<attr name="rim_color" format="color"/>
<attr name="bar_rimshown" format="boolean"/>
</declare-styleable>
</resources>