Wednesday, May 27, 2015

Indeterminate progressbar with rounded corners

The build in android progressbar can be customised to use different drawables, but than the corners would be sharp. Our designer need it to be rounded. My solution is flexible enough that it can be cropped to any shape.



This is how I did it (essentially creating a custom Drawable from program code).

It gets a Drawable, tiles it (repeating it) and crops it to a shape that was provided by the Callable. It handles AnimationDrawable also: creates a similar AnimationDrawable tiling and cropping each frame. For the clip shape I had to use Callable - which is essentially a factory - because a new ShapeDrawable has to be created for each animation frame.  The inner class EquallyRoundCornerRect can be used for the most common scenario: equal round corner.

/**
 * It gets a {@link android.graphics.drawable.Drawable}, tiles it (repeating it) and crops it to a shape that was provided by the
 * {@link java.util.concurrent.Callable}. It handles {@link android.graphics.drawable.AnimationDrawable} also: creates a similar
 * {@link android.graphics.drawable.AnimationDrawable} tiling and cropping each frame. For the clip shape I had to use
 * {@link java.util.concurrent.Callable} - which is essentially a factory - because a new {@link android.graphics.drawable.ShapeDrawable}
 * has to be created for each animation frame.
 * The inner class {@link EquallyRoundCornerRect} can be used for the most common scenario: equal round corner.
 */
public class TiledClippedDrawable {

    public static Drawable createFrom(final Drawable originalDrawable, final Callable<ShapeDrawable> clipShape) {
        return tileifyIndeterminate(originalDrawable, clipShape);
    }

    private static Drawable tileify(Drawable drawable, final Callable<ShapeDrawable> clipShape) {
        final Bitmap tileBitmap = ((BitmapDrawable) drawable).getBitmap();
        try {
            final ShapeDrawable shapeDrawable = clipShape.call();

            final BitmapShader bitmapShader = new BitmapShader(tileBitmap,
                    Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
            shapeDrawable.getPaint().setShader(bitmapShader);

            return new ClipDrawable(shapeDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL);
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Drawable tileifyIndeterminate(Drawable drawable, final Callable<ShapeDrawable> clipShape) {
        if (drawable instanceof AnimationDrawable) {
            AnimationDrawable background = (AnimationDrawable) drawable;
            final int N = background.getNumberOfFrames();
            AnimationDrawable newBg = new AnimationDrawable();
            newBg.setOneShot(background.isOneShot());

            for (int i = 0; i < N; i++) {
                Drawable frame = tileify(background.getFrame(i), clipShape);
                frame.setLevel(10000);
                newBg.addFrame(frame, background.getDuration(i));
            }
            newBg.setLevel(10000);
            drawable = newBg;
            return drawable;
        }
        return tileify(drawable, clipShape);
    }

    public static class EquallyRoundCornerRect implements Callable<ShapeDrawable> {
        private final int cornerRadiusPixel;

        public EquallyRoundCornerRect(final int cornerRadiusPixel) {
            this.cornerRadiusPixel = cornerRadiusPixel;
        }

        @Override
        public ShapeDrawable call() throws Exception {
            final float[] roundedCorners = new float[] {cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel,
                    cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel};
            return new ShapeDrawable(new RoundRectShape(roundedCorners, null, null));
        }
    }
}

And this is how it can be used:

final int cornerRadius = getResources().getDimensionPixelOffset(R.dimen.progressbar_corner_radius);
final Drawable originalDrawable = getResources().getDrawable(R.drawable.progress_bar_indeterminate_horizontal);
final Drawable roundedAnimation = TiledClippedDrawable.createFrom(originalDrawable, new TiledClippedDrawable.EquallyRoundCornerRect(cornerRadius));
unpackingSpinner = (ProgressBar) backView.findViewById(R.id.gallery_item_unpacking_spinner);
unpackingSpinner.setIndeterminateDrawable(roundedAnimation);


Here the R.dimen.progressbar_corner_radius is a number, and the R.drawable.progress_bar_indeterminate_horizontal is an animationlist. Each of those install_progressbar_indeterminate1 images contain only a fragment, because they will be tiled anyway.
<animation-list
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false">
    <item android:drawable="@drawable/install_progressbar_indeterminate1" android:duration="200" />
    <item android:drawable="@drawable/install_progressbar_indeterminate2" android:duration="200" />
    <item android:drawable="@drawable/install_progressbar_indeterminate3" android:duration="200" />
</animation-list>

No comments: