【Android】Canvas APIの使い方

この記事は[レコチョク Advent Calendar 2022](https://qiita.com/advent-calendar/2022/recochoku)の14日目の記事となります。


## はじめに

はじめまして、秦基博さんと日向坂46さんに日々癒しと元気をいただいている休井です。  
株式会社レコチョクでAndroidの開発をしています。  

最近はJetpack Composeを使用してUI作成をすることが多いのですが、何となく難しそうで触れてこなかったCanvas APIについて、実際に使ってみながらご紹介したいと思います。

## Canvas APIについて
Androidでは、Jetpack ComposeというUIツールキットが提供されており、宣言的にUIを作成することができます。  
CanvasはそのJetpack Composeに含まれるAPIで、画面内の位置(座標)を用いてレイアウトの設定を行うことができるので、かなり自由度高くカスタムレイアウトを作成することができます。  

## 基本的な使い方

### 線を引く  
`drawLine()` を使用します。  
`start`と`end`で線の開始と終了を指定して線を引くことができます。

  ```kotlin
    @Composable
    fun DrawLine() {
        Canvas(
            modifier = Modifier
                .fillMaxSize()
        ) {
            val width = size.width
            val height = size.height
            drawLine(
                color = Color.Black,
                start = Offset.Zero,
                end = Offset(width, height),
                strokeWidth = 5F
            )
        }
    }
```

　<img  width="30%"  src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2980857/39cd4308-cc94-40f1-e71a-ee0549d8c050.png
"/>

### 四角形を描く  
`drawRect()`を使用します。  
`topLeft`で描画位置を決め、`size`で大きさを指定して四角形を描くことができます。  

```kotlin
    @Composable
    fun DrawRect() {
        Canvas(
            modifier = Modifier
                .fillMaxSize()
        ) {
            val width = size.width
            val height = size.height
            drawRect(
                color = Color.Black,
                topLeft = Offset(width / 3, height / 3),
                size = Size(width / 3, height / 3)
            )
        }
    }
```

　<img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2980857/e431ba96-313f-cc7d-c0b0-1a1e161142dd.png" />  

角丸の四角形にしたい場合は、`drawRoundRect()`を使い`cornerRadius`に丸の半径を設定します。

  ```kotlin
@Composable
fun DrawRoundRect() {
    Canvas(
        modifier = Modifier
            .fillMaxSize()
    ) {
        val width = size.width
        val height = size.height
        drawRoundRect(
            color = Color.Black,
            topLeft = Offset(width / 3, height / 3),
            size = Size(width / 3, height / 3),
            cornerRadius = CornerRadius(x = 100F, y = 100F)
        )
    }
}
```
<img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2980857/d73a67ad-ba70-9d35-7312-c13bed1aefb9.png" />

### 円を描く  
`drawCircle()`を使用します。
`center`で描画位置を決め、`radius`で半径を設定して円を描くことができます。

```kotlin
@Composable
fun DrawCircle() {
    Canvas(
        modifier = Modifier
            .fillMaxSize()
    ) {
        val width = size.width
        val height = size.height
        drawCircle(
            color = Color.Black,
            radius = 400F,
            center = Offset(width / 2, height / 2)
        )
    }
}
```

　<img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2980857/bcbc26d7-d7c7-52a1-7742-cf530a0dd394.png" />

また、楕円を描きたい場合は、`drawOval()`を使用します。  
`topLeft`で描画位置を決め、`size`で大きさを設定して楕円を描くことができます。

```kotlin
@Composable
fun DrawOval() {
    Canvas(
        modifier = Modifier
            .fillMaxSize()
    ) {
        val width = size.width
        val height = size.height
        drawOval(
            color = Color.Black,
            topLeft = Offset(width / 4, height / 3),
            size = Size(width = width / 2, height = height / 5)
        )
    }
}
```

<img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2980857/c5c2ddbd-7ae6-5eac-1bb0-90cbb3017e46.png" />

### 弧を描く  
`drawArc()`を使用します。  
`startAngle`で開始位置(`0F`が3時の方向)を決め、`sweepAngle`で弧の角度を設定して弧を描くことができます。  
また、`useCenter`を`true`にすると、扇型に描画することができます。

```kotlin
@Composable
fun DrawArc() {
    Canvas(
        modifier = Modifier
            .fillMaxSize()
    ) {
        val width = size.width
        drawArc(
            color = Color.Black,
            startAngle = 0F,
            sweepAngle = 90F,
            useCenter = true,
            size = Size(width = width / 2, height = width / 2)
        )
        drawArc(
            color = Color.Black,
            startAngle = 0F,
            sweepAngle = 90F,
            useCenter = false,
            size = Size(width = width / 2, height = width / 2),
            style = Stroke(width = 5F),
            topLeft = Offset(0F, width / 2)
        )
    }
}
```

<img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2980857/657f9d31-475c-cd2e-4e6f-021341a29e12.png" />

### 点を描く  
`drawPoints()`を使用します。  
`points`で描きたい点の位置をリストで指定して点を描くことができます。  
また、`pointMode`で描き方、`cap`で点の形を設定できます。

```kotlin
@Composable
fun DrawPoints() {
    Canvas(
        modifier = Modifier
            .fillMaxSize()
    ) {
        val height = size.height
        drawPoints(
            points = listOf(
                Offset(300F, height / 2),
                Offset(500F, height / 2),
                Offset(700F, height / 2),
                Offset(900F, height / 2),
                Offset(1100F, height / 2),
                ),
            pointMode = PointMode.Points,
            color = Color.Black,
            strokeWidth = 50F,
            cap = StrokeCap.Square
        )
    }
}
```

<img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2980857/e3fb026d-0c23-54a4-3119-004a8934cb8b.png" />

### パスを決めて描く  
`drawPath()`を使用します。  
`path`を作成し`drawPath()`に渡すことで、作成した`path`に色を付けることができます。  
```kotlin
@Composable
fun DrawPath() {
    Canvas(
        modifier = Modifier
            .fillMaxSize()
    ) {
        val width = size.width
        val height = size.height
        val path = Path()
        path.moveTo(width / 2, height / 3) // pathの開始位置
        path.lineTo(width / 2, height / 2) // 開始位置→(width / 2, height / 2)
        path.lineTo(width, height / 3) // (width / 2, height / 2)→(width, height / 3)
        drawPath(
            path = path,
            color = Color.Black
        )
    }
}
```

<img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2980857/57bcf92e-17a0-aa47-a1f6-1454eb8ecd08.png" />

 ## 使ってみた

 Canvasでできることと使い方の基本について確認できたので、単純なアナログ時計のレイアウトを描いてみました。  

 ```kotlin
@Composable
fun Clock() {
    Canvas (
        modifier = Modifier
            .size(360.dp)
    ) {
        drawCircle(
            color = Color.Black,
            radius = 180.dp.toPx(),
            center = center,
            style = Stroke(width = 3.dp.toPx())
        )
        drawPoints(
            points = listOf(
                center,

                Offset(180.dp.toPx(), 12.dp.toPx()),
                Offset(264.dp.toPx(), 35.dp.toPx()),
                Offset(325.dp.toPx(), 97.dp.toPx()),

                Offset(348.dp.toPx(), 180.dp.toPx()),
                Offset(325.dp.toPx(), 263.dp.toPx()),
                Offset(264.dp.toPx(), 325.dp.toPx()),

                Offset(180.dp.toPx(), 348.dp.toPx()),
                Offset(96.dp.toPx(), 325.dp.toPx()),
                Offset(35.dp.toPx(), 263.dp.toPx()),

                Offset(12.dp.toPx(), 180.dp.toPx()),
                Offset(35.dp.toPx(), 97.dp.toPx()),
                Offset(96.dp.toPx(), 35.dp.toPx()),
            ),
            pointMode = PointMode.Points,
            color = Color.Black,
            cap = StrokeCap.Square,
            strokeWidth = 5.dp.toPx()
        )

        drawLine(
            color = Color.Black,
            start = center,
            end = Offset(center.x, 40.dp.toPx()),
            strokeWidth = 5.dp.toPx()
        )
        drawLine(
            color = Color.Black,
            start = center,
            end = Offset(center.x + 100.dp.toPx(), center.y),
            strokeWidth = 5.dp.toPx()
        )
    }
}
 ```

<img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2980857/e606575f-fa36-587f-e94e-496263596f99.png" />

`center`でCanvasの中心のOffsetを取得したり、`dp.toPx()`でDpからピクセルへの変換をしたりもできるので、機種によってレイアウトが崩れるという心配も少なそうですね。

 ## おわりに

 今回はJetpack ComposeのCanvas APIの使い方についてご紹介しました。  
 私自身今回でほぼ初めて触れましたが、シンプルなレイアウトであればさほど難しくなく作れそうです。  
 アニメーションをつけたり、テキストを入れたりすることもできるようなので、今後勉強して使いこなせるようになりたいと思います。  

 最後まで読んでいただきありがとうございました。  
 明日の[レコチョク Advent Calendar 2022](https://qiita.com/advent-calendar/2022/recochoku)は15日目 シンプルな掲示板を素のPHPとCakePHPで作って比較する となります。お楽しみに！  

 この記事はレコチョクのエンジニアブログの記事を転載したものとなります。

https://techblog.recochoku.jp/8702