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 には sizeDrawScope の現在のサイズを指定する Size オブジェクト)などの便利なフィールドがあります。

何かを描画するには、DrawScope のさまざまな描画関数のどれかを使用します。たとえば、次のコードでは画面の左上隅に長方形を描画します。

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

白い背景上に画面の 1/4 を占めるように描画されたピンクの長方形
図 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() のようなオペレーションは、対応するラムダ内のすべての描画オペレーションに適用されます。たとえば、次のコードは 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 ピクセル、上に 300 ピクセル移動します。

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 が一つずつ計算して保存する必要がなく、すべての変形が 1 回のオペレーションで実行されるため、個々の変形に対してネスト呼び出しを行うよりも効率的です。

たとえば次のコードは、移動と回転の両方を長方形に適用します。

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 を作成し、Measurer で drawText を呼び出します。

val textMeasurer = rememberTextMeasurer()

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

Canvas に描画された Hello が表示されている
図 8. Canvas でのテキストの描画

テキストを測定する

テキストの描画は、他の描画コマンドとは若干仕組みが異なります。通常は、描画コマンドでサイズ(幅と高さ)を指定して、図形や画像を描画します。テキストの場合は、レンダリングされるテキストのサイズをコントロールするパラメータがいくつかあります(フォントサイズ、フォント、合字、文字間隔など)。

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()
)

このコード スニペットにより、テキストの背景がピンク色になります。

背景が長方形で、全領域の 2/3 のサイズを占める複数行のテキスト
図 9. 背景が長方形で、全領域の 2/3 のサイズを占める複数行のテキスト

制約、フォントサイズ、測定したサイズに影響するプロパティを調整すると、新しいサイズが報告されます。widthheight の両方を固定サイズに設定すると、テキストは TextOverflow の設定に従います。たとえば次のコードは、コンポーザブル領域の高さ 1/3、幅 1/3 でテキストをレンダリングし、TextOverflowTextOverflow.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()
)

フルサイズに描画された楕円形で黒色の ShapeDrawable
図 13. キャンバスにアクセスして Drawable を描画

詳細

Compose での描画の詳細については、次のリソースをご覧ください。