Advanced Android 11.2: SurfaceView objects

1. Welcome

Introduction

When you create a custom view and override its onDraw() method, all drawing happens on the UI thread. Drawing on the UI thread puts an upper limit on how long or complex your drawing operations can be, because your app has to complete all its work for every screen refresh.

One option is to move some of the drawing work to a different thread using a SurfaceView.

  • All the views in your view hierarchy are rendered onto one Surface in the UI thread.
  • In the context of the Android framework, Surface refers to a lower-level drawing surface whose contents are eventually displayed on the user's screen.
  • A SurfaceView is a view in your view hierarchy that has its own separate Surface, as shown in the diagram below. You can draw to it in a separate thread.
  • To draw, start a thread, lock the SurfaceView's canvas, do your drawing, and post it to the Surface.

The following diagram shows a View Hierarchy with a Surface for the views and another separate Surface for the SurfaceView.

View Hierarchy with a Surface for the views and another separate Surface for the SurfaceView.

What you should already know

You should be able to:

  • Create a custom View.
  • Draw on and clip a Canvas.
  • Add event handlers to views.
  • Understand basic threading.

What you'll learn

  • How to use a SurfaceView to draw to the screen from a different thread.
  • A basic app architecture for simple games.

What you will do

  • Create an app that uses a SurfaceView to implement a simple game.

2. App overview

The SurfaceViewExample app lets you search for an Android image on a dark phone screen using a "flashlight."

  • At app startup, the user sees a black screen with a white circle, the "flashlight."
  • While the user drags their finger, the white circle follows the touch.
  • When the white circle intersects with the hidden Android image, the screen lights up to reveal the complete image and a "win" message.
  • When the user lifts their finger and touches the screen again, the screen turns black and the Android image is hidden in a new random location.

The following is a screenshot of the SurfaceViewExample app at startup, and after the user has found the Android image by moving around the flashlight.

Screenshot of SurfaceViewExample app at startup and after the user has found the Android image by moving around the flashlight.

Additional features:

  • Size of the flashlight is a ratio of the smallest screen dimension of the device.
  • Flashlight is not centered under the finger, so that the user can see what's inside the circle.

3. Task 1. Create the SurfaceViewExample app

You are going to build the SurfaceViewExample app from scratch. The app consists of the following three classes:

  • MainActivity—Locks screen orientation, gets the display size, creates the GameView, and sets the GameView as its content view. Overrides onPause() and onResume() to pause and resume the game thread along with the MainActivity.
  • FlashlightCone—Represents the cone of a flashlight with a radius that's proportional to the smaller screen dimension of the device. Has get methods for the location and size of the cone and a set method for the cone's location.
  • GameView—A custom SurfaceView where game play takes place. Responds to motion events on the screen. Draws the game screen in a separate thread, with the flashlight cone at the current position of the user's finger. Shows the "win" message when winning conditions are met.

1.1 Create an app

  1. Create an app using the Empty Activity template. Call the app SurfaceViewExample.
  2. Uncheck Generate Layout File. You do not need a layout file.

1.2 Create the FlashlightCone class

  1. Create a Java class called FlashlightCone.
 public class FlashlightCone {}
  1. Add member variables for x, y, and the radius.
 private int mX;
 private int mY;
 private int mRadius;
  1. Add methods to get values for x, y, and the radius. You do not need any methods to set them.
public int getX() {
    return mX;
}

public int getY() {
    return mY;
}

public int getRadius() {
       return mRadius;
}
  1. Add a constructor with integer parameters viewWidth and viewHeight.
  2. In the constructor, set mX and mY to position the circle at the center of the screen.
  3. Calculate the radius for the flashlight circle to be one third of the smaller screen dimension.
public FlashlightCone(int viewWidth, int viewHeight) {
   mX = viewWidth / 2;
   mY = viewHeight / 2;
   // Adjust the radius for the narrowest view dimension.
   mRadius = ((viewWidth <= viewHeight) ? mX / 3 : mY / 3);
}
  1. Add a public void update() method. The method takes integer parameters newX and newY, and it sets mX to newX and mY to newY.
