Compose 中的圖形

很多應用程式都必須能精準地控制畫面上所繪製的內容,像是將方塊或圓形放到畫面上的正確位置,或是詳細布置各式多樣的圖形元素。

使用修飾符和 DrawScope 的基本繪圖

在 Compose 中繪製自訂內容時,基本方法是使用修飾元,例如 Modifier.drawWithContentModifier.drawBehindModifier.drawWithCache

舉例來說,想在可組合項後方繪製內容的話,可以使用 drawBehind 修飾符來執行繪圖指令:

Spacer(
    modifier = Modifier
        .fillMaxSize()
        .drawBehind {
            // this = DrawScope
        }
)

如果您只需要一個可繪製的可組合項,就可以使用 Canvas 可組合項。Canvas 可組合項是 Modifier.drawBehind 的便利包裝函式。將 Canvas 置入版面配置的方式,與置入其他 Compose UI 元素的方式相同。在 Canvas 中繪製元素時,您可以精準控制元素的樣式和位置。

所有繪圖修飾符都會提供 DrawScope,這是一種限定作用範圍並能維持自身狀態的繪製環境,可用來設定一組圖形元素的參數。DrawScope 提供了多個實用欄位,比如 sizeSize 物件的作用是指定 DrawScope 的目前尺寸。

如要繪製內容,您可以使用 DrawScope 上的眾多繪圖函式。例如,以下程式碼的作用是在畫面左上角繪製矩形:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Magenta,
        size = canvasQuadrantSize
    )
}

白色背景上繪製了占畫面四分之一的粉色矩形
圖 1. 在 Compose 中使用 Canvas 繪製的矩形。

如要進一步瞭解各種繪圖修飾符,請參閱「圖形修飾符」說明文件。

座標系統

想在畫面上繪製內容,就必須知道內容的偏移植 (xy) 和大小。很多 DrawScope 上的繪製方法採用的是預設參數值提供的位置和大小。預設參數通常會將項目放在畫布的 [0, 0] 點上,並提供會填滿整個繪圖區域的預設 size,就如上例中左上角的矩形。如果想調整項目的大小和位置,就必須瞭解 Compose 的座標系統。

座標系統的起點 ([0,0]) 是繪圖區域左上角的像素,x 值增加為向右移,y 值增加為向下移。

座標系統網格,左上角為 [0, 0],右下角為 [width, height]
圖 2. 繪圖座標系統/繪圖網格。

舉例來說,如果想繪製一條從畫布右上角到左下角的對角線,可以使用 DrawScope.drawLine() 函式,並用相應的 x 和 y 位置來指定起點和終點的偏移值:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawLine(
        start = Offset(x = canvasWidth, y = 0f),
        end = Offset(x = 0f, y = canvasHeight),
        color = Color.Blue
    )
}

基本轉換

DrawScope 能處理改變繪圖指令執行位置或方式的轉換作業。

縮放

您可以運用 DrawScope.scale() 來依係數增加繪圖作業的大小。scale() 等作業會作用於相應 lambda 中所有的繪圖作業。例如,以下程式碼的作用是將 scaleX 放大 10 倍,將 scaleY放大 15 倍:

Canvas(modifier = Modifier.fillMaxSize()) {
    scale(scaleX = 10f, scaleY = 15f) {
        drawCircle(Color.Blue, radius = 20.dp.toPx())
    }
}

縮放不均勻的圓形
圖 3. 在 Canvas 中對圓形執行縮放作業。

平移

您可以使用 DrawScope.translate() 將繪圖作業向上、下、左或右移動。例如,以下程式碼的作用是將繪圖右移 100 px,上移 300 px:

Canvas(modifier = Modifier.fillMaxSize()) {
    translate(left = 100f, top = -300f) {
        drawCircle(Color.Blue, radius = 200.dp.toPx())
    }
}

偏離中心的圓形
圖 4. 在 Canvas 中對圓形執行平移作業。

旋轉

您可以運用 DrawScope.rotate() 讓繪圖作業圍繞樞紐點旋轉。例如,以下程式碼的作用是將矩形旋轉 45 度:

Canvas(modifier = Modifier.fillMaxSize()) {
    rotate(degrees = 45F) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

手機畫面中央有一個旋轉 45 度的矩形。
圖 5. 我們利用 rotate() 來針對目前的繪圖範圍執行旋轉作業,將矩形旋轉 45 度。

插邊

您可以運用 DrawScope.inset() 調整目前 DrawScope 的預設參數,變更繪圖界線並依此平移繪圖:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    inset(horizontal = 50f, vertical = 30f) {
        drawRect(color = Color.Green, size = canvasQuadrantSize)
    }
}

以下程式碼有效率地在繪圖指令中加上了邊框間距:

四周設有邊框的矩形
圖 6. 對繪圖指令套用插邊。

多重轉換

如要對繪圖套用多重轉換,請使用 DrawScope.withTransform() 函式。這個函式會建立並執行一個結合所有預定變更項目的轉換作業。使用 withTransform() 會比逐一針對轉換作業進行呼叫更有效率,因為所有轉換都會經由同一項作業執行,Compose 不必分開計算並儲存個別轉換結果。

例如,以下程式碼的作用是對矩形套用平移和旋轉作業:

