How to properly create a customized animated drawable?
Asked Answered
I

2

6

Background

I've been searching in plenty of places to find out how to animate a drawable without animating the view and without using the built in drawables.

The reason is that I will need to prepare a customized animation within the drawable, and I might have different requirements for it later.

For now, I'm making a basic animated drawable that just spins a given bitmap inside it.

I've set it on an imageView, but I wish to be able to use it on any kind of view, even customized views that have overridden the onDraw function.

The problem

I can't find out how to show the drawable without being cut, no matter what the size of the view is. Here's what I see:

enter image description here

The code

Here's the code:

private class CircularAnimatedDrawable extends Drawable implements Animatable {
    private static final Interpolator ANGLE_INTERPOLATOR = new LinearInterpolator();
    private static final int ANGLE_ANIMATOR_DURATION = 2000;
    private final RectF fBounds = new RectF();
    private float angle = 0;
    private ObjectAnimator mObjectAnimatorAngle;
    private final Paint mPaint;
    private boolean mRunning;
    private final Bitmap mBitmap;

    public CircularAnimatedDrawable(final Bitmap bitmap) {
        this.mBitmap = bitmap;
        mPaint = new Paint();
        setupAnimations();
    }

    public float getAngle() {
        return this.angle;
    }

    public void setAngle(final float angle) {
        this.angle = angle;
        invalidateSelf();
    }

    @Override
    public Callback getCallback() {
        return mCallback;
    }

    @Override
    public void draw(final Canvas canvas) {
        canvas.save();
        canvas.rotate(angle);
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        canvas.restore();
    }

    @Override
    public void setAlpha(final int alpha) {
        mPaint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(final ColorFilter cf) {
        mPaint.setColorFilter(cf);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSPARENT;
    }

    @Override
    protected void onBoundsChange(final Rect bounds) {
        super.onBoundsChange(bounds);
        fBounds.left = bounds.left;
        fBounds.right = bounds.right;
        fBounds.top = bounds.top;
        fBounds.bottom = bounds.bottom;
    }

    private void setupAnimations() {
        mObjectAnimatorAngle = ObjectAnimator.ofFloat(this, "angle", 360f);
        mObjectAnimatorAngle.setInterpolator(ANGLE_INTERPOLATOR);
        mObjectAnimatorAngle.setDuration(ANGLE_ANIMATOR_DURATION);
        mObjectAnimatorAngle.setRepeatMode(ValueAnimator.RESTART);
        mObjectAnimatorAngle.setRepeatCount(ValueAnimator.INFINITE);
    }

    @Override
    public void start() {
        if (isRunning())
            return;
        mRunning = true;
        mObjectAnimatorAngle.start();
        invalidateSelf();
    }

    @Override
    public void stop() {
        if (!isRunning())
            return;
        mRunning = false;
        mObjectAnimatorAngle.cancel();
        invalidateSelf();
    }

    @Override
    public boolean isRunning() {
        return mRunning;
    }

}

and the usage :

    final ImageView imageView = (ImageView) findViewById(R.id.imageView);
    final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.spinner_76_inner_holo);

    final CircularAnimatedDrawable circularAnimatedDrawable = new CircularAnimatedDrawable(bitmap);
    circularAnimatedDrawable.setCallback(imageView);
    circularAnimatedDrawable.start();
    imageView.setImageDrawable(circularAnimatedDrawable);

The question

How can I set it to make the drawable fit the view?

Should I use the bitmap size? the fBounds? both? Or maybe something else?

