There are two basic ways of drawing 2D objects in Android. Drawing to a View, as described in the last tutorial, is the best option only when your object is static. If your object is moving, or you otherwise regularly need to redraw, you’re better off with a Canvas. Read on for more on drawing with a Canvas, and using a secondary thread to do so, for best user responsiveness.
Drawing with a Canvas
For a slowly-animating application, you can use a custom View as in the previous tutorial. The View’s Canvas is accessible via the onDraw()
method, so you can use that to draw directly to the Canvas.
The other way to get a Canvas is by managing a SurfaceView in a separate thread. This is recommended for fast-moving applications such as games. Unlike with a normal View, a SurfaceView can be drawn on by a background thread, which means it can be updated more regularly. Another situation you might want to use this in is for something like a camera preview window, which again updates regularly. We’ll look first at just managing a SurfaceView directly, then at how to add a second thread to access it in the background.
First create a new class which extends SurfaceView and implements SurfaceHolder.Callback:
public class BubbleSurfaceView extends SurfaceView implements SurfaceHolder.Callback { private SurfaceHolder sh; private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); public BubbleSurfaceView(Context context) { super(context); sh = getHolder(); sh.addCallback(this); paint.setColor(Color.BLUE); paint.setStyle(Style.FILL); } public void surfaceCreated(SurfaceHolder holder) { Canvas canvas = sh.lockCanvas(); canvas.drawColor(Color.BLACK); canvas.drawCircle(100, 200, 50, paint); sh.unlockCanvasAndPost(canvas); } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } public void surfaceDestroyed(SurfaceHolder holder) { } }
We set up the SurfaceView and get a SurfaceHolder, then add a SurfaceHolder.Callback. This means that the View will be informed of any changes to the surface; specifically, if it is created, changed, or destroyed. The surfaceCreated()
, surfaceChanged()
, and surfaceDestroyed()
methods will be called on those events. We also set up a Paint object that we’ll use to draw our bubble.
surfaceCreated()
will be called when the surface is first created. Right now all we have is a static circle, so we can draw that here. This is a good place to draw any static background items. (We’ll ignore surfaceChanged()
and surfaceDestroyed()
for now.)
SurfaceHolder.lockCanvas()
gives us a Canvas to draw to; drawColor()
sets the whole thing to black, and drawCircle()
does what it says on the tin. However, none of this will actually be shown to the user until you call SurfaceHolder.
. This shows the current state of the Canvas on the screen.
To call this from the main Activity is easy:
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new BubbleSurfaceView(this)); }
Again, at present we’re not using a separate thread; it’s all running in the same thread. Compile this and run it on your phone or on the emulator, and you should see a nice blue circle.
Two Threads
OK, now let’s set up our Canvas to be drawn in a separate thread. This is best practice if writing games and other fast-drawing activities, so we’ll also add a little motion to our bubble, to make the threading worthwhile.
Create a new BubbleThread class inside BubbleView:
class BubbleThread extends Thread { private int canvasWidth = 200; private int canvasHeight = 400; private static final int SPEED = 2; private boolean run = false; private float bubbleX; private float bubbleY; private float headingX; private float headingY; public BubbleThread(SurfaceHolder surfaceHolder, Context context, Handler handler) { sh = surfaceHolder; handler = handler; ctx = context; } public void doStart() { synchronized (sh) { // Start bubble in centre and create some random motion bubbleX = canvasWidth / 2; bubbleY = canvasHeight / 2; headingX = (float) (-1 + (Math.random() * 2)); headingY = (float) (-1 + (Math.random() * 2)); } } public void run() { while (run) { Canvas c = null; try { c = sh.lockCanvas(null); synchronized (sh) { doDraw(c); } } finally { if (c != null) { sh.unlockCanvasAndPost(c); } } } } public void setRunning(boolean b) { run = b; } public void setSurfaceSize(int width, int height) { synchronized (sh) { canvasWidth = width; canvasHeight = height; doStart(); } } private void doDraw(Canvas canvas) { bubbleX = bubbleX + (headingX * SPEED); bubbleY = bubbleY + (headingY * SPEED); canvas.restore(); canvas.drawColor(Color.BLACK); canvas.drawCircle(bubbleX, bubbleY, 50, paint); } }
doStart()
sets up the initial position of the bubble, and a couple of random motion values. setRunning()
is used to help manage the thread and check whether or not it is running.setSurfaceSize()
will be called when the surface is changed, to set the canvas size correctly. In run()
, we lock the canvas, synchronize the thread, make the changes in doDraw()
(which moves and redraws the bubble), then unlock the canvas to draw to it. As this is in the finally
block, it will only happen if no exception was thrown.
We now need to set up the BubbleThread from BubbleView:
public class BubbleSurfaceView extends SurfaceView implements SurfaceHolder.Callback { // Variables go here BubbleThread thread; public BubbleSurfaceView(Context context) { // Constructor as before, with additional lines: ctx = context; setFocusable(true); // make sure we get key events } public BubbleThread getThread() { return thread; } public void surfaceCreated(SurfaceHolder holder) { thread = new BubbleThread(sh, ctx, new Handler()); thread.setRunning(true); thread.start(); } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { thread.setSurfaceSize(width, height); } public void surfaceDestroyed(SurfaceHolder holder) { boolean retry = true; thread.setRunning(false); while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { } } } }
Now in surfaceCreated()
, we create a new thread, set it as running, then start it. We stop and rejoin it again in surfaceDestroyed()
, too. Note that we’ve handled pausing very simply in this code: the thread is destroyed when the user exits the app, and recreated brand new from scratch when they restart it. In a game you would probably want to do something to save the state and pause/resume the thread as appropriate.
Every time you draw to the canvas, you need to pass your SurfaceHandler into the thread, and then go through the lockCanvas()
, draw, unlockCanvasAndPost()
cycle as before. Again, you need to repaint the whole surface each time. If you comment the canvas.drawColor()
line out, you’ll see the trace of all previous drawings left behind as the bubble moves. Run this code and you should see your bubble drifting randomly round the screen.
Important note! The Android UI is not thread-safe, so UI changes must be done either in the main thread, or, as here, via a handler in the main thread which is then sent messages by other threads.
In the next article, we’ll look at handling touch events, and drawing on the Canvas in response to them.