SurfaceView in Kotlin XML: Custom Drawing and Game Development for Android

When it comes to custom drawing and game development in Android, SurfaceView provides a robust solution that offers a dedicated drawing surface. This component is especially useful for applications needing high performance and direct control over the screen’s pixels. Although modern Android development favors Jetpack Compose, understanding SurfaceView within Kotlin XML environments remains valuable, especially when maintaining or optimizing existing projects.

What is SurfaceView?

SurfaceView is a View component that includes an embedded surface. This surface is a dedicated piece of memory within the Android system specifically for drawing. Unlike regular views that draw on the UI thread, SurfaceView allows drawing to occur on a separate thread. This avoids blocking the main UI thread, resulting in smoother graphics and animations.

Why Use SurfaceView?

  • Performance: Drawing on a separate thread enhances performance, crucial for games and complex visualizations.
  • Direct Control: Provides direct access to the screen’s pixels via the Surface object.
  • Compatibility: Remains relevant for legacy projects and situations where finer control is needed.

Implementing SurfaceView in Kotlin XML

Implementing SurfaceView involves creating a custom view extending SurfaceView and implementing the SurfaceHolder.Callback interface. This callback allows you to manage the surface lifecycle.

Step 1: Add SurfaceView to XML Layout

First, add the SurfaceView element to your XML layout file (e.g., activity_main.xml):


<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.surfaceviewexample.MySurfaceView
        android:id="@+id/mySurfaceView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Step 2: Create a Custom SurfaceView Class

Create a custom class that extends SurfaceView and implements SurfaceHolder.Callback. This class will handle surface creation, changes, and destruction, as well as the drawing logic.


import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.SurfaceHolder
import android.view.SurfaceView

class MySurfaceView(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs), SurfaceHolder.Callback {

    private var drawingThread: DrawingThread? = null
    private val surfaceHolder: SurfaceHolder = holder.apply {
        addCallback(this@MySurfaceView)
    }

    // Paint object for drawing
    private val paint = Paint().apply {
        color = Color.RED
        style = Paint.Style.FILL
    }

    init {
        isFocusable = true
        isFocusableInTouchMode = true
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        drawingThread = DrawingThread(holder).apply {
            isRunning = true
            start()
        }
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
        // Handle surface changes, such as resizing
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        drawingThread?.let {
            it.isRunning = false
            try {
                it.join()
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }
        }
        drawingThread = null
    }

    inner class DrawingThread(private val surfaceHolder: SurfaceHolder) : Thread() {
        var isRunning = false

        override fun run() {
            while (isRunning) {
                var canvas: Canvas? = null
                try {
                    canvas = surfaceHolder.lockCanvas()
                    synchronized(surfaceHolder) {
                        // Draw on the canvas
                        canvas?.drawColor(Color.BLACK)  // Clear the canvas
                        canvas?.drawCircle(width / 2f, height / 2f, 50f, paint) // Draw a red circle
                    }
                } catch (e: Exception) {
                    e.printStackTrace()
                } finally {
                    canvas?.let {
                        try {
                            surfaceHolder.unlockCanvasAndPost(it)
                        } catch (e: Exception) {
                            e.printStackTrace()
                        }
                    }
                }

                try {
                    sleep(10) // Control frame rate
                } catch (e: InterruptedException) {
                    e.printStackTrace()
                }
            }
        }
    }
}

Explanation:

  • Constructor: Takes the context and attributes (if defined in XML).
  • SurfaceHolder.Callback: Implements the necessary callback methods (surfaceCreated, surfaceChanged, surfaceDestroyed).
  • DrawingThread: An inner class extending Thread, responsible for continuously drawing on the surface.
  • Drawing Logic: Inside the run method of DrawingThread, drawing operations are performed. The canvas is locked, drawing operations are executed, and the canvas is then unlocked and posted.
  • Initialization: Sets the focus to enable touch events, if needed.
  • Paint Object: A Paint object is created to specify the color and style of drawing.

Step 3: Use the Custom SurfaceView in Activity/Fragment

In your Activity or Fragment, reference the custom SurfaceView from your XML layout.


import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Important Considerations

  • Thread Management: Proper thread management is critical to avoid memory leaks and crashes. Ensure that the drawing thread is stopped correctly when the surface is destroyed.
  • Synchronization: Accessing the SurfaceHolder and Canvas should be synchronized to prevent race conditions.
  • Frame Rate: Controlling the frame rate (e.g., via Thread.sleep()) is important to manage CPU usage and ensure smooth rendering.

Drawing with SurfaceHolder and Canvas

The core of using SurfaceView lies in manipulating the Canvas object obtained from the SurfaceHolder.


    inner class DrawingThread(private val surfaceHolder: SurfaceHolder) : Thread() {
        var isRunning = false

        override fun run() {
            while (isRunning) {
                var canvas: Canvas? = null
                try {
                    canvas = surfaceHolder.lockCanvas()
                    synchronized(surfaceHolder) {
                        // Draw on the canvas
                        canvas?.drawColor(Color.BLACK)  // Clear the canvas
                        canvas?.drawCircle(width / 2f, height / 2f, 50f, paint) // Draw a red circle
                    }
                } catch (e: Exception) {
                    e.printStackTrace()
                } finally {
                    canvas?.let {
                        try {
                            surfaceHolder.unlockCanvasAndPost(it)
                        } catch (e: Exception) {
                            e.printStackTrace()
                        }
                    }
                }

                try {
                    sleep(10) // Control frame rate
                } catch (e: InterruptedException) {
                    e.printStackTrace()
                }
            }
        }
    }
  • Locking the Canvas: surfaceHolder.lockCanvas() acquires the canvas for drawing.
  • Drawing Operations: Use canvas.draw...() methods to draw shapes, text, and images. Be sure to specify paint settings like color, style and stroke width.
  • Unlocking and Posting: surfaceHolder.unlockCanvasAndPost(canvas) releases the canvas, making the changes visible.

Benefits and Limitations

Benefits:

  • High Performance: Ideal for drawing-intensive applications.
  • Direct Access: Offers low-level control over screen rendering.

Limitations:

  • Complexity: Requires careful thread management and synchronization.
  • Verbose Code: Generally involves more boilerplate code than newer UI solutions.

Conclusion

SurfaceView is a powerful tool for custom drawing and game development on Android when using XML layouts. While it demands careful handling, the performance gains and direct control it provides can be invaluable, particularly in applications needing optimized graphics or when working with existing projects. Understanding its implementation in Kotlin can enhance your ability to develop and maintain performant visual applications on Android.