Ible answered 8/9, 2014 at 8:55 Comment(15)
try scalingFarfetched
@PankajKumar I don't understand how and why.Ible
override getIntrisic* methods, btw you dont need a callback imhoOkubo
@Okubo If I won't use callback, I don't think the animation will work, as written here: developer.android.com/reference/android/graphics/drawable/… . about your solution, please show an example of what should be done.Ible
in getIntrinsicWidth return mBitmap's width, the same for heightOkubo
@Okubo Shouldn't I also use fBounds ?Ible
fBounds are never used, why you keep them? the same mCallback, what is it for?Okubo
@Okubo They were traces from previous attempts in fixing this issue. I also know that the drawable might be used on custom views with their own onDraw, so I thought that the bounds should be used too (padding etc...) . do you say that I won't be needing those?Ible
did you override getIntrinsic methods?Okubo
@Okubo Yes, but I don't think it works well when I use setBounds. I want to be able to control the size of the drawable (using setBounds) from the view itself.Ible
so implement it to use bounds set via setBounds, did you try this?Okubo
@Okubo As I've written, it was one of my attempts of fixing it.Ible
so post these methofsOkubo
@Okubo There are no additional methods. I've written all of the methods of the drawable. anyway, I've found the answer. I've added the getIntrisic* methods even though they do not affect the solution in my case (but they are more correct than without), as instead of an ImageView I use the code above in a customized view with its own onDraw method, and it sets its own bounds for the drawable. Anyway, I've upvoted your comment for helping on this.Ible
I tried to comment out circularAnimatedDrawable.setCallback(imageView);, or use setCallback(null), the animation works properly, why?Cowgill
O
3

try this modified version of your Drawable:

class CircularAnimatedDrawable extends Drawable implements Animatable, TimeAnimator.TimeListener {
    private static final float TURNS_PER_SECOND = 0.5f;
    private Bitmap mBitmap;
    private boolean mRunning;
    private TimeAnimator mTimeAnimator = new TimeAnimator();
    private Paint mPaint = new Paint();
    private Matrix mMatrix = new Matrix();

    public CircularAnimatedDrawable(final Bitmap bitmap) {
        mBitmap = bitmap;
        mTimeAnimator.setTimeListener(this);
    }
    @Override
    public void draw(final Canvas canvas) {
        canvas.drawBitmap(mBitmap, mMatrix, mPaint);
    }
    @Override
    protected void onBoundsChange(Rect bounds) {
        Log.d(TAG, "onBoundsChange " + bounds);
        mMatrix.setRectToRect(new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()),
                new RectF(bounds),
                Matrix.ScaleToFit.CENTER);
    }
    @Override
    public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
        Rect b = getBounds();
        mMatrix.postRotate(360 * TURNS_PER_SECOND * deltaTime / 1000, b.centerX(), b.centerY());
        invalidateSelf();
    }
    @Override
    public void setAlpha(final int alpha) {
        mPaint.setAlpha(alpha);
    }
    @Override
    public void setColorFilter(final ColorFilter cf) {
        mPaint.setColorFilter(cf);
    }
    @Override
    public int getOpacity() {
        return PixelFormat.TRANSPARENT;
    }
    @Override
    public void start() {
        if (isRunning())
            return;
        mRunning = true;
        mTimeAnimator.start();
        invalidateSelf();
    }
    @Override
    public void stop() {
        if (!isRunning())
            return;
        mRunning = false;
        mTimeAnimator.cancel();
        invalidateSelf();
    }
    @Override
    public boolean isRunning() {
        return mRunning;
    }
}

EDIT: version without Animator stuff (uses [un]scheduleSelf), NOTE it uses View's Drawable.Callback mechanism so it usually cannot be started directly from onCreate where View doesn't have attached Handler yet

class CircularAnimatedDrawable extends Drawable implements Animatable, Runnable {
    private static final float TURNS_PER_SECOND = 0.5f;
    private static final long DELAY = 50;
    private Bitmap mBitmap;
    private long mLastTime;
    private boolean mRunning;
    private Paint mPaint = new Paint();
    private Matrix mMatrix = new Matrix();

