在5.0以前的版本上实现Material ProgressBar
英文原文:Material ProgressBar 。因为对ProgressBar的源码不熟,对于本文的具体实现也不是全懂,不过方法确实足够简单。
在写这篇文章的时候我和我Novoda的小伙伴们正在为英国的第四广播频道写一个视频点播的app。其中一个需求是实现一个标准的material风格的indeterminate ProgressBar。在Lollipop和之后的设备上这不难,但是这个app还必须支持早期的设备。本文将带你看看解决这个问题的方法。
首先看一下Lollipop上的indeterminate ProgressBar:
注:indeterminate即无限重复循环的效果,只能看到加载动画,无法看出加载进度,与之相反的是determinate。
虽然bar的样式很容易实现,但是问题在于indeterminate动画看起来比较复杂。一个短bar从左到右移动,并且这个bar在移动过程中长度是变化的。
我的第一个方法是尝试让material indeterminate进度条的实现能向后兼容。但事实证明这个方法比较难,因为它使用了在Lollipop之前设备上没有的AnimatedVectorDrawable。解决的办法比较取巧,但是得到了和预期非常接近的效果。
这个方法需要创建一个我们自己的ProgressBar(继承标准的ProgressBar),绕过了标准的indeterminate逻辑,在ProgressBar内置的 primary和secondary progress的基础上实现自己的indeterminate逻辑。技巧在于如何渲染 - 首先是background,然后是secondary progress,最后是 primary progress。如果我们让background和primary progress 的背景颜色相同,而secondary progress 为不同的颜色,我就能制造出绘制一段bar的假象。
一个例子说明这个问题。如果我们把背景设置为淡绿色,secondary progress的颜色设置成普通绿色而primary progress设置成深绿色,我将得到下面的效果:
但是,如果我们让primary progress颜色和背景颜色一致,那么secondary progress就会给你一种我们绘制了一段bar的感觉:
通过分别设置ProgressBar(标准sdk中的ProgressBar)的secondaryProgress和progress变量的值,我们可以为这段bar指定开始和结束。
让我们看看在代码中是如何实现它的:
MaterialProgressBar.java
public class MaterialProgressBar extends ProgressBar {
private static final int INDETERMINATE_MAX = 1000;
private static final String SECONDARY_PROGRESS = "secondaryProgress";
private static final String PROGRESS = "progress";
private Animator animator = null;
private final int duration;
public MaterialProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MaterialProgressBar, defStyleAttr, 0);
int backgroundColour;
int progressColour;
try {
backgroundColour = ta.getColor(R.styleable.MaterialProgressBar_backgroundColour, 0);
progressColour = ta.getColor(R.styleable.MaterialProgressBar_progressColour, 0);
int defaultDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime);
duration = ta.getInteger(R.styleable.MaterialProgressBar_duration, defaultDuration);
} finally {
ta.recycle();
}
Resources resources = context.getResources();
setProgressDrawable(resources.getDrawable(android.R.drawable.progress_horizontal));
createIndeterminateProgressDrawable(backgroundColour, progressColour);
setMax(INDETERMINATE_MAX);
super.setIndeterminate(false);
this.setIndeterminate(true);
}
private void createIndeterminateProgressDrawable(@ColorInt int backgroundColour, @ColorInt int progressColour) {
LayerDrawable layerDrawable = (LayerDrawable) getProgressDrawable();
if (layerDrawable != null) {
layerDrawable.mutate();
layerDrawable.setDrawableByLayerId(android.R.id.background, createShapeDrawable(backgroundColour));
layerDrawable.setDrawableByLayerId(android.R.id.progress, createClipDrawable(backgroundColour));
layerDrawable.setDrawableByLayerId(android.R.id.secondaryProgress, createClipDrawable(progressColour));
}
}
private Drawable createClipDrawable(@ColorInt int colour) {
ShapeDrawable shapeDrawable = createShapeDrawable(colour);
return new ClipDrawable(shapeDrawable, Gravity.START, ClipDrawable.HORIZONTAL);
}
private ShapeDrawable createShapeDrawable(@ColorInt int colour) {
ShapeDrawable shapeDrawable = new ShapeDrawable();
setColour(shapeDrawable, colour);
return shapeDrawable;
}
private void setColour(ShapeDrawable drawable, int colour) {
Paint paint = drawable.getPaint();
paint.setColor(colour);
}
.
.
.
}
这里的关键方法是createIndeterminateProgressDrawable(),它用相应的颜色替换了LayerDrawable(将被被渲染成ProgressBar的东西)中的layer。
另外值得一提的是我们在构造方法中把它写死成了 indeterminate ProgressBar - 目的只是为了保持代码的简单易懂,在产品代码中还需要额外的代码让其可以控制自己是一个标准的ProgressBar还是一个indeterminate ProgressBar。
我们现在可以绘制一段的效果了,那么如何处理它的动画呢?这比想象的简单 - 我们对ProgressBar的progress 与 secondary progress属性使用动画,但是每段的结尾采用不同的插值器(interpolator),导致段的长度在动画期间不断变化:
public class MaterialProgressBar extends ProgressBar {
.
.
.
@Override
public synchronized void setIndeterminate(boolean indeterminate) {
if (isStarted()) {
return;
}
animator = createIndeterminateAnimator();
animator.setTarget(this);
animator.start();
}
private boolean isStarted() {
return animator != null && animator.isStarted();
}
private Animator createIndeterminateAnimator() {
AnimatorSet set = new AnimatorSet();
Animator progressAnimator = getAnimator(SECONDARY_PROGRESS, new DecelerateInterpolator());
Animator secondaryProgressAnimator = getAnimator(PROGRESS, new AccelerateInterpolator());
set.playTogether(progressAnimator, secondaryProgressAnimator);
set.setDuration(duration);
return set;
}
@NonNull
private ObjectAnimator getAnimator(String propertyName, Interpolator interpolator) {
ObjectAnimator progressAnimator = ObjectAnimator.ofInt(this, propertyName, 0, INDETERMINATE_MAX);
progressAnimator.setInterpolator(interpolator);
progressAnimator.setDuration(duration);
progressAnimator.setRepeatMode(ValueAnimator.RESTART);
progressAnimator.setRepeatCount(ValueAnimator.INFINITE);
return progressAnimator;
}
}
把ProgressBar调粗一点,动画放慢一点可以看到如下效果:
回到正常大小与速度然后与标准的Lollipop indeterminate ProgressBar 相比较:
两者的效果并不完全一样 - Lollipop的实现中动画更短,看起来更快。但是这种优雅的实现方式已经很接近了。
本文的源代码在这里 。
2015, Mark Allison。保留所有权利。本文最先发表在Styling Android 。