dmytrodanylyk/circular-progress-button源码解析(二)

源码下载http://download.csdn.net/detail/kangaroo835127729/8755815

在上篇文章http://blog.csdn.net/crazy__chen/article/details/46278423中,我主要讲述了circular-progress-button状态切换的动画过程,接下来我们看一个最特殊的状态,就是加载状态,这个状态会显示一个圆环来表示当前加载的进度,但是其实circular-progress-button提供给了我们两个选择,一个是圆环弧度代表进度(例如下载文件),一个是在不知道进度(例如json数据请求)的情况下,有一个特别的旋转样式(这个样式的实现比较复杂,这篇文章主要也是为了讲它)。

上述样式,由一个属性mIndeterminateProgressMode来定义(有提供set方法)。当mIndeterminateProgressMode为false(出现进度样式),为true,出现旋转样式

下面看一下这两种样式(还不会截动图,过几天上传)

对于这两个两个样式,我们先从circular-progress-button的ondraw()方法说起

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (mProgress > 0 && mState == State.PROGRESS && !mMorphingInProgress) {
            if (mIndeterminateProgressMode) {
                drawIndeterminateProgress(canvas);
            } else {
                drawProgress(canvas);
            }
        }
    }

从ondraw()方法可以看出,当mState为mState == State.PROGRESS,也就是为加载状态时,才调用函数来进行自定义绘制(绘制圆环),否则自己绘制按钮样式就可以了。

我先来看drawProgress(canvas)方法,也就是根据进度绘圆环,不旋转

/**
     * 进度环形背景
     * @param canvas
     */
    private void drawProgress(Canvas canvas) {
        if (mProgressDrawable == null) {
            //偏移,因为要求环形在原按钮的正中间
            int offset = (getWidth() - getHeight()) / 2;
            //环形高度
            int size = getHeight() - mPaddingProgress * 2;
            mProgressDrawable = new CircularProgressDrawable(size, mStrokeWidth, mColorIndicator);
            int left = offset + mPaddingProgress;
            mProgressDrawable.setBounds(left, mPaddingProgress, left, mPaddingProgress);
        }
        float sweepAngle = (360f / mMaxProgress) * mProgress;
        mProgressDrawable.setSweepAngle(sweepAngle);
        mProgressDrawable.draw(canvas);
    }

这里关键是我们定义了一个CircularProgressDrawable,设置了它的位置,范围,和颜色等

class CircularProgressDrawable extends Drawable {

    private float mSweepAngle;
    private float mStartAngle;
    private int mSize;
    private int mStrokeWidth;
    private int mStrokeColor;

    public CircularProgressDrawable(int size, int strokeWidth, int strokeColor) {
        mSize = size;
        mStrokeWidth = strokeWidth;
        mStrokeColor = strokeColor;
        mStartAngle = -90;
        mSweepAngle = 0;
    }

    public void setSweepAngle(float sweepAngle) {
        mSweepAngle = sweepAngle;
    }

    public int getSize() {
        return mSize;
    }

    @Override
    public void draw(Canvas canvas) {
        final Rect bounds = getBounds();

        if (mPath == null) {
            mPath = new Path();
        }
        mPath.reset();
        mPath.addArc(getRect(), mStartAngle, mSweepAngle);
        mPath.offset(bounds.left, bounds.top);
        canvas.drawPath(mPath, createPaint());
    }

    @Override
    public void setAlpha(int alpha) {

    }

    @Override
    public void setColorFilter(ColorFilter cf) {
    }

    @Override
    public int getOpacity() {
        return 1;
    }

    private RectF mRectF;
    private Paint mPaint;
    private Path mPath;

    private RectF getRect() {
        if (mRectF == null) {
            int index = mStrokeWidth / 2;
            mRectF = new RectF(index, index, getSize() - index, getSize() - index);
        }
        return mRectF;
    }

    private Paint createPaint() {
        if (mPaint == null) {
            mPaint = new Paint();
            mPaint.setAntiAlias(true);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(mStrokeWidth);
            mPaint.setColor(mStrokeColor);
        }

        return mPaint;
    }
}

关键是里面的draw方法,mpath用于设置绘制圆环的路径,我们知道调用mPath.addArc()就可以设置绘制圆环,那么RectF是什么呢,看getRect()方法注意到,我们创建一个RectF,这个RectF的left是笔触的一半,为什么是一半呢?因为我沿着一半画,笔触宽度刚好被这一半平分。(如果不是一半,就画的时候就会越界,因为笔触也是有宽度的)

int index = mStrokeWidth / 2;
            mRectF = new RectF(index, index, getSize() - index, getSize() - index);

然后是,使之移动到正中间

mPath.offset(bounds.left, bounds.top);

最后,mSweepAngle觉得了扫过的弧度,而这个弧度,是根据进度process除以100*360计算出来的

mPath.addArc(getRect(), mStartAngle, mSweepAngle);

OK,就是这么简单,我们绘制出了圆弧。

接下来我们看另外一个方法drawIndeterminateProgress()

/**
     * 环形循环式背景
     * @param canvas
     */
    private void drawIndeterminateProgress(Canvas canvas) {
        if (mAnimatedDrawable == null) {
            int offset = (getWidth() - getHeight()) / 2;
            mAnimatedDrawable = new CircularAnimatedDrawable(mColorIndicator, mStrokeWidth);
            //使之在正中间
            int left = offset + mPaddingProgress;
            int right = getWidth() - offset - mPaddingProgress;
            int bottom = getHeight() - mPaddingProgress;
            int top = mPaddingProgress;
            mAnimatedDrawable.setBounds(left, top, right, bottom);
            mAnimatedDrawable.setCallback(this);
            mAnimatedDrawable.start();
        } else {
            mAnimatedDrawable.draw(canvas);
        }
    }

这个方法与上面的其实类似,只是创建的对象不同,我们这里创建了一个CircularAnimatedDrawable对象(从这里可以看出,每种不同的样式,其实就是不同的Drawable对象)

这个对象比较复杂,我们慢慢来看,首先是一些基本属性和构造方法

class CircularAnimatedDrawable extends Drawable implements Animatable {
	/**
	 * 线性时间插入器
	 */
    private static final Interpolator ANGLE_INTERPOLATOR = new LinearInterpolator();
    /**
	 * 时间插入器,先快后慢
	 */
    private static final Interpolator SWEEP_INTERPOLATOR = new DecelerateInterpolator();
    private static final int ANGLE_ANIMATOR_DURATION = 2000;
    private static final int SWEEP_ANIMATOR_DURATION = 600;
    public static final int MIN_SWEEP_ANGLE = 30;
    private final RectF fBounds = new RectF();

