5

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?

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • try [scaling](http://developer.android.com/reference/android/widget/ImageView.ScaleType.html) – Sagar Pilkhwal Sep 08 '14 at 09:04
  • @PankajKumar I don't understand how and why. – android developer Sep 08 '14 at 09:09
  • 1
    override getIntrisic* methods, btw you dont need a callback imho – pskink Sep 08 '14 at 09:20
  • @pskink If I won't use callback, I don't think the animation will work, as written here: http://developer.android.com/reference/android/graphics/drawable/Drawable.html#invalidateSelf() . about your solution, please show an example of what should be done. – android developer Sep 08 '14 at 09:41
  • 1
    in getIntrinsicWidth return mBitmap's width, the same for height – pskink Sep 08 '14 at 09:46
  • @pskink Shouldn't I also use fBounds ? – android developer Sep 08 '14 at 09:57
  • fBounds are never used, why you keep them? the same mCallback, what is it for? – pskink Sep 08 '14 at 10:04
  • @pskink 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? – android developer Sep 08 '14 at 10:29
  • did you override getIntrinsic methods? – pskink Sep 08 '14 at 10:37
  • @pskink 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. – android developer Sep 08 '14 at 10:43
  • so implement it to use bounds set via setBounds, did you try this? – pskink Sep 08 '14 at 10:46
  • @pskink As I've written, it was one of my attempts of fixing it. – android developer Sep 08 '14 at 10:48
  • so post these methofs – pskink Sep 08 '14 at 10:52
  • @pskink 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. – android developer Sep 08 '14 at 12:26
  • I tried to comment out `circularAnimatedDrawable.setCallback(imageView);`, or use `setCallback(null)`, the animation works properly, why? – zhangxaochen Jul 18 '21 at 14:58

2 Answers2

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;
    }

}
android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • well, you made it too far complex, especially your way of drawing the Bitmap is weird... – pskink Sep 08 '14 at 19:25
  • @pskink Maybe, but I didn't have much choice, as I had to change a library that uses them a lot: https://github.com/dmytrodanylyk/circular-progress-button , and I had to finish customizing it in a short time. – android developer Sep 08 '14 at 19:57
  • @pskink 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. – android developer Sep 08 '14 at 21:41
  • 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 wish – pskink Sep 08 '14 at 22:01
  • @pskink 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. – android developer Sep 09 '14 at 05:30
  • 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 setBackgroundDrawable – pskink Sep 09 '14 at 05:57
  • @pskink 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. – android developer Sep 09 '14 at 06:52
  • 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 that – pskink Sep 09 '14 at 06:58
  • @pskink Thank you. It's rare that I customize drawable classes, which is why I've asked this. – android developer Sep 09 '14 at 08:31
  • @pskink How would you use use each of the other classes for it? – android developer Sep 09 '14 at 08:44
  • 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.... – pskink Sep 09 '14 at 08:47
  • "How would you use use each of the other classes for it?" what you mean? – pskink Sep 09 '14 at 08:50
  • @pskink I meant about your suggestions: scheduleSelf and ValueAnimator – android developer Sep 09 '14 at 09:10
  • see ValueAnimator.addUpdateListener(this) and sheduleSelf(this, SystemClock.uptimeMillis() + DELAY_IN_MILLIS) – pskink Sep 09 '14 at 09:16
  • I meant how would you use them in the code. anyway, I will just try them out. – android developer Sep 09 '14 at 09:58
  • this is a quick start on how to use them, the rest you will figure out – pskink Sep 09 '14 at 10:01
  • If you wish, you can post an answer , and even use one of those functions. – android developer Sep 09 '14 at 11:12
2

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();
    }
}
pskink
  • 23,874
  • 6
  • 66
  • 77