    public CircularAnimatedDrawable(final Bitmap bitmap) {
        mBitmap = bitmap;
    }
    @Override
    public void draw(final Canvas canvas) {
        canvas.drawBitmap(mBitmap, mMatrix, mPaint);
    }
    @Override
    protected void onBoundsChange(Rect bounds) {
        Log.d(TAG, "onBoundsChange " + bounds);
        mMatrix.setRectToRect(new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()),
                new RectF(bounds),
                Matrix.ScaleToFit.CENTER);
    }
    @Override
    public void setAlpha(final int alpha) {
        mPaint.setAlpha(alpha);
    }
    @Override
    public void setColorFilter(final ColorFilter cf) {
        mPaint.setColorFilter(cf);
    }
    @Override
    public int getOpacity() {
        return PixelFormat.TRANSPARENT;
    }
    @Override
    public void start() {
        if (isRunning())
            return;
        mRunning = true;
        mLastTime = SystemClock.uptimeMillis();
        scheduleSelf(this, 0);
        invalidateSelf();
    }
    @Override
    public void stop() {
        if (!isRunning())
            return;
        mRunning = false;
        unscheduleSelf(this);
        invalidateSelf();
    }
    @Override
    public boolean isRunning() {
        return mRunning;
    }
    @Override
    public void run() {
        long now = SystemClock.uptimeMillis();
        Rect b = getBounds();
        long deltaTime = now - mLastTime;
        mLastTime = now;
        mMatrix.postRotate(360 * TURNS_PER_SECOND * deltaTime / 1000, b.centerX(), b.centerY());
        scheduleSelf(this, now + DELAY);
        invalidateSelf();
    }
}
Okubo answered 9/9, 2014 at 11:29 Comment(4)
Thank you. it works. However, as I've said, TimeAnimator is available only from API 16 and I use API 14 , so I've only updated the part of the matrix.Ible
Anyway, I've updated my original answer, in case anyone wishes to support older versions.Ible
added non Animator versionOkubo
I think I prefer the TimeListener or ObjectAnimator better, but thanks.Ible
I
2

ok, the fix is:

    @Override
    public void draw(final Canvas canvas) {
        canvas.save();
        canvas.rotate(angle, fBounds.width() / 2 + fBounds.left, fBounds.height() / 2 + fBounds.top);
        canvas.translate(fBounds.left, fBounds.top);
        canvas.drawBitmap(mBitmap, null, new Rect(0, 0, (int) fBounds.width(), (int) fBounds.height()), mPaint);
        canvas.restore();
    }

    @Override
    public int getIntrinsicHeight() {
        return mBitmap.getHeight();
    }

    @Override
    public int getIntrinsicWidth() {
        return mBitmap.getWidth();
    }

It works fine. I hope it will be enough for the future changes.

EDIT: here's an optimization to the above, including all changes:

class CircularAnimatedDrawable extends Drawable implements Animatable {
    private static final Interpolator ANGLE_INTERPOLATOR = new LinearInterpolator();
    private static final int ANGLE_ANIMATOR_DURATION = 2000;
    private float angle = 0;
    private ObjectAnimator mObjectAnimatorAngle;
    private final Paint mPaint;
    private boolean mRunning;
    private final Bitmap mBitmap;
    private final Matrix mMatrix = new Matrix();

    public CircularAnimatedDrawable(final Bitmap bitmap) {
        this.mBitmap = bitmap;
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        setupAnimations();
    }

    @SuppressWarnings("unused")
    public float getAngle() {
        return this.angle;
    }

    @SuppressWarnings("unused")
    public void setAngle(final float angle) {
        this.angle = angle;
        invalidateSelf();
    }

    @Override
    public void draw(final Canvas canvas) {
        final Rect b = getBounds();
        canvas.save();
        canvas.rotate(angle, b.centerX(), b.centerY());
        canvas.drawBitmap(mBitmap, mMatrix, mPaint);
        canvas.restore();
    }

    @Override
    public void setAlpha(final int alpha) {
        mPaint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(final ColorFilter cf) {
        mPaint.setColorFilter(cf);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSPARENT;
    }

    @Override
    protected void onBoundsChange(final Rect bounds) {
        super.onBoundsChange(bounds);
        mMatrix.setRectToRect(new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()), new RectF(bounds),
                Matrix.ScaleToFit.CENTER);
    }

