Is it possible to have an animated drawable?

35,979

Solution 1

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();

Solution 2

Drawables

There you go! And this one for RotateDrawable. I believe that from the Doc it should be pretty straitght forward. You can define everything in a xml file and set the background of a view as the drawable xml. /drawable/myrotate.xml -> @drawable/myrotate

Edit: This is an answer I found here. Drawable Rotating around its center Android

Edit 2: You are right the RotateDrawable seem broken. I don't know I tried it too. I haven't yet succeded in making it animate. But I did succed to rotate it. You have to use setLevel which will rotate it. Though it doesn't look really useful. I browsed the code and the RotateDrawable doesn't even inflate the animation duration and the current rotation seems strangely use the level as a measure for rotation. I believe you have to use it with a AnimationDrawable but here again. It just crashed for me. I haven't used that feature yet but planned to use it in the future. I browsed the web and the RotateDrawable seems to be very undocumented like almost every Drawable objects.

Solution 3

Here is one of possible ways (especially useful when you have a Drawable somewhere set and need to animate it). The idea is to wrap the drawable and decorate it with animation. In my case, I had to rotate it, so below you can find sample implementation:

public class RotatableDrawable extends DrawableWrapper {

    private float rotation;
    private Rect bounds;
    private ObjectAnimator animator;
    private long defaultAnimationDuration;

    public RotatableDrawable(Resources resources, Drawable drawable) {
        super(vectorToBitmapDrawableIfNeeded(resources, drawable));
        bounds = new Rect();
        defaultAnimationDuration = resources.getInteger(android.R.integer.config_mediumAnimTime);
    }

    @Override
    public void draw(Canvas canvas) {
        copyBounds(bounds);
        canvas.save();
        canvas.rotate(rotation, bounds.centerX(), bounds.centerY());
        super.draw(canvas);
        canvas.restore();
    }

    public void rotate(float degrees) {
        rotate(degrees, defaultAnimationDuration);
    }

    public void rotate(float degrees, long millis) {
        if (null != animator && animator.isStarted()) {
            animator.end();
        } else if (null == animator) {
            animator = ObjectAnimator.ofFloat(this, "rotation", 0, 0);
            animator.setInterpolator(new AccelerateDecelerateInterpolator());
        }
        animator.setFloatValues(rotation, degrees);
        animator.setDuration(millis).start();
    }

    @AnimatorSetter
    public void setRotation(float degrees) {
        this.rotation = degrees % 360;
        invalidateSelf();
    }

    /**
     * Workaround for issues related to vector drawables rotation and scaling:
     * https://code.google.com/p/android/issues/detail?id=192413
     * https://code.google.com/p/android/issues/detail?id=208453
     */
    private static Drawable vectorToBitmapDrawableIfNeeded(Resources resources, Drawable drawable) {
        if (drawable instanceof VectorDrawable) {
            Bitmap b = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
            Canvas c = new Canvas(b);
            drawable.setBounds(0, 0, c.getWidth(), c.getHeight());
            drawable.draw(c);
            drawable = new BitmapDrawable(resources, b);
        }
        return drawable;
    }
}

and you can use it like this (rotating toolbar navigation icon 360 degrees):

backIcon = new RotatableDrawable(getResources(), toolbar.getNavigationIcon().mutate());
toolbar.setNavigationIcon(backIcon);
backIcon.rotate(360);

It shouldn't be hard to add a method that will rotate it indefinite (setRepeatMode INFINITE for animator)

Solution 4

You can start from studying the ProgressBar2 from API Demos project (it is available as a part of the SDK). Specifically pay attention to R.layout.progressbar_2.

Share:
35,979
cottonBallPaws
Author by

cottonBallPaws

Updated on December 15, 2020

Comments

  • cottonBallPaws
    cottonBallPaws over 3 years

    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?

  • cottonBallPaws
    cottonBallPaws over 13 years
    I tried my luck with the RotateDrawable and it did not animate. More details up in my original question. Any idea about what I am doing wrong?
  • cottonBallPaws
    cottonBallPaws over 13 years
    So the RotateDrawable has to be programmicatly started? I guess that kind of defeats the purpose of trying to deal with it in xml. How would you start it?
  • android developer
    android developer almost 9 years
    Isn't there a solution that's only in XML ?
  • Andrea Lazzarotto
    Andrea Lazzarotto about 8 years
    The RotateDrawable is (programmatically) totally useless prior to Android API 21. You can initialize one but you cannot set its child, nor set its rotation angle. Crazy.
  • Steve M
    Steve M over 7 years
    What is @AnimatorSetter it doesn't compile with it.
  • Steve M
    Steve M over 7 years
    instanceof VectorDrawable causes crash on pre-Lollipop and not needed unless it is vector.
  • Steve M
    Steve M over 7 years
    Also this class will need to be kept in ProGuard.
  • GregoryK
    GregoryK over 7 years
    @SteveM you can use the following check to support pre Lollipop (drawable instanceof VectorDrawableCompat || drawable.getClass().getSimpleName().equals("VectorDrawable")‌​) insteado (drawable instanceof VectorDrawable) AnimatorSetter is my custom annotation that I use to indicate to ProGuard and Android studio that the method should be kept as is.
  • ipcjs
    ipcjs almost 7 years
    drawable.setLevel() is more convenient. eg: ObjectAnimator.ofInt(drawable, "level", 0, 10000)
  • Timmmm
    Timmmm almost 7 years
    That's... in my answer.
  • Ludvig W
    Ludvig W over 4 years
    @Timmmm This is not what the OP was asking for, he was specifically asking for a purely XML solution.
  • Timmmm
    Timmmm over 4 years
    @LudvigW: This is as close as you can get as far as I know.