    private ObjectAnimator mObjectAnimatorSweep;
    private ObjectAnimator mObjectAnimatorAngle;
    /**
     * 是否改变头尾
     */
    private boolean mModeAppearing;
    private Paint mPaint;
    /**
     * 循环从0到360,每次增加2*MIN_SWEEP_ANGLE(60)
     */
    private float mCurrentGlobalAngleOffset;
    /**
     * 当前角度,线性增长
     */
    private float mCurrentGlobalAngle;
    /**
     * 当前扫过角度,先快后慢增长
     */
    private float mCurrentSweepAngle;
    /**
     * 边框宽度
     */
    private float mBorderWidth;
    /**
     * 是否正在运行动画
     */
    private boolean mRunning;

构造方法

public CircularAnimatedDrawable(int color, float borderWidth) {
        mBorderWidth = borderWidth;
        //初始化画笔
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
        //设置画笔宽度
        mPaint.setStrokeWidth(borderWidth);
        mPaint.setColor(color);

        setupAnimations();
    }

color是圆环颜色,borderwidth是笔触大小

然后是画笔初始化,接着调用setupAnimations()方法初始化动画

/**
     * 装载动画
     */
    private void setupAnimations() {
    	//角度动画
    	/**
    	 * target 	The object whose property is to be animated.被设置动画的对象
		 * property 	The property being animated. 被设置动画的属性
		 * values 	A set of values that the animation will animate between over time. 设置的值
		 * 这个构造函数说明,对CircularAnimatedDrawable类对象,设置关于mCurrentGlobalAngle属性的动画
		 * 也就是线性的给mCurrentGlobalAngle从0到360f赋值
    	 */
        mObjectAnimatorAngle = ObjectAnimator.ofFloat(this, mAngleProperty, 360f);
        //设置插入器
        mObjectAnimatorAngle.setInterpolator(ANGLE_INTERPOLATOR);
        //设置动画时长
        mObjectAnimatorAngle.setDuration(ANGLE_ANIMATOR_DURATION);
        //设置循环模式
        mObjectAnimatorAngle.setRepeatMode(ValueAnimator.RESTART);
        //设置循环次数
        mObjectAnimatorAngle.setRepeatCount(ValueAnimator.INFINITE);

        //扫过动画
        //从快到慢地给mCurrentSweepAngle从0到(360f -MIN_SWEEP_ANGLE * 2)(也就是300)赋值
        mObjectAnimatorSweep = ObjectAnimator.ofFloat(this, mSweepProperty, 360f - MIN_SWEEP_ANGLE * 2);
        mObjectAnimatorSweep.setInterpolator(SWEEP_INTERPOLATOR);
        mObjectAnimatorSweep.setDuration(SWEEP_ANIMATOR_DURATION);
        mObjectAnimatorSweep.setRepeatMode(ValueAnimator.RESTART);
        mObjectAnimatorSweep.setRepeatCount(ValueAnimator.INFINITE);
        mObjectAnimatorSweep.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {

            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            	//当尾追上头,改变头尾
                toggleAppearingMode();
            }
        });
    }

这里实质上创建了两种动画,都是ObjectAnimator动画,而ObjectAnimator继承自ValueAnimator,所以说这两个动画,都是做提供当前值的作用,本身不改变控件样式

一个是mObjectAnimatorAngle,线性增长,用于计算起始角度

一个是mObjectAnimatorSweep,先快后慢增长,用于计算扫过的角度

我们要仔细观察动画的过程(可能很快,大家在测试时,可以加大动画默认播放时间,便于观察),注意到过程是这样的,首先是快的追慢的,使圆弧不断缩小(同时整个圆弧在移动,因为快慢两头都在动),追上以后(相距MIN_SWEEP_ANGLE = 30称为追上),快的和慢的交换(也就是快的变成慢,慢的变成快),之后快的继续追慢的,使圆弧不断增大,追上以后,再次交换,重复上述过程。

设置好上面的动画以后,我们来看ondraw()方法

 @Override
    public void draw(Canvas canvas) {
        float startAngle = mCurrentGlobalAngle - mCurrentGlobalAngleOffset;
        float sweepAngle = mCurrentSweepAngle;
        if (!mModeAppearing) {
            startAngle = startAngle + sweepAngle;
            sweepAngle = 360 - sweepAngle - MIN_SWEEP_ANGLE;
        } else {
            sweepAngle += MIN_SWEEP_ANGLE;
        }

        /**
         * 绘制圆弧
         * oval :指定圆弧的外轮廓矩形区域。
         * startAngle: 圆弧起始角度,单位为度。
         * sweepAngle: 圆弧扫过的角度,顺时针方向,单位为度。
         * useCenter: 如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形。
    	 * paint: 绘制圆弧的画板属性,如颜色,是否填充等。
         */
        canvas.drawArc(fBounds, startAngle, sweepAngle, false, mPaint);
    }

首先是mModeAppearing这个属性,初始值为false,然后我们注意到,在每次mObjectAnimatorSweep增长到300度时,就会对这个值取反,为什么要这样,继续看。

我们模拟一下最开始的过程,首先这时候mModeAppearing为false,mCurrentGlobalAngleOffset为0,

