天天看点

Android自定义控件——手把手教你写出Google样式的ProgressBar

本文介绍一下如何实现一个Google样式ProgressBar

这里有个相对简单的热热身先        Android 自定义控件——Simple_Loading

然后我们换种思路来重新实现一下

看图先:

Android自定义控件——手把手教你写出Google样式的ProgressBar

分析:

        根据前面链接中的重写方式,我们需要重写一个View,然后在View中通过计算,实现一个不断旋转的圆弧,我们回过头来想想,既然系统中已经有ProgressBar,并且它本身就继承子View,我们何不直接重写ProgressBar来实现了呢?带着这个问题开始探究。

        ProgressBar继承自View,ProgressBar中显示出来的旋转的动画其实就是在画布上画的Drawable,具体的方法是这个

setIndeterminateDrawable()
           

        要想ProgressBar有动态效果,Drawable本身就是动态变化着的并一直在重绘。所以我们要做的工作就是写一个动态的Drawable,想要让Drawable动态建议实现Animatable,Animatable是一个支持动画接口。

        集体的动画怎么计算呢?我这里使用了属性动画来计算值变化的过程,以及使用的插值器来是动画有加速和减速效果。

        动画1:mRotationAnimator   0-360度不断restart方式重复,

        动画2:mSweepAppearing   20-300给弧度增加度数的动画,30-300是自己定义的变化范围,你也可以自己定义

        动画3:mSweepDisappesring   300-20 给弧度减少度数的动画,与上一个正好相反。

        三个动画执行的顺序如下:

        动画1在一直重复不断的执行,从0-360,也就是说动画1负责转圈,当动画1开始执行时,动画2也开始执行了,动画2的值加速变化到300,也就是A-B弧长加速变长的效果,动画2在执行的时候A的速度保持原有的速度,当动画2结束之后,动画3开始执行,A-B的弧长又加速变短,同时A点的度数加速。所以动画2,3负责的是弧长由长到短,由短到长交替的工作,由长到短的时候A点的值加速增大,造成B点在变短的时候被没有倒退的现象。看起来像A一直在追B,但又追不上。

Android自定义控件——手把手教你写出Google样式的ProgressBar
Android自定义控件——手把手教你写出Google样式的ProgressBar
Android自定义控件——手把手教你写出Google样式的ProgressBar

        这三张图就差不多表示一个周期的  初-中-结束  的状态。虽然画的有点丑。如果这样不好理解,你还可以在放在直线上理解

直线上啊从O点开始,有两个小人,在a和b路程上分别加速,和匀速交替跑,两个始终都追不上,现象就是两人的距离在一个最大值和一个最小值之间交替,就是上面园中所说的弧长。

Android自定义控件——手把手教你写出Google样式的ProgressBar

下面看看代码是怎么实现,只贴出了关键代码,细节的地方还需要完善。

一直出于重绘状态的Drawable  

SimpleLoadingDrawable.java

public class SimpleLoadingDrawable extends Drawable implements Animatable {

	private final String TAG = "mingwei";

	//
	public interface OnEndListener {
		public void onEnd(Drawable drawable);
	}

	private OnEndListener mOnEndListener;
	private RectF mRectF;
	private Paint mPaint;
	private int mColor;
	private float mStrokeWidth = 8;

	//
	private Interpolator mEndInterpolator = new LinearInterpolator();
	private Interpolator mRotationInterpolator = new LinearInterpolator();
	private Interpolator mSweepInterpolator = new DecelerateInterpolator();
	//
	private boolean isRunning;
	private ValueAnimator mSweepAppearingAnimator;
	private ValueAnimator mSweepDisAppearingAnimator;
	private ValueAnimator mRotationAnimator;
	private ValueAnimator mEndAnimator;
	//

	private float mCurrentRotationAngle = 0;
	private float mCurrentSweepAngle;
	private float mCurrentRotationAngleOffset = 0;
	private float mCurrentEndRation = 1f;
	//
	private float mRotatonSpeed = 0.5f;
	private int mSweepAngleMin = 20;
	private int mSweepAngleMax = 300;
	//
	private int mRotationDuration = 2000;
	private int mSweepDuration = 600;
	private int mEndDuration = 200;
	//
	private boolean mFirstSweepAnimator;
	private boolean mModeAppearing;

	//
	public SimpleLoadingDrawable() {
		Log.i(TAG, "SimpleLoadingDrawable()");
		this.mPaint = new Paint();
		this.mPaint.setAntiAlias(true);
		this.mPaint.setStrokeWidth(mStrokeWidth);
		this.mPaint.setStyle(Paint.Style.STROKE);
		this.mPaint.setStrokeCap(Cap.ROUND);
		this.mColor = Color.RED;
		this.mPaint.setColor(mColor);
		startDeceAnimation();
	}