    @Override
    public int getIntrinsicHeight() {
        return mBitmap.getHeight();
    }

    @Override
    public int getIntrinsicWidth() {
        return mBitmap.getWidth();
    }

    private void setupAnimations() {
        mObjectAnimatorAngle = ObjectAnimator.ofFloat(this, "angle", 360f);
        mObjectAnimatorAngle.setInterpolator(ANGLE_INTERPOLATOR);
        mObjectAnimatorAngle.setDuration(ANGLE_ANIMATOR_DURATION);
        mObjectAnimatorAngle.setRepeatMode(ValueAnimator.RESTART);
        mObjectAnimatorAngle.setRepeatCount(ValueAnimator.INFINITE);
    }

    @Override
    public void start() {
        if (isRunning())
            return;
        mRunning = true;
        mObjectAnimatorAngle.start();
        invalidateSelf();
    }

    @Override
    public void stop() {
        if (!isRunning())
            return;
        mRunning = false;
        mObjectAnimatorAngle.cancel();
        invalidateSelf();
    }

    @Override
    public boolean isRunning() {
        return mRunning;
    }

}
Ible answered 8/9, 2014 at 12:24 Comment(17)
well, you made it too far complex, especially your way of drawing the Bitmap is weird...Okubo
@Okubo Maybe, but I didn't have much choice, as I had to change a library that uses them a lot: github.com/dmytrodanylyk/circular-progress-button , and I had to finish customizing it in a short time.Ible
@Okubo Again, my task was to customize a library. I had to change the progress animation of this view on the library, and it used bounds as one of its parameters. I had to simplify the question in order to help people help me. Your solution didn't work for this task, and that's why I said I need to include bounds and a custom view. Sorry.Ible
the drawable i posted reacts on any bounds you want, try a view 10x10 or 1000x1000 and you will see how it works, using the Matrix you can scale your Bitmap to any bounds you wishOkubo
@Okubo Sorry I didn't read your solution much as I've already found a solution. If the view wishes to draw the drawable in a specific bounds within it (say, to the left, and centered vertically) , will your code work too? If so, you can put another answer that people might want to use.Ible
yes, it will work, the Matrix that is used by Canvas.drawBitmap is setup in onBoundsChange so it reacts on actuall Drawable bounds set either directly by Drawable.setBounds or indirectly by setImageDrawable or setBackgroundDrawableOkubo
@Okubo you used TimeListener which is available as of API 16 .I use API 14 as minSDK. I've ignored this part and changed the part of the bounds, and it also works. I think your solution is better. If you wish, you can post it here.Ible
you can easily use ValueAnimator which is faster than ObjectAnimator since it doesn't use reflaction stuff, btw you can get rid of Animator stuff at all and use Drawable mechanisms for that, see Drawable.scheduleSelf and unscheduleSelf, they are designed for things like thatOkubo
@Okubo Thank you. It's rare that I customize drawable classes, which is why I've asked this.Ible
@Okubo How would you use use each of the other classes for it?Ible
in fact custom Drawables (created from java code) are very powerful beasts, for example custom ProgressBars/SeekBars, Buttons with small counter numbers in the corner, etc, the possibilities are endless....Okubo
"How would you use use each of the other classes for it?" what you mean?Okubo
@Okubo I meant about your suggestions: scheduleSelf and ValueAnimatorIble
see ValueAnimator.addUpdateListener(this) and sheduleSelf(this, SystemClock.uptimeMillis() + DELAY_IN_MILLIS)Okubo
I meant how would you use them in the code. anyway, I will just try them out.Ible
this is a quick start on how to use them, the rest you will figure outOkubo
If you wish, you can post an answer , and even use one of those functions.Ible

© 2022 - 2024 — McMap. All rights reserved.