(4.2.23)又一个material design风格的progressbar

这里写图片描述

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>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值