那么startAngle(起始点) = mCurrentGlobalAngle维持线性增长,sweepAngle = mCurrentSweepAngle为非线性增长

然后到条件语句,会调用

startAngle = startAngle + sweepAngle;

sweepAngle = 360 - sweepAngle - MIN_SWEEP_ANGLE;

这时startAngle为线性增长和非线性增长的和,所以本质为非线性增长(先快后慢)

而我们来考虑圆弧的终止点,等于startAngle+sweepAngle=360 + startAngle- MIN_SWEEP_ANGLE(注意前后startAngle意义不同,其实就是上面两式相加)

可以看出终止点是线性增长的。

所以显然,起始点会追上终止点,所以在第一次绘制中,运动地快的那个,其实是起始点,慢的是终止点

由于两者速度差异,而我们绘制的是起始点到终止点的圆弧,自然就会逐渐缩小了。

OK,当起始点追上终止点(相距MIN_SWEEP_ANGLE = 30称为追上)时,我们要交换两者的增长方式,所以mModeAppearing被设置为true

我再来看true的时候,做了些什么

float startAngle = mCurrentGlobalAngle - mCurrentGlobalAngleOffset;

起始点为线性增长

float sweepAngle = mCurrentSweepAngle;

扫过弧度为非线性,导致终止点为非线性

else {
            sweepAngle += MIN_SWEEP_ANGLE;
        }

还要加上MIN_SWEEP_ANGLE,因为交换完之后,mCurrentSweepAngle被置零了,而我们还要保持原来相差30度的样式,所以要加上一个30度

就这样,通过起始点和终止点计算方式的反复交换,就可以生成图上的效果。

OK,circular-progress-button到这里就讲解完毕了,下面贴一段对circular-progress-button进行使用的代码,不进行过多说明,大家简单看看

public class MainActivity extends Activity {		

    private CircularProgressButton circularProgressButton;
    private CircularProgressButton circularProgressButton2;

	@Override
    public void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
    	setContentView(R.layout.main);
    	circularProgressButton = (CircularProgressButton) findViewById(R.id.mp);
    	circularProgressButton2 = (CircularProgressButton) findViewById(R.id.mp2);
    	//设置为旋转样式
    	circularProgressButton2.setIndeterminateProgressMode(true);
    	circularProgressButton2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (circularProgressButton2.getProgress() == 0) {
                	circularProgressButton2.setProgress(50);
                } else if (circularProgressButton2.getProgress() == -1) {
                	circularProgressButton2.setProgress(0);
                } else {
                	circularProgressButton2.setProgress(-1);
                }
            }
        });

    	circularProgressButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				new GetDataTask().execute();
			}
		});

    }    

	private class GetDataTask extends AsyncTask<Void, Integer, Void> {
		int i = 0;
        @Override
        protected Void doInBackground(Void... params) {
        	while(i<=100){
        		try {
    				Thread.sleep(20);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
            	publishProgress(i++);
        	}
			return null;
        }        

        @Override
        protected void onProgressUpdate(Integer... values) {
        	circularProgressButton.setProgress(values[0]);
        	super.onProgressUpdate(values);
        }

        @Override
        protected void onPostExecute(Void result) {
        	circularProgressButton.setProgress(100);
        	super.onPostExecute(result);
        }
    }

}
时间: 2024-10-26 03:51:44

dmytrodanylyk/circular-progress-button源码解析(二)的相关文章

Spring 源码解析之HandlerAdapter源码解析(二)

Spring 源码解析之HandlerAdapter源码解析(二) 前言 看这篇之前需要有Spring 源码解析之HandlerMapping源码解析(一)这篇的基础,这篇主要是把请求流程中的调用controller流程单独拿出来了 解决上篇文章遗留的问题 getHandler(processedRequest) 这个方法是如何查找到对应处理的HandlerExecutionChain和HandlerMapping的,比如说静态资源的处理和请求的处理肯定是不同的HandlerMapping ge

chenglei1986/DatePicker源码解析(二)

