Appendix B

Canvas, Paint & SurfaceView Quick Reference

The Android drawing and game APIs used throughout the book, with the chapter where each first appeared.

Paint — the "brush" (Ch1)

val paint = Paint()
paint.color = Color.RED
paint.style = Paint.Style.FILL        // or Paint.Style.STROKE (outline)
paint.strokeWidth = 8f                // line thickness when STROKE
paint.textSize = 70f                  // text height for drawText
paint.alpha = 128                     // 0 (clear) to 255 (solid)
paint.isAntiAlias = true              // smooth edges (good for shapes/text)
paint.isFilterBitmap = false          // OFF = crisp pixel-art scaling (Ch22)

Color (Ch1, Ch22)

Color.RED, Color.WHITE, Color.BLACK, Color.CYAN, Color.DKGRAY  // constants
Color.rgb(255, 140, 0)                // red, green, blue (0..255)
Color.parseColor("#FF8800")           // hex string

Canvas — the "paper" (Ch1)

canvas.drawColor(Color.DKGRAY)                       // fill whole screen
canvas.drawRect(left, top, right, bottom, paint)     // rectangle
canvas.drawRoundRect(l, t, r, b, rx, ry, paint)      // rounded corners
canvas.drawCircle(centerX, centerY, radius, paint)   // circle
canvas.drawText("Score", x, y, paint)                // text (y = baseline)
canvas.drawBitmap(bitmap, srcRect, dstRectF, paint)  // image / sprite (Ch22)

// Temporary transforms, e.g. screen shake (Ch30):
canvas.save()
canvas.translate(dx, dy)
// ...draw...
canvas.restore()

Coordinates: (0, 0) is the top-left. X grows right, Y grows down.

Custom View + simple game loop (Ch1, Ch3)

class MyView(context: Context) : View(context) {
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // ...draw...
        invalidate()           // request another frame (the simple loop)
    }
}

width and height give the view's size (Int). invalidate() asks for a redraw.

Touch input (Ch5)

override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> { /* finger pressed */ }
        MotionEvent.ACTION_MOVE -> { /* finger dragged */ }
    }
    val x = event.x   // touch position (Float)
    val y = event.y
    return true       // we handled it
}

Bitmaps & the texture atlas (Ch22)

// Load an image once, from the assets folder:
val texture = BitmapFactory.decodeStream(context.assets.open("lkbbag_texture.png"))!!

// Rect = source region in the texture (whole pixels)
val src = Rect(left, top, right, bottom)
// RectF = destination on screen (floats; can be scaled bigger)
val dst = RectF(x, y, x + w, y + h)
canvas.drawBitmap(texture, src, dst, paint)

Atlas file line format: name, x, y, width, height. Put the texture and atlas in app/src/main/assets/.

Rect vs RectF & collision (Ch22, Ch23)

RectF.intersects(boxA, boxB)   // true if the two rectangles overlap

Audio — SoundPool (Ch22)

val soundPool = SoundPool.Builder().setMaxStreams(8).build()
val jump = soundPool.load(context, R.raw.sfx_jump, 1)   // file in res/raw
soundPool.play(jump, 1f, 1f, 1, 0, 1f)
//        id,  leftVol, rightVol, priority, loop(0=once), rate(1f=normal pitch)

Vary the last value for pitch variation (Ch30). Sound files go in app/src/main/res/raw/.

Threaded game loop — SurfaceView (Ch20, Ch21)

class GameView(context: Context) : SurfaceView(context), Runnable {
    private var thread: Thread? = null
    @Volatile private var playing = false
    private var deltaTime = 0f

    override fun run() {                       // the loop, on its own thread
        while (playing) {
            val start = System.currentTimeMillis()
            if (holder.surface.isValid) { update(); draw() }
            val ms = System.currentTimeMillis() - start
            if (ms < 16L) Thread.sleep(16L - ms)        // ~60 FPS
            deltaTime = (System.currentTimeMillis() - start) / 1000f
        }
    }
    private fun draw() {
        val canvas = holder.lockCanvas()       // lock
        // ...draw on canvas...
        holder.unlockCanvasAndPost(canvas)     // post to screen
    }
    fun resume() { playing = true; thread = Thread(this); thread?.start() }
    fun pause()  { playing = false; thread?.join() }
}

Start/stop from the activity's onResume() / onPause().

Delta-time movement (Ch21)

// Speed is pixels PER SECOND; multiply by delta so motion is frame-rate independent
x += velX * deltaSeconds

Saving data — SharedPreferences (Ch30)

val prefs = context.getSharedPreferences("mygame", Context.MODE_PRIVATE)
val best = prefs.getInt("highScore", 0)                 // read (default 0)
prefs.edit().putInt("highScore", best).apply()          // write