public void update(int newX, int newY) {
   mX = newX;
   mY = newY;
}

1.3 Create a new SurfaceView class

  1. Create a new Java class and call it GameView.
  2. Let it extend SurfaceView and implement Runnable. Runnable adds a run() method to your class to run its operations on a separate thread.
public class GameView extends SurfaceView implements Runnable {}
  1. Implement methods to add a stub for the only required method, run().
@Override
public void run(){}
  1. Add the stubs for the constructors and have each constructor call init().
public GameView(Context context) {
   super(context);
   init(context);
}

public GameView(Context context, AttributeSet attrs) {
   super(context, attrs);
   init(context);
}

public GameView(Context context, AttributeSet attrs, int defStyleAttr) {
   super(context, attrs, defStyleAttr);
   init(context);
}
  1. Add private init() method and set the mContext member variable to context.
private void init(Context context) {
   mContext = context;
}
  1. In the GameView class, add stubs for the pause() and resume() methods. Later, you will manage your thread from these two methods.

1.4 Finish the MainActivity

  1. In MainActivity, create a member variable for the GameView class.
private GameView mGameView;

In the onCreate() method:

  1. Lock the screen orientation into landscape. Games often lock the screen orientation.
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
  1. Create an instance of GameView.
  2. Set mGameView to completely fill the screen.
  3. Set mGameView as the content view for MainActivity.
mGameView = new GameView(this);
// Android 4.1 and higher simple way to request fullscreen.
mGameView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
setContentView(mGameView);
  1. Still in MainActivity, override the onPause() method to also pause the mGameView object. This onPause() method shows an error, because you have not implemented the pause() method in the GameView class.
@Override
protected void onPause() {
   super.onPause();
   mGameView.pause();
}
  1. Override onResume() to resume the mGameView. The onResume() method shows an error, because you have not implemented the resume() method in the GameView.
@Override
protected void onResume() {
   super.onResume();
   mGameView.resume();
}

1.5 Finish the init() method for the GameView class

In the constructor for the GameView class:

  1. Assign the context to mContext.
  2. Get a persistent reference to the SurfaceHolder. Surfaces are created and destroyed by the system while the holder persists.
  3. Create a Paint object and initialize it.
  4. Create a Path to hold drawing instructions. If prompted, import android.graphics.Path.

Skateboarding Android image.

Here is the code for the init() method.

private void init(Context context) {
   mContext = context;
   mSurfaceHolder = getHolder();
   mPaint = new Paint();
   mPaint.setColor(Color.DKGRAY);
   mPath = new Path();
}
  1. After copy/pasting the code, define the missing member variables.

1.6 Add the setUpBitmap() method to the GameView class

The setUpBitmap() method calculates a random location on the screen for the Android image that the user has to find. You also need a way to calculate whether the user has found the bitmap.

  1. Set mBitmapX and mBitmapY to random x and y positions that fall inside the screen.
  2. Define a rectangular bounding box that contains the Android image.
  3. Define the missing member variables.
private void setUpBitmap() {
   mBitmapX = (int) Math.floor(
           Math.random() * (mViewWidth - mBitmap.getWidth()));
   mBitmapY = (int) Math.floor(
           Math.random() * (mViewHeight - mBitmap.getHeight()));
   mWinnerRect = new RectF(mBitmapX, mBitmapY,
           mBitmapX + mBitmap.getWidth(),
           mBitmapY + mBitmap.getHeight());
}

1.7 Implement the methods to pause and resume the GameView class

The pause() and resume() methods on the GameView are called from the MainActivity when it is paused or resumed. When the MainActivity pauses, you need to stop the GameView thread. When the MainActivity resumes, you need to create a new GameView thread.

  1. Add the pause() and resume() methods using the code below. The mRunning member variable tracks the thread status, so that you do not try to draw when the activity is not running anymore.
public void pause() {
   mRunning = false;
   try {
       // Stop the thread (rejoin the main thread)
       mGameThread.join();
   } catch (InterruptedException e) {
   }
}

