Compose のグラフィック

Jetpack Compose を使用することで、カスタム グラフィックを簡単に使えるようになります。多くのアプリは、画面上に描画される内容を正確に制御できる必要があります。これには、ボックスや円を画面の適切な場所に配置するといった簡単なことから、グラフィック要素をさまざまなスタイルで配置するといった複雑なことまで含まれます。Compose の宣言型アプローチでは、すべてのグラフィック設定を、メソッド呼び出しと Paint ヘルパー オブジェクトに分割することなく、1 か所で行うことができます。Compose は、必要なオブジェクトの作成や更新を効率的に行います。

Compose による宣言型グラフィック

Compose は、宣言型アプローチをグラフィックの処理方法に拡張しています。Compose のアプローチには、次のような利点があります。

  • Compose は、グラフィック要素内の状態を最小限に抑え、状態のプログラミングの問題を回避するのに役立ちます。
  • 描画時にすべてのオプションが、コンポーズ可能な関数で想定どおりに正しく機能します。
  • Compose のグラフィック API が、効率的な方法でオブジェクトを作成および解放します。

Canvas

カスタム グラフィックの中核をなすコンポーザブルは Canvas です。Canvas は他の Compose UI 要素と同じように、レイアウト内に配置します。Canvas 内で、スタイルと場所を正確に制御しながら要素を描画できます。

たとえば、次のコードでは、親要素内で使用可能なすべてのスペースを満たす Canvas コンポーザブルを作成しています。

Canvas(modifier = Modifier.fillMaxSize()) {
}

Canvas は、自身の状態を保持する、スコープ設定された描画環境である DrawScope を自動的に公開します。これにより、グラフィック要素のグループに対してパラメータを設定できます。DrawScope は、sizeDrawScope の現在のサイズと最大サイズを指定する Size オブジェクト)などの便利なフィールドを提供します。

たとえば、キャンバスの右上隅から左下隅に斜線を引くとします。これを行うには、drawLine コンポーザブルを追加します。

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

画面全体に細い斜線が描かれたスマートフォン。

図 1. drawLine を使ってキャンバス全体に線を描画します。上記のコードでは線の色を設定していますが、幅はデフォルト値を使用しています。

他のパラメータを使用して、描画をカスタマイズできます。たとえば、線はデフォルトでは非常に細い幅で描画され、描画のスケールに関係なく 1 ピクセルとして表示されます。このデフォルトをオーバーライドするには、strokeWidth 値を設定します。

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,
        strokeWidth = 5F
    )
}

画面全体に太い斜線が描かれたスマートフォン。

図 2. デフォルトの幅をオーバーライドして、図 1 の線を変更します。

drawRectdrawCircle など、他にもシンプルな描画関数が多数用意されています。たとえば次のコードでは、キャンバスの中央に、直径がキャンバスの短辺の半分に等しい塗りつぶしの円を描画しています。

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawCircle(
        color = Color.Blue,
        center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
        radius = size.minDimension / 4
    )
}

青い円が画面の中央に表示されたスマートフォン。

図 3. drawCircle を使用して、キャンバスの中心に円を配置します。デフォルトでは、drawCircle は塗りつぶしの円を描画するため、その設定を明示的に指定する必要はありません。

描画関数には、便利なデフォルトのパラメータが用意されています。たとえば、デフォルトでは drawRectangle() はその親スコープ全体を埋める大きさになり、drawCircle() の半径は親の短辺の半分になります。Kotlin の場合と同様に、デフォルトのパラメータ値を活用して、変更する必要のあるパラメータのみを設定することで、コードをよりシンプルかつ明確にすることができます。描画される要素のデフォルト設定は親スコープの設定に基づいて決まるため、DrawScope 描画メソッドに明示的なパラメータを指定することで、コードの簡素化と明確化を実現できます。

DrawScope

前述のように、各 Compose CanvasDrawScope を公開します。このスコープ設定された描画環境で、描画コマンドを実際に発行します。

たとえば、次のコードはキャンバスの左上隅に長方形を描画します。

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

関数 DrawScope.inset() を使用して現在のスコープのデフォルト パラメータを調整し、描画境界の変更と描画結果の変換を行うことができます。inset() のようなオペレーションは、対応するラムダ内のすべての描画オペレーションに適用されます。

val canvasQuadrantSize = size / 2F
inset(50F, 30F) {
    drawRect(
        color = Color.Green,
        size = canvasQuadrantSize
    )
}

DrawScope には、他にも rotate() などの単純な変換が用意されています。たとえば、次のコードは、キャンバスを 3×3 分割した中央のマスに塗りつぶしの長方形を描画します。

val canvasSize = size
val canvasWidth = size.width
val canvasHeight = size.height
drawRect(
    color = Color.Gray,
    topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
    size = canvasSize / 3F
)

画面中央に塗りつぶしの長方形が表示されたスマートフォン。

図 4. drawRect を使用して、画面中央に塗りつぶしの長方形を描画します。

長方形を回転させるには、次のように DrawScope に回転を適用します。

rotate(degrees = 45F) {
    drawRect(
        color = Color.Gray,
        topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
        size = canvasSize / 3F
    )
}

45 度回転した長方形が画面中央に表示されたスマートフォン。

図 5. rotate() を使用して現在の描画スコープに回転を適用し、長方形を 45 度回転させています。

図形描画に複数の変換を適用する場合は、ネストされた DrawScope 環境は作成せず、代わりに、withTransform() 関数を使用することをおすすめします。この関数は、必要なすべての変更を組み合わせた単一の変換を作成して適用します。withTransform() では、Compose がネストされた各変換を計算して保存する必要がなく、すべての変換が 1 回のオペレーションで実行されるため、個々の変換に対してネスト呼び出しを行うよりも効率が良くなります。

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

withTransform({
    translate(left = canvasWidth / 5F)
    rotate(degrees = 45F)
}) {
    drawRect(
        color = Color.Gray,
        topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
        size = canvasSize / 3F
    )
}

回転した長方形が画面の端にシフトされたスマートフォン。

図 6. ここでは withTransform を使用して、回転と変換の両方を適用し、長方形を回転させて左にシフトしています。