Making a Running Man Animation on Android is a great way to learn how to work with Bitmaps, Thread and SurfaceView. First thing to make a Running Man Game Animation is to have a character to animate. For that, we will use the following sprite sheet :

Like you can see, our character sprite sheet has 8 frames. Each frame show the character in a different position when he runs. Note that you can also discover this tutorial in video on Youtube :

For our animation, we are going to create a custom GameView extending the SurfaceView class and implementing the Runnable interface. First, we need to define some properties like the game thread, the surface holder, the canvas where running man will be drawn, the bitmap used to load the sprite sheet and some parameters to customize the running man animation like the speed, the size or the position of the man on the screen :


 class GameView extends SurfaceView implements Runnable {

     private Thread gameThread;
     private SurfaceHolder ourHolder;
     private volatile boolean playing;
     private Canvas canvas;
     private Bitmap bitmapRunningMan;
     private boolean isMoving;
     private float runSpeedPerSecond = 500;
     private float manXPos = 10, manYPos = 10;
     private int frameWidth = 230, frameHeight = 274;
     private int frameCount = 8;
     private int currentFrame = 0;
     private long fps;
     private long timeThisFrame;
     private long lastFrameChangeTime = 0;
     private int frameLengthInMillisecond = 50;

     // ...

 }

To draw correctly the good frame for the running man, we need two Rectangle instances. One used to define the current frame in the sprite sheet and an other to define where to draw the current frame on the screen :


private Rect frameToDraw = new Rect(0, 0, frameWidth, frameHeight);
private RectF whereToDraw = new RectF(manXPos, manYPos,
                     manXPos + frameWidth, frameHeight);

On the GameView constructor, we get the surface holder and then, we load the sprite sheet into the bitmapRunningMan variable. We apply a scale transformation according values defined in frameWidth and frameHeight parameters :


 public GameView(Context context) {
    super(context);
    ourHolder = getHolder();
    bitmapRunningMan = BitmapFactory.decodeResource(getResources(),
                             R.drawable.running_man);
    bitmapRunningMan = Bitmap.createScaledBitmap(bitmapRunningMan,
                             frameWidth * frameCount, frameHeight, false);
 }

Now, it’s time to make the event loop for animation inside the run method overrided from Runnable interface :

 

 @Override
 public void run() {
    while (playing) {
       long startFrameTime = System.currentTimeMillis();
       update();
       draw();

       timeThisFrame = System.currentTimeMillis() - startFrameTime;

       if (timeThisFrame >= 1) {
          fps = 1000 / timeThisFrame;
       }
    }
 }

Note that we animate the character while the playing variable is set to true. Like usual in a game, we update the elements and then we draw before to calculate frame per seconds. The update method is just used here to move the man positions in X and Y. Note that when the man reach the end of the screen horizontally or vertically, we set its position to the left or top of the screen :


        public void update() {
            if (isMoving) {
                manXPos = manXPos + runSpeedPerSecond / fps;

                if (manXPos > getWidth()) {
                    manYPos += (int) frameHeight;
                    manXPos = 10;
                }

                if (manYPos + frameHeight > getHeight()) {
                    manYPos = 10;
                }
            }
        }

Before to write the draw method, we need to define a method to manage the current frame to display for the character. We change the current frame only when he have ended the frame duration :


  public void manageCurrentFrame() {
     long time = System.currentTimeMillis();

     if (isMoving) {
         if (time > lastFrameChangeTime + frameLengthInMillisecond) {
            lastFrameChangeTime = time;
            currentFrame++;

            if (currentFrame >= frameCount) {
               currentFrame = 0;
            }
         }
     }

     frameToDraw.left = currentFrame * frameWidth;
     frameToDraw.right = frameToDraw.left + frameWidth;
  }

And now, we define the draw method :


  public void draw() {
     if (ourHolder.getSurface().isValid()) {
        canvas = ourHolder.lockCanvas();
        canvas.drawColor(Color.WHITE);
        whereToDraw.set((int) manXPos, (int) manYPos, (int) manXPos
                        + frameWidth, (int) manYPos + frameHeight);
        manageCurrentFrame();
        canvas.drawBitmap(bitmapRunningMan, frameToDraw, whereToDraw, null);
        ourHolder.unlockCanvasAndPost(canvas);
     }
  }

