11.2: Creating a SurfaceView object

Contents:

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 <code>Surface</code> for the views and another separate <code>Surface</code> for the <code>SurfaceView</code>.

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 will LEARN

You will learn how to:

  • 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.

App overview

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

  1. At app startup, the user sees a black screen with a white circle, the "flashlight."
  2. While the user drags their finger, the white circle follows the touch.
  3. When the white circle intersects with the hidden Android image, the screen lights up to reveal the complete image and a "win" message.
  4. 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.

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 with an empty activity

  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 {}
    
  2. Add member variables for x, y, and the radius.
      private int mX;
      private int mY;
      private int mRadius;
    
  3. 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;
     }
    
  4. Add a constructor with integer parameters viewWidth and viewHeight.
  5. In the constructor, set mX and mY to position the circle at the center of the screen.
  6. 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);
     }
    
  7. 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 {}
    
  3. Implement methods to add a stub for the only required method, run().
     @Override
     public void run(){}
    
  4. 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);
     }
    
  5. Add private init() method and set the mContext member variable to context.
     private void init(Context context) {
        mContext = context;
     }
    
  6. 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:

  2. Lock the screen orientation into landscape. Games often lock the screen orientation.

     setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
    
  3. Create an instance of GameView.
  4. Set mGameView to completely fill the screen.
  5. 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);
    
  6. 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();
     }
    
  7. 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();
    }
    
  5. 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();
     }
    
  2. 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:

  3. Store the width and height in member variables mViewWidth and mViewHeight.

     mViewWidth = w;
     mViewHeight = h;
    
  4. Create a FlashlightCone and pass in mViewWidth and mViewHeight.
     mFlashlightCone = new FlashlightCone(mViewWidth, mViewHeight);
    
  5. Set the font size proportional to the view height.
     mPaint.setTextSize(mViewHeight / 5);
    
  6. 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;
    
  2. Create a loop that only runs while mRunning is true. All the following code must be inside that loop.

     while (mRunning) {
    
     }
    
  3. 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.

  4. 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();
    
  5. Lock the canvas.

    In an app, with more threads, you must enclose this with a try/catch block to make sure only one thread is trying to write to the Surface.
canvas = mSurfaceHolder.lockCanvas();
  1. Save the current canvas state.
     canvas.save();
    
  2. Fill the canvas with white color.
     canvas.drawColor(Color.WHITE);
    
  3. Draw the Skateboarding Android bitmap on the canvas.
      canvas.drawBitmap(mBitmap, mBitmapX, mBitmapY, mPaint);
    
  4. Add a circle that is the size of the flashlight cone to mPath.
     mPath.addCircle(x, y, radius, Path.Direction.CCW);
    
  5. Set the circle as the clipping path using the DIFFERENCE operator, so that's what's inside the circle is clipped (not drawn).
    canvas.clipPath(mPath, Region.Op.DIFFERENCE);
    
  6. Fill everything outside of the circle with black.
    canvas.drawColor(Color.BLACK);
    
  7. 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);
    }
    
  8. 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);
    
    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;
     }
    
  2. 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);
     }
    
  3. Run your app and GAME ON!
  4. After you win, tap the screen to play again.

Solution code

Android Studio project: SurfaceViewExample

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.

The related concept documentation is in The SurfaceView class.

Learn more

Android developer documentation:

results matching ""

    No results matching ""