Animation / Drawable / Material Design / ProgressBar

Indeterminate – Part 4

Indeterminate ProgressBars are a useful tool for communicating to our users that an operation is in progress when we cannot predict how long it is likely to take. Previously on Styling Android we’ve covered how to create a backwardly compatible approximation of the material styled horizontal indeterminate ProgressBar but we haven’t looked at the circular form – in this series we’ll create an approximation of the material circular indeterminate ProgressBar which will be backwardly compatible to API 11 (Honeycomb).

In the previous article we created a custom Drawable which is capable of drawing an arc and has setters to control the start and end points of the arc and the overall rotation. The next logical set is to create a set object Animators which will change these values over time and therefore create the animations that we require.

Before we look at the code I should point out that I’m not a big lover of utility classes which only contain static methods – they do not represent actual objects so do not have a clear role in pure Object Oriented code. However I have used one in this case purely to separate the Animator construction away from the Drawable itself to compartmentalise things and (hopefully!) make the code easier to follow.

final class IndeterminateAnimatorFactory {
    private static final String START_ANGLE = "startAngle";
    private static final String END_ANGLE = "endAngle";
    private static final String ROTATION = "rotation";

    private static final int SWEEP_DURATION = 1333;
    private static final int ROTATION_DURATION = 6665;
    private static final float END_ANGLE_MAX = 360;
    private static final float START_ANGLE_MAX = END_ANGLE_MAX - 1;
    private static final int ROTATION_END_ANGLE = 719;

    private IndeterminateAnimatorFactory() {
        // NO-OP
    }

    public static Animator createIndeterminateDrawableAnimator(IndeterminateDrawable drawable) {
        AnimatorSet animatorSet = new AnimatorSet();
        Animator startAngleAnimator = createStartAngleAnimator(drawable);
        Animator sweepAngleAnimator = createSweepAngleAnimator(drawable);
        Animator rotationAnimator = createAnimationAnimator(drawable);
        animatorSet.playTogether(startAngleAnimator, sweepAngleAnimator, rotationAnimator);
        return animatorSet;
    }
    .
    .
    .
}

This is simple enough, we’re creating three different Animators and adding them to an AnimatorSet to play them together.

final class IndeterminateAnimatorFactory {
    .
    .
    .
    private static Animator createStartAngleAnimator(IndeterminateDrawable drawable) {
        ObjectAnimator animator = ObjectAnimator.ofFloat(drawable, START_ANGLE, 0f, START_ANGLE_MAX);
        animator.setDuration(SWEEP_DURATION);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setRepeatMode(ValueAnimator.RESTART);
        animator.setInterpolator(createStartInterpolator());
        return animator;
    }

    private static Interpolator createStartInterpolator() {
        return new LinearInterpolator();
    }

    private static Animator createSweepAngleAnimator(IndeterminateDrawable drawable) {
        ObjectAnimator animator = ObjectAnimator.ofFloat(drawable, END_ANGLE, 0f, END_ANGLE_MAX);
        animator.setDuration(SWEEP_DURATION);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setRepeatMode(ValueAnimator.RESTART);
        animator.setInterpolator(createEndInterpolator());
        return animator;
    }

    private static Interpolator createEndInterpolator() {
        return new LinearInterpolator();
    }
    .
    .
    .
}

These are the two Animators which will control the start and end points of the arc. The value of the SWEEP_DURATION constant has been copied directly from the AOSP code that we looked at to try and match it as closely as possible. The methods to create the Interpolators are placeholders for the moment – we’ll do something far more interesting with them in a moment.

Next we have the rotation animator:

final class IndeterminateAnimatorFactory {
    .
    .
    .
    private static Animator createAnimationAnimator(IndeterminateDrawable drawable) {
        ObjectAnimator rotateAnimator = ObjectAnimator.ofFloat(drawable, ROTATION, 0, ROTATION_END_ANGLE);
        rotateAnimator.setDuration(ROTATION_DURATION);
        rotateAnimator.setRepeatMode(ValueAnimator.RESTART);
        rotateAnimator.setRepeatCount(ValueAnimator.INFINITE);
        rotateAnimator.setInterpolator(new LinearInterpolator());
        return rotateAnimator;
    }
    .
    .
    .
}

Once again, the value of ROTATION_DURATION is copied from the AOSP implementation. In this case we’re going to stick with a LinearInterpolator because we want a uniform rotation.

We now need to update the createAnimator() method in IndeterminateDrawable:

public class IndeterminateDrawable extends Drawable implements Animatable {
    .
    .
    .
    private void createAnimator() {
        animator = IndeterminateAnimatorFactory.createIndeterminateDrawableAnimator(this);
    }
    .
    .
    .
}

If we run this we get a short, rotating line:

The start and end points are moving together because of they are both using identical LinearInterpolators. The reason for the short segment being drawn despite the start and end values being the same is the square caps that we set on our Paint object in the Drawable – this will case the ends to be rendered slightly beyond the length of the arc being drawn.

We can improve this by using different interpolators for the start and end animators:

final class IndeterminateAnimatorFactory {
    .
    .
    .
    private static Interpolator createStartInterpolator() {
        return new DecelerateInterpolator();
    }
    .
    .
    .
    private static Interpolator createEndInterpolator() {
        return new AccelerateInterpolator();
    }
    .
    .
    .
}

So the start point Animator will start fast end slow down as it gets towards the end, and the end Animator will start slow and accelerate as it approaches the end:

That’s getting closer, but we can do better!

Before we move on it’s just worth seeing what things look like if we disable the rotation – it helps understand what these two interpolators do together, and also shows what the rotation animator is doing for us:

In the final article in this series we’ll work more on the Interpolators which are the special sauce which brings everything together.

The source code for this article is available here.

© 2016, Mark Allison. All rights reserved.

Copyright © 2016 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.