public void resume() {
   mRunning = true;
   mGameThread = new Thread(this);
   mGameThread.start();
}
  1. As before, add the missing member variables.

Thread management can become a lot more complex after you have multiple threads in your game. See Sending Operations to Multiple Threads for lessons in thread management.

1.8 Implement the onSizeChanged() method

There are several ways in which to set up the view after the system has fully initialized the view. The onSizeChangedMethod() is called every time the view changes.The view starts out with 0 dimensions. When the view is first inflated, its size changes and onSizeChangedMethod() is called. Unlike in onCreate(), the view's correct dimensions are available.

  1. Get the image of Android on a skateboard from github and add it to your drawable folder, or use a small image of your own choice.
  2. In GameView, override the onSizeChanged() method. Both the new and the old view dimensions are passed as parameters as shown below.
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
   super.onSizeChanged(w, h, oldw, oldh);
}

Inside the onSizeChanged() method:

  1. Store the width and height in member variables mViewWidth and mViewHeight.
mViewWidth = w;
mViewHeight = h;
  1. Create a FlashlightCone and pass in mViewWidth and mViewHeight.
mFlashlightCone = new FlashlightCone(mViewWidth, mViewHeight);
  1. Set the font size proportional to the view height.
mPaint.setTextSize(mViewHeight / 5);
  1. Create a Bitmap and call setupBitmap().
mBitmap = BitmapFactory.decodeResource(
        mContext.getResources(), R.drawable.android);
setUpBitmap();

1.9 Implement the run() method in the GameView class

The interesting stuff, such as drawing and screen refresh synchronization, happens in the run() method. Inside your run() method stub, do the following:

  1. Declare a Canvas canvas variable at the top of the run() method:
Canvas canvas;
  1. Create a loop that only runs while mRunning is true. All the following code must be inside that loop.
while (mRunning) {

}
  1. Check whether there is a valid Surface available for drawing. If not, do nothing.
 if (mSurfaceHolder.getSurface().isValid()) {

All code that follows must be inside this if statement.

  1. Because you will use the flashlight cone coordinates and radius multiple times, create local helper variables inside the if statement.
int x = mFlashlightCone.getX();
int y = mFlashlightCone.getY();
int radius = mFlashlightCone.getRadius();
  1. Lock the canvas.

In an app, with more threads, you must enclose this code in a

try/catch

block to make sure only one thread is trying to write to the

Surface

at a time.

canvas = mSurfaceHolder.lockCanvas();
  1. Save the current canvas state.
canvas.save();
  1. Fill the canvas with white color.
canvas.drawColor(Color.WHITE);
  1. Draw the Skateboarding Android bitmap on the canvas.
 canvas.drawBitmap(mBitmap, mBitmapX, mBitmapY, mPaint);
  1. Add a circle that is the size of the flashlight cone to mPath.
mPath.addCircle(x, y, radius, Path.Direction.CCW);
  1. Set the circle as the clipping path using the DIFFERENCE operator, so that's what's inside the circle is clipped (not drawn).
// The method clipPath(path, Region.Op.DIFFERENCE) was
// deprecated in API level 26. The recommended alternative
// method is clipOutPath(Path), which is currently available
// in API level 26 and higher.
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
   canvas.clipPath(mPath, Region.Op.DIFFERENCE);
} else {
   canvas.clipOutPath(mPath);
}
  1. Fill everything outside of the circle with black.
canvas.drawColor(Color.BLACK);
  1. Check whether the the center of the flashlight circle is inside the winning rectangle. If so, color the canvas white, redraw the Android image, and draw the winning message.
if (x > mWinnerRect.left && x < mWinnerRect.right
       && y > mWinnerRect.top && y < mWinnerRect.bottom) {
   canvas.drawColor(Color.WHITE);
   canvas.drawBitmap(mBitmap, mBitmapX, mBitmapY, mPaint);
   canvas.drawText(
           "WIN!", mViewWidth / 3, mViewHeight / 2, mPaint);
}
  1. Drawing is finished, so you need to rewind the path, restore the canvas, and release the lock on the canvas.
