本文介绍一下如何实现一个Google样式ProgressBar
这里有个相对简单的热热身先 Android 自定义控件——Simple_Loading
然后我们换种思路来重新实现一下
看图先:
分析:
根据前面链接中的重写方式,我们需要重写一个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,但又追不上。
这三张图就差不多表示一个周期的 初-中-结束 的状态。虽然画的有点丑。如果这样不好理解,你还可以在放在直线上理解
直线上啊从O点开始,有两个小人,在a和b路程上分别加速,和匀速交替跑,两个始终都追不上,现象就是两人的距离在一个最大值和一个最小值之间交替,就是上面园中所说的弧长。
下面看看代码是怎么实现,只贴出了关键代码,细节的地方还需要完善。
一直出于重绘状态的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