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

时间:2022-01-20 18:05:52

本文介绍一下如何实现一个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样式的ProgressBarAndroid自定义控件——手把手教你写出Google样式的ProgressBarAndroid自定义控件——手把手教你写出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