mPath.rewind();
canvas.restore();
mSurfaceHolder.unlockCanvasAndPost(canvas);
  1. Run your app. It should display a black screen with a white circle at the center of the screen.

1.10 Respond to motion events

For the game to work, your app needs to detect and respond to the user's motions on the screen.

  1. In GameView, override the onTouchEvent() method and update the flashlight position on the ACTION_DOWN and ACTION_MOVE events.
@Override
public boolean onTouchEvent(MotionEvent event) {
   float x = event.getX();
   float y = event.getY();

   // Invalidate() is inside the case statements because there are
   // many other motion events, and we don't want to invalidate
   // the view for those.
   switch (event.getAction()) {
       case MotionEvent.ACTION_DOWN:
           setUpBitmap();
           updateFrame((int) x, (int) y);
           invalidate();
           break;
       case MotionEvent.ACTION_MOVE:
           updateFrame((int) x, (int) y);
           invalidate();
           break;
       default:
           // Do nothing.
   }
   return true;
}
  1. Implement the updateFrame() method called in onTouchEvent() to set the new coordinates of the FlashlightCone.
private void updateFrame(int newX, int newY) {
   mFlashlightCone.update(newX, newY);
}
  1. Run your app and GAME ON!
  2. After you win, tap the screen to play again.

4. Solution code

Android Studio project: SurfaceViewExample

5. Summary

  • To offload drawing to a different thread, create a custom view that extends SurfaceView and implements Runnable. The SurfaceView is part of your view hierarchy but has a drawing Surface that is separate from the rest of the view hierarchy.
  • Create an instance of your custom view and set it as the content view of your activity.
  • Add pause() and resume() methods to the SurfaceView that stop and start a thread.
  • Override onPause() and onResume() in the activity to call the pause() and resume() methods of the SurfaceView.
  • If appropriate, handle touch events, for example, by overriding onTouchEvent().
  • Add code to update your data.
  • In the SurfaceView, implement the run() method to:
  • Check whether a Surface is available.
  • Lock the canvas.
  • Draw.
  • Unlock the canvas and post to the Surface.

6. Learn more

The related concept documentation is in 11.2 The SurfaceView class.

Android developer documentation:

7. Homework

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

  • Assign homework if required.
  • Communicate to students how to submit homework assignments.
  • Grade the homework assignments.

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Build and run an app

Implement the same MemoryGame app that you created in the 11.1 homework, but use a SurfaceView object.

The MemoryGame app hides and reveals "cards" as the user taps on the screen. Use clipping to implement the hide/reveal effect.

  • You can use simple colored squares or shapes for the "cards."
  • If the user reveals two matching cards, show a congratulatory toast. If the user reveals two cards that don't match, show an encouraging message telling them to tap to continue.
  • Click handling: On the first tap, show the first card. On the second tap, show the second card and display a message. On the next tap, restart.

Answer these questions

Question 1

What is a SurfaceView?

  • A view in your app's view hierarchy that has its own separate surface.
  • A view that directly accesses a lower-level drawing surface.
  • A view that is not part of the view hierarchy.
  • A view that can be drawn to from a separate thread.

Question 2

What is the most distinguishing benefit of using a SurfaceView?

  • A SurfaceView can make an app more responsive to user input.
  • You can move drawing operations away from the UI thread.
  • Your animations may run more smoothly.

Question 3

When should you consider using a SurfaceView? Select up to three.

  • When your app does a lot of drawing, or does complex drawing.
  • When your app combines complex graphics with user interaction.
  • When your app uses a lot of images as backgrounds.
  • When your app stutters, and moving drawing off the UI thread could improve performance.

Submit your app for grading

Guidance for graders

Check that the app has the following features:

  • When the user taps, the app reveals a "card."
  • When the user taps again, the app reveals a second "card" and shows a toast congratulating or encouraging the user.
  • On the third tap, the game restarts.
  • The code uses a SurfaceView object, and a separate thread for drawing.

8. Next codelab

To see all the codelabs in the Advanced Android Development training course, visit the Advanced Android Development codelabs landing page.