First, we check if the surface is valid. Then we lock the canvas and we draw the character current frame. Last, we unlock the canvas and post it on the Surface View. Finally, we define two methods to pause or resume the running man animation :


  public void pause() {
     playing = false;

     try {
        gameThread.join();
     } catch(InterruptedException e) {
        Log.e("ERR", "Joining Thread");
     }
  }

  public void resume() {
     playing = true;
     gameThread = new Thread(this);
     gameThread.start();
  }

To start the running man animation, we’re going to wait the user click on the surface view. So, we need to override the onTouchEvent method and wait for an ACTION_DOWN event. When the event is made, we have just to change the isMoving boolean value. If man is running, we stop it. If man doesn’t run, we start to move it :


  @Override
  public boolean onTouchEvent(MotionEvent event) {
     switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN :
           isMoving = !isMoving;
           break;
     }

     return true;
  }

Last thing to make is to assemble all the pieces of the puzzle, create the game view on the main activity, set it as the content view and then resume or pause the game animation when the activity is resumed or paused :


package com.ssaurel.runningman;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class RunningManAnimation extends AppCompatActivity {

    private GameView gameView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        gameView = new GameView(this);
        setContentView(gameView);
    }

    @Override
    protected void onResume() {
        super.onResume();
        gameView.resume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        gameView.pause();
    }

    class GameView extends SurfaceView implements Runnable {

        private Thread gameThread;
        private SurfaceHolder ourHolder;
        private volatile boolean playing;
        private Canvas canvas;
        private Bitmap bitmapRunningMan;
        private boolean isMoving;
        private float runSpeedPerSecond = 500;
        private float manXPos = 10, manYPos = 10;
        private int frameWidth = 230, frameHeight = 274;
        private int frameCount = 8;
        private int currentFrame = 0;
        private long fps;
        private long timeThisFrame;
        private long lastFrameChangeTime = 0;
        private int frameLengthInMillisecond = 50;

        private Rect frameToDraw = new Rect(0, 0, frameWidth, frameHeight);

        private RectF whereToDraw = new RectF(manXPos, manYPos, manXPos + frameWidth, frameHeight);

        public GameView(Context context) {
            super(context);
            ourHolder = getHolder();
            bitmapRunningMan = BitmapFactory.decodeResource(getResources(), R.drawable.running_man);
            bitmapRunningMan = Bitmap.createScaledBitmap(bitmapRunningMan, frameWidth * frameCount, frameHeight, false);
        }

        @Override
        public void run() {
            while (playing) {
                long startFrameTime = System.currentTimeMillis();
                update();
                draw();

                timeThisFrame = System.currentTimeMillis() - startFrameTime;

                if (timeThisFrame >= 1) {
                    fps = 1000 / timeThisFrame;
                }
            }
        }

        public void update() {
            if (isMoving) {
                manXPos = manXPos + runSpeedPerSecond / fps;

                if (manXPos > getWidth()) {
                    manYPos += (int) frameHeight;
                    manXPos = 10;
                }

                if (manYPos + frameHeight > getHeight()) {
                    manYPos = 10;
                }
            }
        }

        public void manageCurrentFrame() {
            long time = System.currentTimeMillis();

            if (isMoving) {
                if (time > lastFrameChangeTime + frameLengthInMillisecond) {
                    lastFrameChangeTime = time;
                    currentFrame++;

                    if (currentFrame >= frameCount) {
                        currentFrame = 0;
                    }
                }
            }

            frameToDraw.left = currentFrame * frameWidth;
            frameToDraw.right = frameToDraw.left + frameWidth;
        }

        public void draw() {
            if (ourHolder.getSurface().isValid()) {
                canvas = ourHolder.lockCanvas();
                canvas.drawColor(Color.WHITE);
                whereToDraw.set((int) manXPos, (int) manYPos, (int) manXPos + frameWidth, (int) manYPos + frameHeight);
                manageCurrentFrame();
                canvas.drawBitmap(bitmapRunningMan, frameToDraw, whereToDraw, null);
                ourHolder.unlockCanvasAndPost(canvas);
            }
        }

        public void pause() {
            playing = false;

            try {
                gameThread.join();
            } catch(InterruptedException e) {
                Log.e("ERR", "Joining Thread");
            }
        }

        public void resume() {
            playing = true;
            gameThread = new Thread(this);
            gameThread.start();
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN :
                    isMoving = !isMoving;
                    break;
            }

            return true;
        }
    }
}

Now, you have just to run the application on your Android emulator or on your real device and to enjoy your Running Man Game Animation.