Canvas(modifier = Modifier.fillMaxSize()) {
    withTransform({
        translate(left = size.width / 5F)
        rotate(degrees = 45F)
    }) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

手機畫面上有一個角度旋轉並偏移到側邊的矩形
圖 7. 使用 withTransform 執行旋轉和平移,將矩形旋轉並向左移。

常見的繪圖作業

繪製文字

在 Compose 中繪製文字時,您通常可以使用 Text 可組合項。不過,如果您使用的是 DrawScope,或想利用自訂方式來手動繪製文字,可以使用 DrawScope.drawText() 方法。

如要繪製文字,請使用 rememberTextMeasurer 建立 TextMeasurer,並使用測量器呼叫 drawText

val textMeasurer = rememberTextMeasurer()

Canvas(modifier = Modifier.fillMaxSize()) {
    drawText(textMeasurer, "Hello")
}

畫布上繪製的 Hello 文字
圖 8. 在畫布上繪製文字。

測量文字

繪製文字的做法和其他繪圖指令稍有不同。一般來說,您會在繪圖指令中指定所繪形狀/圖片的大小 (寬度和高度)。繪製文字時,則需要用一些參數來控制轉譯文字的大小,例如字型大小、字型、連字和字母間距。

在 Compose 中,您可以使用 TextMeasurer 量測由上述條件決定的文字大小。如想繪製文字後面的背景,可以從測量到的資訊來掌握文字所占區域的大小:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixedWidth((size.width * 2f / 3f).toInt()),
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

這個程式碼片段會使文字背景變成粉紅色:

多行文字占據畫面上 ⅔ 的區域,搭配矩形背景
圖 9. 多行文字占據畫面上 ⅔ 的區域,搭配矩形背景。

如果調整限制、字型大小或任何會影響測量大小的屬性,系統就會回報新的大小。您可以針對 widthheight 設定固定大小,文字將符合設定的 TextOverflow。例如,以下程式碼的作用是以可組合項區域為準,在 ⅓ 高度和 ⅓ 寬度的範圍內轉譯文字,並將 TextOverflow 設為 TextOverflow.Ellipsis

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixed(
                        width = (size.width / 3f).toInt(),
                        height = (size.height / 3f).toInt()
                    ),
                    overflow = TextOverflow.Ellipsis,
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

以下是依限制條件繪製,並以刪節號結尾的文字片段:

在粉紅色背景上繪製的文字,帶有截斷文字的刪節號。
圖 10. TextOverflow.Ellipsis 搭配對於測量文字的固定限制。

繪製圖片

如要使用 DrawScope 繪製 ImageBitmap,請使用 ImageBitmap.imageResource() 載入圖片並呼叫 drawImage

val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    drawImage(dogImage)
})

在 Canvas 上繪製的犬隻圖片
圖 11. 在 Canvas 上繪製 ImageBitmap

繪製基本形狀

DrawScope 有很多用來繪製形狀的函式。如要繪製形狀,請使用一個預先定義的繪圖函式,例如 drawCircle

val purpleColor = Color(0xFFBA68C8)
Canvas(
    modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
    onDraw = {
        drawCircle(purpleColor)
    }
)

API

輸出

drawCircle()

繪製圓形

drawRect()

繪製矩形

drawRoundedRect()

繪製圓角矩形

drawLine()

繪製線條

drawOval()

繪製橢圓形

drawArc()

繪製弧形

drawPoints()

繪製點

繪製路徑

路徑是一連串數學指令,執行後就會建立繪圖。DrawScope 可以使用 DrawScope.drawPath() 方法繪製路徑。

舉例來說,假設您想繪製一個三角形,可以使用 lineTo()moveTo() 之類函式以根據繪製區域的大小產生路徑,然後用這個新建立的路徑呼叫 drawPath() 來產生一個三角形。

Spacer(
    modifier = Modifier
        .drawWithCache {
            val path = Path()
            path.moveTo(0f, 0f)
            path.lineTo(size.width / 2f, size.height / 2f)
            path.lineTo(size.width, 0f)
            path.close()
            onDrawBehind {
                drawPath(path, Color.Magenta, style = Stroke(width = 10f))
            }
        }
        .fillMaxSize()
)

在 Compose 中繪製顛倒的紫色三角形路徑
圖 12. 在 Compose 中建立及繪製 Path

存取 Canvas 物件

使用 DrawScope 時,您無法直接存取 Canvas 物件,但可以運用 DrawScope.drawIntoCanvas() 來取得 Canvas 物件本身的存取權,藉此呼叫其中的函式。

舉例來說,如果想在畫布上繪製自訂 Drawable,可以存取畫布並呼叫 Drawable#draw(),將其傳遞到 Canvas 物件中:

val drawable = ShapeDrawable(OvalShape())
Spacer(
    modifier = Modifier
        .drawWithContent {
            drawIntoCanvas { canvas ->
                drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
                drawable.draw(canvas.nativeCanvas)
            }
        }
        .fillMaxSize()
)

以全尺寸繪製的黑色橢圓形形狀可繪項目
圖 13. 藉由存取畫布繪製 Drawable

瞭解詳情

如要進一步瞭解如何在 Compose 中繪圖,請參閱下列資源: