62

I want to use certain animations such as bounce inside my canvas. Is it possible to use the animation interpolators inside?

In my case I want to rotate an image from 0 to 180 degrees with a bounce at the end.

How is it possible?

biquillo
  • 6,469
  • 7
  • 37
  • 40

1 Answers1

150

The Android animation class applies to objects such as views and layouts. The canvas is just a surface for drawing which is either part of a View or linked to a bitmap. In onDraw in a custom view only one frame is drawn at the time until next invalidate is called, which means that you have to draw your animation frame by frame. Here is an example of bouncing ball which rotates, which you may find useful.

rotating bouncing ball

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.os.Bundle;
import android.text.format.Time;
import android.view.View;

public class StartActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new BallBounce(this));
    }
}


class BallBounce extends View {
    int screenW;
    int screenH;
    int X;
    int Y;
    int initialY ;
    int ballW;
    int ballH;
    int angle;
    float dY;
    float acc;
    Bitmap ball, bgr;

    public BallBounce(Context context) {
        super(context);
        ball = BitmapFactory.decodeResource(getResources(),R.drawable.football); //load a ball image
        bgr = BitmapFactory.decodeResource(getResources(),R.drawable.sky_bgr); //load a background
        ballW = ball.getWidth();
        ballH = ball.getHeight();
        acc = 0.2f; //acceleration
        dY = 0; //vertical speed
        initialY = 100; //Initial vertical position.
        angle = 0; //Start value for rotation angle.
    }

    @Override
    public void onSizeChanged (int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        screenW = w;
        screenH = h;
        bgr = Bitmap.createScaledBitmap(bgr, w, h, true); //Resize background to fit the screen.
        X = (int) (screenW /2) - (ballW / 2) ; //Centre ball into the centre of the screen.
        Y = initialY;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //Draw background.
        canvas.drawBitmap(bgr, 0, 0, null);

        //Compute roughly ball speed and location.
        Y+= (int) dY; //Increase or decrease vertical position.
        if (Y > (screenH - ballH)) {
            dY=(-1)*dY; //Reverse speed when bottom hit.
        }
        dY+= acc; //Increase or decrease speed.

        //Increase rotating angle.
        if (angle++ >360)
            angle =0;

        //Draw ball
        canvas.save(); //Save the position of the canvas.
        canvas.rotate(angle, X + (ballW / 2), Y + (ballH / 2)); //Rotate the canvas.
        canvas.drawBitmap(ball, X, Y, null); //Draw the ball on the rotated canvas.
        canvas.restore(); //Rotate the canvas back so that it looks like ball has rotated.

        //Call the next frame.
        invalidate();
    }
}

This is just a simple illustration but I would use surfaceView and drive frames from another thread, which is a bit more complicated but a proper way to do when making interactive animations like games etc. Here is an example with a scrolling backround and the user can move the ball with his finger:

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class SurfaceViewActivity extends Activity {
    BallBounces ball;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ball = new BallBounces(this);
        setContentView(ball);
    }
}


class BallBounces extends SurfaceView implements SurfaceHolder.Callback {
    GameThread thread;
    int screenW; //Device's screen width.
    int screenH; //Devices's screen height.
    int ballX; //Ball x position.
    int ballY; //Ball y position.
    int initialY ;
    float dY; //Ball vertical speed.
    int ballW;
    int ballH;
    int bgrW;
    int bgrH;
    int angle;
    int bgrScroll;
    int dBgrY; //Background scroll speed.
    float acc;
    Bitmap ball, bgr, bgrReverse;
    boolean reverseBackroundFirst;
    boolean ballFingerMove;

    //Measure frames per second.
    long now;
    int framesCount=0;
    int framesCountAvg=0;
    long framesTimer=0;
    Paint fpsPaint=new Paint();

    //Frame speed
    long timeNow;
    long timePrev = 0;
    long timePrevFrame = 0;
    long timeDelta;


    public BallBounces(Context context) {
        super(context);
        ball = BitmapFactory.decodeResource(getResources(),R.drawable.football); //Load a ball image.
        bgr = BitmapFactory.decodeResource(getResources(),R.drawable.sky_bgr); //Load a background.
        ballW = ball.getWidth();
        ballH = ball.getHeight();

        //Create a flag for the onDraw method to alternate background with its mirror image.
        reverseBackroundFirst = false;

        //Initialise animation variables.
        acc = 0.2f; //Acceleration
        dY = 0; //vertical speed
        initialY = 100; //Initial vertical position
        angle = 0; //Start value for the rotation angle
        bgrScroll = 0;  //Background scroll position
        dBgrY = 1; //Scrolling background speed

        fpsPaint.setTextSize(30);

        //Set thread
        getHolder().addCallback(this);

        setFocusable(true);
    }

