Android Compose UI Canvas 自定義繪圖

當 Compose 提供的高階元件(如 Box, Image, Button)無法滿足複雜的視覺需求時,我們可以使用底層的 Canvas API 直接在畫布上繪製任意形狀、圖表或路徑。

核心概念:DrawScope 與 座標系

在 Compose 中使用 Canvas 時,你是在 DrawScope 中撰寫程式碼。這是一個受限的環境,專為高效繪圖設計。

  • 座標系統:左上角為 (0, 0)。X 軸向右增加,Y 軸向下增加。
  • 單位:底層繪圖 API 使用的是 像素 (Pixels)。雖然 DrawScope 提供了 size (像素) 和轉換工具,但如果你需要 DP,必須自行轉換。
Canvas(modifier = Modifier.fillMaxSize()) {
    // size 屬性代表目前畫布的維度 (以像素為單位)
    val canvasWidth = size.width
    val canvasHeight = size.height

    // 繪製一個基礎圓心
    drawCircle(
        color = Color.Blue,
        center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
        radius = size.minDimension / 4
    )
}

常用繪圖函式

除了 drawCircledrawLineDrawScope 還提供了豐富的基礎形狀:

  • drawRect:繪製矩形。
  • drawRoundRect:繪製帶圓角的矩形。
  • drawOval:繪製橢圓。
  • drawArc:繪製圓弧(可用於環狀圖或進度條)。
Canvas(modifier = Modifier.size(200.dp)) {
    // 繪製一個空心圓弧
    drawArc(
        color = Color.Red,
        startAngle = 0f,    // 從 3 點鐘方向開始
        sweepAngle = 270f,  // 旋轉 270 度
        useCenter = false,  // 是否連接圓心
        style = Stroke(width = 8.dp.toPx()) // 設定為空心線條
    )
}

變形處理 (Transformations)

與其手動計算每個點的偏移,不如使用變形 API。這通常更直觀且性能更佳:

Canvas(modifier = Modifier.size(100.dp)) {
    // 所有的變形都發生在 inset 區塊內
    inset(20f, 20f) {
        rotate(degrees = 45f) {
            drawRect(color = Color.Blue)
        }
    }
}

常用的變換包括:translate (平移)、rotate (旋轉)、scale (縮放) 以及 inset (內縮區間)。

實戰範例:簡易圓餅圖 (Pie Chart)

以下程式碼展示如何根據數據比例,動態繪製一個圓餅圖。

@Composable
fun SimplePieChart(proportions: List<Float>, colors: List<Color>) {
    Canvas(modifier = Modifier.size(200.dp)) {
        var startAngle = -90f // 從上方開始繪製

        proportions.forEachIndexed { index, prop ->
            val sweepAngle = prop * 360f
            drawArc(
                color = colors[index],
                startAngle = startAngle,
                sweepAngle = sweepAngle,
                useCenter = true
            )
            startAngle += sweepAngle
        }
    }
}

存取原生 Android Canvas

如果你需要的功能在 Compose DrawScope 中找不到(例如舊有的 drawText 或第三方圖形庫),你可以暫時切換回原生 Android Canvas:

Canvas(modifier = Modifier.fillMaxSize()) {
    drawIntoCanvas { canvas ->
        val nativeCanvas = canvas.nativeCanvas
        // 這裡可以使用 nativeCanvas.drawText(...) 等原生方法
    }
}

效能建議與最佳實踐

  1. 避免在畫布內配置物件Canvas 的程式碼會頻繁重新執行。絕對不要在 Canvas { ... } 區塊內呼叫 Path()Paint()。請在 remember 區塊內建立它們。
  2. 利用 drawBehind:如果你只是想在某個 Composable 下方畫背景,使用 Modifier.drawBehind 通常比開一個 Canvas 更省資源。
  3. 區分重組 (Recomposition) 與重繪 (Invalidation):改變 remember 中的普通變數通常不會觸發重繪。若要讓畫布動起來,請使用 Compose 的 State

Canvas 是 Compose 中最強大的「畫筆」。雖然它的門檻較高,但對於需要精細像素控制的應用場景(如:圖表、相機濾鏡、遊戲 UI)來說是不可或缺的。