	private void reinitValues() {
		mFirstSweepAnimator = true;
		mCurrentEndRation = 1f;
		// mPaint.setColor(mColor);
	}

	private void setAppearing() {
		mModeAppearing = true;
		mCurrentRotationAngleOffset += mSweepAngleMin;

	}

	private void setDisAppearing() {
		mModeAppearing = false;
		mCurrentRotationAngleOffset = mCurrentRotationAngleOffset + (360 - mSweepAngleMax);
	}

	@Override
	public void draw(Canvas canvas) {
		float startAngle = mCurrentRotationAngle - mCurrentRotationAngleOffset;
		float sweepAngle = mCurrentSweepAngle;
		if (!mModeAppearing) {
			startAngle = startAngle + (360 - sweepAngle);
		}
		startAngle %= 360;
		if (mCurrentEndRation < 1f) {
			float newSweepAngle = sweepAngle * mCurrentEndRation;
			startAngle = (startAngle + (sweepAngle - newSweepAngle)) % 360;
			sweepAngle = newSweepAngle;
		}
		canvas.drawArc(mRectF, startAngle, sweepAngle, false, mPaint);

	}

	private void startDeceAnimation() {
		mRotationAnimator = ValueAnimator.ofFloat(0f, 360f);
		mRotationAnimator.setDuration((long) (mRotationDuration / mRotatonSpeed));
		mRotationAnimator.setInterpolator(mRotationInterpolator);
		mRotationAnimator.setRepeatCount(ValueAnimator.INFINITE);
		mRotationAnimator.setRepeatMode(ValueAnimator.RESTART);
		mRotationAnimator.addUpdateListener(new AnimatorUpdateListener() {
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				// float rotation = getAnimatedFraction(animation) * 360f;
				setCurrentRotationAngle((Float) animation.getAnimatedValue());
			}
		});
		//
		mSweepAppearingAnimator = ValueAnimator.ofFloat(mSweepAngleMin, mSweepAngleMax);
		mSweepAppearingAnimator.setDuration(mSweepDuration);
		mSweepAppearingAnimator.setInterpolator(mSweepInterpolator);
		mSweepAppearingAnimator.addUpdateListener(new AnimatorUpdateListener() {
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				float anitationFraction = getAnimatedFraction(animation);
				float angle;
				if (mFirstSweepAnimator) {
					angle = anitationFraction * mSweepAngleMax;
				} else {
					angle = mSweepAngleMin + anitationFraction * (mSweepAngleMax - mSweepAngleMin);
				}
				setCurrentSweepAngle(angle);
			}
		});
		mSweepAppearingAnimator.addListener(new AnimatorListener() {
			boolean cancel = false;

			@Override
			public void onAnimationStart(Animator animation) {
				cancel = false;
				mModeAppearing = true;
			}

			@Override
			public void onAnimationRepeat(Animator animation) {

			}

			@Override
			public void onAnimationEnd(Animator animation) {
				if (!cancel) {
					mFirstSweepAnimator = false;
					setDisAppearing();
					mSweepDisAppearingAnimator.start();
				}
			}

			@Override
			public void onAnimationCancel(Animator animation) {
				cancel = true;
			}
		});
		//
		mSweepDisAppearingAnimator = ValueAnimator.ofFloat(mSweepAngleMax, mSweepAngleMin);
		mSweepDisAppearingAnimator.setInterpolator(mSweepInterpolator);
		mSweepDisAppearingAnimator.setDuration(mSweepDuration);
		mSweepDisAppearingAnimator.addUpdateListener(new AnimatorUpdateListener() {
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				float floatFraction = getAnimatedFraction(animation);
				setCurrentSweepAngle(mSweepAngleMax - floatFraction * (mSweepAngleMax - mSweepAngleMin));
				long duration = animation.getDuration();
				long currentTime = animation.getCurrentPlayTime();
				float fraction = currentTime / duration;

			}
		});
		mSweepDisAppearingAnimator.addListener(new AnimatorListener() {
			boolean cancel;

			@Override
			public void onAnimationStart(Animator animation) {
				cancel = false;
			}

			@Override
			public void onAnimationRepeat(Animator animation) {

			}

			@Override
			public void onAnimationEnd(Animator animation) {
				if (!cancel) {
					setAppearing();
					mSweepAppearingAnimator.start();
				}
			}

			@Override
			public void onAnimationCancel(Animator animation) {
				cancel = true;
			}
		});
		//
		mEndAnimator = ValueAnimator.ofFloat(1f, 0f);
		mEndAnimator.setInterpolator(mEndInterpolator);
		mEndAnimator.setDuration(mEndDuration);
		mEndAnimator.addUpdateListener(new AnimatorUpdateListener() {
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				float endRation = getAnimatedFraction(animation);
				initEndRation(1.0f - endRation);
			}

		});
		mEndAnimator.addListener(new AnimatorListener() {
			boolean cancel;

			@Override
			public void onAnimationStart(Animator animation) {
				cancel = false;
			}

			@Override
			public void onAnimationRepeat(Animator animation) {

			}

			@Override
			public void onAnimationEnd(Animator animation) {
				initEndRation(0f);
				if (!cancel) {
					stop();
				}
			}

			@Override
			public void onAnimationCancel(Animator animation) {
				cancel = false;
			}
		});
	}

	@Override
	public void start() {
		if (isRunning()) {
			return;
		}
		isRunning = true;
		reinitValues();
		mRotationAnimator.start();
		mSweepAppearingAnimator.start();
		invalidateSelf();

	}

	@Override
	public void stop() {
		if (!isRunning()) {
			return;
		}
		isRunning = false;
		stopAnimators();
		invalidateSelf();
	}

	private void stopAnimators() {
		mRotationAnimator.cancel();
		mSweepAppearingAnimator.cancel();
		mSweepDisAppearingAnimator.cancel();
		mEndAnimator.cancel();
	}

	@Override
	public void setBounds(int left, int top, int right, int bottom) {
		super.setBounds(left, top, right, bottom);
		mRectF = new RectF(left + mStrokeWidth / 2f + 0.5f, top + mStrokeWidth / 2f + 0.5f, right - mStrokeWidth / 2f - 0.5f,
				bottom - mStrokeWidth / 2f - 0.5f);
	}

	protected void setCurrentRotationAngle(float rotationAngle) {
		mCurrentRotationAngle = rotationAngle;
		invalidateSelf();
	}

	protected void setCurrentSweepAngle(float sweepAngle) {
		mCurrentSweepAngle = sweepAngle;
		invalidateSelf();
	}

	private void initEndRation(float f) {
		mCurrentEndRation = f;
		invalidateSelf();
	}

	@Override
	public void setAlpha(int alpha) {
		mPaint.setAlpha(alpha);
	}

	@Override
	public void setColorFilter(ColorFilter cf) {
		mPaint.setColorFilter(cf);
	}

	@Override
	public int getOpacity() {
		return PixelFormat.TRANSLUCENT;
	}

	public void progressiveStop() {
		progressiveStop(null);
	}

	private void progressiveStop(OnEndListener listener) {
		if (!isRunning() || mEndAnimator.isRunning()) {
			return;
		}
		mOnEndListener = listener;
		mEndAnimator.addListener(new AnimatorListener() {

			@Override
			public void onAnimationStart(Animator animation) {

			}

			@Override
			public void onAnimationRepeat(Animator animation) {

			}

			@Override
			public void onAnimationEnd(Animator animation) {
				mEndAnimator.removeListener(this);
				if (mOnEndListener != null) {
					mOnEndListener.onEnd(SimpleLoadingDrawable.this);
				}
			}

			@Override
			public void onAnimationCancel(Animator animation) {

			}
		});
		mEndAnimator.start();
	}

	@Override
	public boolean isRunning() {
		return isRunning;
	}

	public static class Build {
		public Build() {

		}

		public SimpleLoadingDrawable builder() {
			return new SimpleLoadingDrawable();
		}
	}
}
           

重写的ProgressBar,别忘记给加上一个ProgressBar的样式,否则没有绘制效果

SimpleLoading.java

public class SimpleLoading extends ProgressBar {

	public SimpleLoading(Context context) {
		this(context, null);
	}

	public SimpleLoading(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public SimpleLoading(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		init(attrs);
	}

	private void init(AttributeSet attrs) {
		if (isInEditMode()) {
			setIndeterminateDrawable(new SimpleLoadingDrawable.Build().builder());
		}
		setIndeterminateDrawable(new SimpleLoadingDrawable.Build().builder());
	}

}
           

样式

<resources>

    <style name="LoadingBarStyle" parent="android:Widget.Holo.ProgressBar"></style>

</resources>
           

属性

<resources>

    <declare-styleable name="Loading">
        <attr name="style" format="reference" />
        <attr name="color" format="color" />
        <attr name="colors" format="reference" />
        <attr name="stroke_width" format="dimension" />
        <attr name="min_sweep_angle" format="integer" />
        <attr name="max_sweep_angle" format="integer" />
        <attr name="sweep_speed" format="float" />
        <attr name="rotation_speed" format="float" />
    </declare-styleable>

</resources>
           

在布局文件中使用,别忘记加样式style

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.mingwei.sampleloading2.MainActivity" >

    <com.mingwei.sampleloading2.SimpleLoading
        style="@style/LoadingBarStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>
           

Activity中啥也没干就不贴出来了。

GitHub地址:https://github.com/Mingwei360/RotatonProgressBar