    @Override
    public void onSizeChanged (int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //This event-method provides the real dimensions of this custom view.
        screenW = w;
        screenH = h;

        bgr = Bitmap.createScaledBitmap(bgr, w, h, true); //Scale background to fit the screen.
        bgrW = bgr.getWidth();
        bgrH = bgr.getHeight();

        //Create a mirror image of the background (horizontal flip) - for a more circular background.
        Matrix matrix = new Matrix();  //Like a frame or mould for an image.
        matrix.setScale(-1, 1); //Horizontal mirror effect.
        bgrReverse = Bitmap.createBitmap(bgr, 0, 0, bgrW, bgrH, matrix, true); //Create a new mirrored bitmap by applying the matrix.

        ballX = (int) (screenW /2) - (ballW / 2) ; //Centre ball X into the centre of the screen.
        ballY = -50; //Centre ball height above the screen.
    }

    //***************************************
    //*************  TOUCH  *****************
    //***************************************
    @Override
    public synchronized boolean onTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                ballX = (int) ev.getX() - ballW/2;
                ballY = (int) ev.getY() - ballH/2;

                ballFingerMove = true;
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                ballX = (int) ev.getX() - ballW/2;
                ballY = (int) ev.getY() - ballH/2;

                break;
            }

            case MotionEvent.ACTION_UP:
                ballFingerMove = false;
                dY = 0;
                break;
            }
        return true;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //Draw scrolling background.
        Rect fromRect1 = new Rect(0, 0, bgrW - bgrScroll, bgrH);
        Rect toRect1 = new Rect(bgrScroll, 0, bgrW, bgrH);

        Rect fromRect2 = new Rect(bgrW - bgrScroll, 0, bgrW, bgrH);
        Rect toRect2 = new Rect(0, 0, bgrScroll, bgrH);

        if (!reverseBackroundFirst) {
            canvas.drawBitmap(bgr, fromRect1, toRect1, null);
            canvas.drawBitmap(bgrReverse, fromRect2, toRect2, null);
        }
        else{
            canvas.drawBitmap(bgr, fromRect2, toRect2, null);
            canvas.drawBitmap(bgrReverse, fromRect1, toRect1, null);
        }

        //Next value for the background's position.
        if ( (bgrScroll += dBgrY) >= bgrW) {
            bgrScroll = 0;
            reverseBackroundFirst = !reverseBackroundFirst;
        }

        //Compute roughly the ball's speed and location.
        if (!ballFingerMove) {
            ballY += (int) dY; //Increase or decrease vertical position.
            if (ballY > (screenH - ballH)) {
                dY=(-1)*dY; //Reverse speed when bottom hit.
            }
            dY+= acc; //Increase or decrease speed.
        }

        //Increase rotating angle
        if (angle++ >360)
            angle =0;

        //DRAW BALL
        //Rotate method one
        /*
        Matrix matrix = new Matrix();
        matrix.postRotate(angle, (ballW / 2), (ballH / 2)); //Rotate it.
        matrix.postTranslate(ballX, ballY); //Move it into x, y position.
        canvas.drawBitmap(ball, matrix, null); //Draw the ball with applied matrix.

        */// Rotate method two

        canvas.save(); //Save the position of the canvas matrix.
        canvas.rotate(angle, ballX + (ballW / 2), ballY + (ballH / 2)); //Rotate the canvas matrix.
        canvas.drawBitmap(ball, ballX, ballY, null); //Draw the ball by applying the canvas rotated matrix.
        canvas.restore(); //Rotate the canvas matrix back to its saved position - only the ball bitmap was rotated not all canvas.

        //*/

        //Measure frame rate (unit: frames per second).
         now=System.currentTimeMillis();
         canvas.drawText(framesCountAvg+" fps", 40, 70, fpsPaint);
         framesCount++;
         if(now-framesTimer>1000) {
                 framesTimer=now;
                 framesCountAvg=framesCount;
                 framesCount=0;
         }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        thread = new GameThread(getHolder(), this);
        thread.setRunning(true);
        thread.start();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        boolean retry = true;
        thread.setRunning(false);
        while (retry) {
            try {
                thread.join();
                retry = false;
            } catch (InterruptedException e) {

            }
        }
    }


    class GameThread extends Thread {
        private SurfaceHolder surfaceHolder;
        private BallBounces gameView;
        private boolean run = false;

        public GameThread(SurfaceHolder surfaceHolder, BallBounces gameView) {
            this.surfaceHolder = surfaceHolder;
            this.gameView = gameView;
        }

        public void setRunning(boolean run) {
            this.run = run;
        }

        public SurfaceHolder getSurfaceHolder() {
            return surfaceHolder;
        }

        @Override
        public void run() {
            Canvas c;
            while (run) {
                c = null;

                //limit frame rate to max 60fps
                timeNow = System.currentTimeMillis();
                timeDelta = timeNow - timePrevFrame;
                if ( timeDelta < 16) {
                    try {
                        Thread.sleep(16 - timeDelta);
                    }
                    catch(InterruptedException e) {

                    }
                }
                timePrevFrame = System.currentTimeMillis();

                try {
                    c = surfaceHolder.lockCanvas(null);
                    synchronized (surfaceHolder) {
                       //call methods to draw and process next fame
                        gameView.onDraw(c);
                    }
                } finally {
                    if (c != null) {
                        surfaceHolder.unlockCanvasAndPost(c);
                    }
                }
            }
        }
    }
}

