在5.0以前的版本上实现Material ProgressBar

英文原文:Material ProgressBar 。因为对ProgressBar的源码不熟,对于本文的具体实现也不是全懂,不过方法确实足够简单。

在写这篇文章的时候我和我Novoda的小伙伴们正在为英国的第四广播频道写一个视频点播的app。其中一个需求是实现一个标准的material风格的indeterminate ProgressBar。在Lollipop和之后的设备上这不难,但是这个app还必须支持早期的设备。本文将带你看看解决这个问题的方法。

首先看一下Lollipop上的indeterminate ProgressBar:

Untitled.gif

注: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设置成深绿色,我将得到下面的效果:

blob.png

但是,如果我们让primary progress颜色和背景颜色一致,那么secondary progress就会给你一种我们绘制了一段bar的感觉: 

blob.png 

通过分别设置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调粗一点,动画放慢一点可以看到如下效果:

Untitled.gif

回到正常大小与速度然后与标准的Lollipop indeterminate ProgressBar 相比较:

Untitled.gif

两者的效果并不完全一样 - Lollipop的实现中动画更短,看起来更快。但是这种优雅的实现方式已经很接近了。

本文的源代码在这里  。

2015, Mark Allison。保留所有权利。本文最先发表在Styling Android  。