Android – Is it possible to have an animated drawable

androidanimationdrawableprogress-bar

Is it possible to create a drawable that has some sort of animation, whether it is a frame by frame animation, rotation, etc, that is defined as a xml drawable and can be represented by a single Drawable object without having to deal with the animation in code?

How I am thinking to use it:
I have a list and each item in this list may at sometime have something happening to it. While it is happening, I would like to have a spinning progress animation similar to a indeterminate ProgressBar. Since there may also be several of these on screen I thought that if they all shared the same Drawable they would only need one instance of it in memory and their animations would be synced so you wouldn't have a bunch of spinning objects in various points in the spinning animation.

I'm not attached to this approach. I'm just trying to think of the most efficient way to display several spinning progress animations and ideally have them synced together so they are consistent in appearance.

Thanks

In response to Sybiam's answer:

I have tried implementing a RotateDrawable but it is not rotating.

Here is my xml for the drawable so far:

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
 android:drawable="@drawable/my_drawable_to_rotate"
 android:fromDegrees="0" 
 android:toDegrees="360"
 android:pivotX="50%"
 android:pivotY="50%"
 android:duration="800"
 android:visible="true" />

I have tried using that drawable as the src and background of a ImageView and both ways only produced a non-rotating image.

Is there something that has to start the image rotation?

Best Answer

Yes! The (undocumented) key, which I discovered by reading the ProgressBar code is that you have to call Drawable.setLevel() in onDraw() in order for the <rotate> thing to have any effect. The ProgressBar works something like this (extra unimportant code omitted):

The drawable XML:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <rotate
             android:drawable="@drawable/spinner_48_outer_holo"
             android:pivotX="50%"
             android:pivotY="50%"
             android:fromDegrees="0"
             android:toDegrees="1080" />
    </item>
    <item>
        <rotate
             android:drawable="@drawable/spinner_48_inner_holo"
             android:pivotX="50%"
             android:pivotY="50%"
             android:fromDegrees="720"
             android:toDegrees="0" />
    </item>
</layer-list>

In onDraw():

    Drawable d = getDrawable();
    if (d != null)
    {
        // Translate canvas so a indeterminate circular progress bar with
        // padding rotates properly in its animation
        canvas.save();
        canvas.translate(getPaddingLeft(), getPaddingTop());

        long time = getDrawingTime();

        // I'm not sure about the +1.
        float prog = (float)(time % ANIM_PERIOD+1) / (float)ANIM_PERIOD; 
        int level = (int)(MAX_LEVEL * prog);
        d.setLevel(level);
        d.draw(canvas);

        canvas.restore();

        ViewCompat.postInvalidateOnAnimation(this);
    }

MAX_LEVEL is a constant, and is always 10000 (according to the docs). ANIM_PERIOD is the period of your animation in milliseconds.

Unfortunately since you need to modify onDraw() you can't just put this drawable in an ImageView since ImageView never changes the drawable level. However you may be able to change the drawable level from outside the ImageView's. ProgressBar (ab)uses an AlphaAnimation to set the level. So you'd do something like this:

mMyImageView.setImageDrawable(myDrawable);

ObjectAnimator anim = ObjectAnimator.ofInt(myDrawable, "level", 0, MAX_LEVEL);
anim.setRepeatCount(ObjectAnimator.INFINITE);
anim.start();

It might work but I haven't tested it.

Edit

There is actually an ImageView.setImageLevel() method so it might be as simple as:

ObjectAnimator anim = ObjectAnimator.ofInt(myImageVew, "ImageLevel", 0, MAX_LEVEL);
anim.setRepeatCount(ObjectAnimator.INFINITE);
anim.start();