SurfaceView
Game Development
Thread Strategy
Android Programming
Game Loop

Programming with SurfaceView and thread strategy for game development

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

SurfaceView is an Android view that provides a dedicated drawing surface rendered on a separate thread from the UI thread. This makes it ideal for game development where you need to draw frames at 30-60 fps without blocking the main thread. The standard pattern is a game loop running on a dedicated thread that acquires the Canvas from the SurfaceHolder, performs all drawing operations, and then posts the frame. Understanding the SurfaceHolder.Callback lifecycle and frame timing is essential for building smooth Android games.

Basic SurfaceView Game Structure

java
1public class GameView extends SurfaceView implements SurfaceHolder.Callback {
2
3    private GameThread gameThread;
4    private boolean isRunning = false;
5
6    public GameView(Context context) {
7        super(context);
8        getHolder().addCallback(this);
9        setFocusable(true);
10    }
11
12    @Override
13    public void surfaceCreated(SurfaceHolder holder) {
14        isRunning = true;
15        gameThread = new GameThread(holder, this);
16        gameThread.start();
17    }
18
19    @Override
20    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
21        // Handle orientation or size changes
22    }
23
24    @Override
25    public void surfaceDestroyed(SurfaceHolder holder) {
26        isRunning = false;
27        boolean retry = true;
28        while (retry) {
29            try {
30                gameThread.join();
31                retry = false;
32            } catch (InterruptedException e) {
33                // Keep trying to join
34            }
35        }
36    }
37
38    public void update() {
39        // Update game state (positions, collisions, physics)
40    }
41
42    @Override
43    public void draw(Canvas canvas) {
44        super.draw(canvas);
45        if (canvas != null) {
46            canvas.drawColor(Color.BLACK);  // Clear screen
47            // Draw game objects
48        }
49    }
50}

The Game Thread

java
1public class GameThread extends Thread {
2
3    private static final int TARGET_FPS = 60;
4    private static final long FRAME_TIME = 1000 / TARGET_FPS;  // ~16ms per frame
5
6    private final SurfaceHolder surfaceHolder;
7    private final GameView gameView;
8
9    public GameThread(SurfaceHolder holder, GameView view) {
10        this.surfaceHolder = holder;
11        this.gameView = view;
12    }
13
14    @Override
15    public void run() {
16        while (gameView.isRunning) {
17            long startTime = System.currentTimeMillis();
18            Canvas canvas = null;
19
20            try {
21                canvas = surfaceHolder.lockCanvas();
22                synchronized (surfaceHolder) {
23                    gameView.update();      // Update game logic
24                    gameView.draw(canvas);  // Render frame
25                }
26            } finally {
27                if (canvas != null) {
28                    surfaceHolder.unlockCanvasAndPost(canvas);
29                }
30            }
31
32            // Frame timing — sleep to maintain target FPS
33            long elapsed = System.currentTimeMillis() - startTime;
34            long sleepTime = FRAME_TIME - elapsed;
35            if (sleepTime > 0) {
36                try {
37                    Thread.sleep(sleepTime);
38                } catch (InterruptedException e) {
39                    Thread.currentThread().interrupt();
40                }
41            }
42        }
43    }
44}

Delta Time for Smooth Movement

Fixed-FPS timing causes stuttering when frames are dropped. Use delta time to make movement frame-rate independent.

java
1@Override
2public void run() {
3    long previousTime = System.nanoTime();
4
5    while (gameView.isRunning) {
6        long currentTime = System.nanoTime();
7        float deltaTime = (currentTime - previousTime) / 1_000_000_000f;  // Seconds
8        previousTime = currentTime;
9
10        Canvas canvas = null;
11        try {
12            canvas = surfaceHolder.lockCanvas();
13            synchronized (surfaceHolder) {
14                gameView.update(deltaTime);
15                gameView.draw(canvas);
16            }
17        } finally {
18            if (canvas != null) {
19                surfaceHolder.unlockCanvasAndPost(canvas);
20            }
21        }
22    }
23}
24
25// In GameView
26public void update(float deltaTime) {
27    // Move 200 pixels per second regardless of frame rate
28    playerX += playerSpeedX * deltaTime;
29    playerY += playerSpeedY * deltaTime;
30}

Handling Touch Input

java
1@Override
2public boolean onTouchEvent(MotionEvent event) {
3    // Touch events come from the UI thread
4    // Synchronize access to shared game state
5    synchronized (getHolder()) {
6        switch (event.getAction()) {
7            case MotionEvent.ACTION_DOWN:
8                playerTargetX = event.getX();
9                playerTargetY = event.getY();
10                break;
11            case MotionEvent.ACTION_MOVE:
12                playerTargetX = event.getX();
13                playerTargetY = event.getY();
14                break;
15            case MotionEvent.ACTION_UP:
16                // Handle touch release
17                break;
18        }
19    }
20    return true;
21}

Kotlin Version

kotlin
1class GameView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
2
3    private var gameThread: Thread? = null
4    @Volatile var isRunning = false
5
6    init {
7        holder.addCallback(this)
8        isFocusable = true
9    }
10
11    override fun surfaceCreated(holder: SurfaceHolder) {
12        isRunning = true
13        gameThread = Thread {
14            var previousTime = System.nanoTime()
15            while (isRunning) {
16                val currentTime = System.nanoTime()
17                val dt = (currentTime - previousTime) / 1_000_000_000f
18                previousTime = currentTime
19
20                val canvas = holder.lockCanvas() ?: continue
21                try {
22                    synchronized(holder) {
23                        update(dt)
24                        drawFrame(canvas)
25                    }
26                } finally {
27                    holder.unlockCanvasAndPost(canvas)
28                }
29            }
30        }.also { it.start() }
31    }
32
33    override fun surfaceDestroyed(holder: SurfaceHolder) {
34        isRunning = false
35        gameThread?.join()
36    }
37
38    override fun surfaceChanged(holder: SurfaceHolder, fmt: Int, w: Int, h: Int) {}
39}

Common Pitfalls

  • Not joining the thread in surfaceDestroyed: If the game thread is still running when the surface is destroyed, lockCanvas() returns null and the app crashes. Always set isRunning = false and call gameThread.join() in surfaceDestroyed to ensure the thread stops before the surface is reclaimed.
  • Drawing on a null Canvas: lockCanvas() can return null if the surface is not ready or is being destroyed. Always check canvas != null before drawing. Using a try/finally block ensures unlockCanvasAndPost is called even if drawing throws an exception.
  • Object allocation in the game loop: Creating objects (e.g., new Paint(), new Rect()) inside the game loop triggers garbage collection, causing frame drops and stuttering. Allocate reusable objects in the constructor and reuse them every frame.
  • Not using synchronized for shared state: Touch events arrive on the UI thread while the game loop runs on its own thread. Reading/writing shared variables (player position, game state) without synchronization causes race conditions and visual glitches. Synchronize on surfaceHolder for both threads.
  • Fixed timestep without delta time: Using Thread.sleep(16) for 60 FPS assumes each frame takes exactly 16ms. When a frame takes longer (e.g., during collision detection), movement stutters. Use delta time to decouple game logic speed from frame rate.

Summary

  • SurfaceView provides a separate drawing surface that can be rendered from a background thread
  • Implement SurfaceHolder.Callback to manage the surface lifecycle (created, changed, destroyed)
  • Use a dedicated game thread with lockCanvas() / unlockCanvasAndPost() for the render loop
  • Use delta time (elapsed seconds) instead of fixed-step timing for smooth, frame-rate-independent movement
  • Synchronize access to shared game state between the game thread and the UI thread (touch events)

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.