Android Logo MathCS.org - Android

Using a SurfaceView

java -> android -> drawing with View ...

In the previous section we created a custom drawing with animation using a View to contain the drawing and a Thread to do the animation. But that approach turns out to be too slow for high frame-rate, smooth animations. Instead, we need to directly call the onDraw method to regenerate the drawing instead of relying on postInvalidate to eventually get around to calling onDraw after taking care of everything else.

A SurfaceView is the class to use instead of a View for smooth animation. It is a little more tricky, but will allow for high frame rates. The first change to our code - see the code listing from the last section - is obvious: instead of extending View, our GameArena needs to extend SurfaceView:

public class GameArena extends SurfaceView
{ 
   ...
}

Unfortunately, while this will compile just fine, nothing will show on the screen. The thread may be busy moving the circles around but nothing will be visible. So, more changes are necessary. First, we not only extend SurfaceView but we also implement the SurfaceHolder.Callback interface. This means we need to implement three additional methods:

We also need to link the surface to these callback methods in the constructor. Here is our new GameArena class, with the changed code in bold:

public class GameArena extends SurfaceView implements SurfaceHolder.Callback
{
	private Context context;
	private MyCircle circle = null;
	private GameThread thread = null;

	public GameArena(Context context)
	{
		super(context);
		this.context = context;

		circle = new MyCircle(100, 150);

		getHolder().addCallback(this);
	}

	public void startGame()
	{
		if (thread == null)
		{
			thread = new GameThread(this);
			thread.startThread();
		}
	}

	public void stopGame()
	{
		if (thread != null)
		{
			thread.stopThread();

			// Waiting for the thread to die by calling thread.join,
			// repeatedly if necessary
			boolean retry = true;
			while (retry)
			{
				try
				{
					thread.join();
					retry = false;
				} 
				catch (InterruptedException e)
				{
				}
			}
			thread = null;
		}
	}

	public void moveCircle()
	{
		circle.checkBounds(getWidth(), getHeight());
		circle.move();
	}

	public void onDraw(Canvas canvas) 
	{
		canvas.drawColor(Color.WHITE);
		circle.draw(canvas);
	}

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

	public void surfaceCreated(SurfaceHolder holder)
	{
		startGame();
	}

	public void surfaceDestroyed(SurfaceHolder holder)
	{
		stopGame();
	}
}

In other words, we link up the callback functions in the constructor, then call startGame when the surface is created, and stopGame when the surface is destroyed (for example when our app moves into the background).

Our thread class also needs to change by replacing the call to postInvalidate() by something else. The trick is to obtain a reference to the canvas containing the surface to draw on, and make sure that this happens in a thread-safe manner. Here is the code of the new GameThread class, with the changes in bold:

public class GameThread extends Thread
{
	private final static int SLEEP_TIME = 40;

	private boolean running = false;
	private GameArena canvas = null;
	private SurfaceHolder surfaceHolder = null;

	public GameThread(GameArena canvas)
	{
		super();
		this.canvas = canvas;
		this.surfaceHolder = canvas.getHolder();
	}

	public void startThread()
	{
		running = true;
		super.start();
	}

	public void stopThread()
	{
		running = false;
	}

	public void run()
	{
		Canvas c = null;
		while (running)
		{
			c = null;
			try
			{
				c = surfaceHolder.lockCanvas();
				synchronized (surfaceHolder)
				{
					if (c != null)
					{
						canvas.moveCircle();
						canvas.onDraw(c);
					}
				}
				sleep(SLEEP_TIME);
			}
			catch(InterruptedException ie)
			{ 
			}
			finally 
			{
				// do this in a finally so that if an exception is thrown
				// we don't leave the Surface in an inconsistent state
				if (c != null) 
				{
					surfaceHolder.unlockCanvasAndPost(c);
				}
			}
		}
	}
}

The changed method is of course the run method. It uses the lockCanvas method to get a reference to the drawing canvas, prevent any other thread to gain access to it via the synchronized directive, and calls on the onDraw method of the underlying surface, with the proper canvas c as input.

We can now remove the calls to startGame and stopGame in the onPause and onResume methods of our main activity, since the thread is started and stopped now when the drawing surface is ready via the callback methods.

The circles should now animate fine. Note in particular that we do not need to change the MyCircle class at all. It does not care who draws when, but it simply determines, as before, how to draw and that did not change.

Now we are ready to add some more fun to our game: instead of drawing just one circle, we now want to add a new animated circle to our app when ever the user taps the screen. The idea is as follows:

This is not too bad to implement but it will create a synchronization problem. We will provide the details soon ...