本文介绍一下如何实现一个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(); } } }
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>
<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