Here are the images:

sky backround

enter image description here

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Lumis
  • 21,517
  • 8
  • 63
  • 67
  • 1
    A great tutorial, and my trivial fix. gameView.onDraw(c); => if(gameView!=null && c!=null) gameView.onDraw(c); –  Jun 05 '12 at 07:20
  • 1
    You should probably use `postInvalidateOnAnimation()` to get smooth animations. – Timmmm Oct 15 '12 at 17:57
  • I have tried this application and it's crashes one two cases [1] when I click the back button that will close the application [2] when I change the orientation of the screen from "Portrait" to "Landscape" and VS , I have the same problem in my example http://stackoverflow.com/questions/14212165/surfaceview-crashes-on-surfacechange-android#comment19709278_14212165 can anyone help me solve this problem ?? – Amira Elsayed Ismail Jan 08 '13 at 10:05
  • 1
    Consider the draw loop thread closing strategy: http://stackoverflow.com/questions/5318847/android-crash-when-app-is-closed-and-reopened/5319629#5319629 – Lumis Jan 08 '13 at 13:12
  • Thanks for such a nice tutorial , can you please tell me how to give ripple effect in your code.. –  Jan 16 '13 at 11:34
  • @lumis - understood that this is nearly 3 years old, but it still works well :) ... in the second version, you have two rotation methods when drawing the ball. I honestly don't see any differences in performance or memory metrics. If you have a minute can you mention what they are? – Howard Pautz Oct 15 '13 at 16:33
  • @Howard Pautz, there is no difference it is all matrix based, except in the logic of your program, for example if you rotate Canvas then you can draw many bitmaps before you restore it and do not need to create a separate Matrix object, whatever is more practical for you at the time. – Lumis Oct 15 '13 at 17:39
  • is there more efficient way for this? can i just draw the background once and reuse it in subsequenr frames and only invalidate the ball ? – suitianshi Mar 02 '14 at 02:40
  • @suitianshi Yes, you can draw only the segment of the background where the ball is to 'erase' it. Then draw the new ball and try to do the partial invalidate. With the Surface View you would have to keep track of two buffers switching every time! But if the background is moving or you have many sprites, that may not be practical. – Lumis Mar 17 '14 at 13:54
  • invalidate(); will call next frame and forever? if I don't misunderstand , is this a good practice? can someone answer me? – Jongz Puangput Jun 21 '14 at 02:11
  • 1
    @JongzPuangput `invalidate()` basically redraws the view. So if you update some position variables and then call `invalidate()`, you can draw something to the screen (whatever is in the `onDraw` method) with those new values. So yes, if you update a few variables and call `invalidate()` within a loop, it can be a sort of frame-by-frame animation. Whether it's good practice depends on what you're trying to do. – Zach H Jun 26 '14 at 06:01
  • Ugly examples. In the first one you call invalidate() directly from onDraw() which is basically wrong and will cause huge overwork. In the second one you recreate new shape objects each time onDraw() is called. You have to use once created ones and just update them with set() method – Leo DroidCoder Feb 21 '17 at 12:01