接上一篇文章chenglei1986/DatePicker源码解析(一),我们继续将剩余的部分讲完,其实剩余的内容,就是利用Numberpicker来组成一个datePicker,代码非常的简单 为了实现自定义布局的效果,我们给Datepciker定制了一个layout,大家可以定制自己的layout <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="h

erlang下lists模块sort(排序)方法源码解析(二)

上接erlang下lists模块sort(排序)方法源码解析(一),到目前为止,list列表已经被分割成N个列表,而且每个列表的元素是有序的(从大到小) 下面我们重点来看看mergel和rmergel模块,因为我们先前主要分析的split_1_*对应的是rmergel,我们先从rmergel查看,如下 ....................................................... split_1(X, Y, [], R, Rs) -> rmergel([[Y, X

jQuery 源码解析(二十五) DOM操作模块 html和text方法的区别

html和text都可以获取和修改DOM节点里的内容,方法如下: html(value)     ;获取匹配元素集合中的一个元素的innerHTML内容,或者设置每个元素的innerHTML内容,                ;value可选,可以是html代码或返回html代码的函数,如果没有参数则获取匹配元素集合中第一个元素的innerHTML内容 text(text)         ;获取匹配元素集合中所有元素合并后的文本内容,或者设置每个元素的文本内容,封装了createTextNo

AFNetworking2.0源码解析&lt;二&gt;

本篇我们继续来看看AFNetworking的下一个模块 — AFURLRequestSerialization. AFURLRequestSerialization用于帮助构建NSURLRequest,主要做了两个事情: 1.构建普通请求:格式化请求参数,生成HTTP Header. 2.构建multipart请求. 分别看看它在这两点具体做了什么,怎么做的. 1.构建普通请求 A.格式化请求参数 一般我们请求都会按key=value的方式带上各种参数,GET方法参数直接加在URL上,POST方

volley源码解析(二)--Request&lt;T&gt;类的介绍

在上一篇文章中,我们已经提到volley的使用方式和设计的整体思路,从这篇文章开始,我就要结合具体的源码来给大家说明volley功能的具体实现. 我们第一个要介绍的类是Request<T>这个一个抽象类,我将Request称为一个请求,通过继承Request<T>来自定义request,为volley提供了更加灵活的接口. Request<T>中的泛型T,是指解析response以后的结果.在上一篇文章中我们知道,ResponseDelivery会把response分派

Mybatis 源码解析(二) - Configuration.xml解析

文章个人学习源码所得,若存在不足或者错误之处,请大家指出. 上一章中叙述了Configuration.xml流化到Mybatis内存中的过程,那么接下来肯定就是Configuration.xml文件解析操作,在Mybatis中,这个解析的操作由SqlSesssionFactoryBuilder负责.接下来我们看看SqlSessionFactoryBuilder的方法签名: SqlSessionFactoryBuilder提供了9个签名方法,其中前8个方法都是Configuration.xml的解

第37篇 Asp.Net源码解析(二)--详解HttpApplication

这篇文章花了点时间,差点成烂到电脑里面,写的过程中有好几次修改,最终的这个版本也不是很满意,东西说的不够细,还需要认真的去看下源码才能有所体会,先这样吧,后面有时间把细节慢慢的再修改.顺便对于开发的学习,个人是觉得源码的阅读是最快的提高方式,当然阅读不是走马观花,应该多次阅读. 上次说到获得HttpApplication对象的创建,创建完成后调用InitInternal方法,这个方法任务比较多,也比较长,这里就不贴全码了,一个一个过程的去说: 初始化HttpModule 对于HttpModule

Maxwin-z/XListView-Android(下拉刷新上拉加载)源码解析(二)

转载请注明出处http://blog.csdn.net/crazy__chen/article/details/45980399 源文件下载地址http://download.csdn.net/detail/kangaroo835127729/8736887 本文主要是贴出xlistview的源代码和一个使用实例,没有过多讲解 使用实例,MainActivity public class MainActivity extends Activity { private LinkedList<Str