【Android】複雑な画像をボタンにする

# 1.はじめに
こちらは、[NTTテクノクロス Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ntt-tx)シリーズ2の4日目の記事です。

こんにちは、NTTテクノクロスの岡崎と申します。
普段はMaas関連のモバイルアプリ開発を行っています。

業務中、PNG形式の画像の透明ではない部分にのみ当たり判定をつける実装をする必要があり、あまり参考になる記事がなく色々と試行錯誤しました。あまりそのような場面はないとは思いますが、当時自分が試したことを備忘録も兼ねて書かせていただきます。
今回は、3パターンのアプローチを行いました。使用する画像やタイムボックスによってベストプラクティスが変わると思うので、是非自分にとって最適な手法を使っていただけたらと思います。

# 2.使用する画像
今回は、例として以下の豆電球のような画像に当たり判定をつけます。形式はPNGでwidth80, height160を想定しております。豆電球以外の部分は透明であるという想定です。
<img width="160" alt="bottle_aquarium_denkyu.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2919458/85a28f5e-ce05-d872-8d01-160b5bebd2a1.png">

# 3.四角や丸で表現
### 手軽さ☆☆☆ 正確さ☆★★
今回のように画像のシルエットが長方形に近いのであれば、以下のコードでも問題ないと思います。しかし、電球の口金の左右の部分は画像と実際のタップエリアの差が大きく、人によっては違和感を感じるかもしれません。
<img width="160" alt="無題3_20241204125856.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2919458/435901c4-8987-5657-9380-ef59120bcddb.png">
```Kotlin
@Composable
fun HogeButton(action: () -> Unit) {
    Image(
        modifier = Modifier
            .width(80.dp)
            .height(160.dp)
            .clickable { action() },
        painter = painterResource(id = R.drawable.img_hoge),
        contentDescription = ""
    )
}
```


# 4.図形を組み合わせる
### 手軽さ☆☆★ 正確さ☆☆★
より正確なタップエリアを表現したいのであれば、以下のように四角や丸などの図形を画像と重ね合わせて配置するといいです。それぞれの図形にclickableを付けるのを忘れないようにしましょう。
<img width="160" alt="無題3_20241204125856.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2919458/990a02cc-f50e-38be-068b-80e99d7ef7f2.png">
```Kotlin
@Composable
fun HogeButton(action: () -> Unit) {
    Box(
        modifier = Modifier
            .width(80.dp)
            .height(160.dp),
        contentAlignment = Alignment.TopCenter
    ) {
        Box(
            modifier = Modifier
                .width(40.dp)
                .height(50.dp)
                .clickable { action() }
        )
        Box(
            modifier = Modifier
                .padding(top = 80.dp)
                .size(80.dp)
                .clip(CircleShape)
                .clickable { action() }
        )
        Image(
            modifier = Modifier
                .width(80.dp)
                .height(160.dp),
            painter = painterResource(id = R.drawable.img_hoge),
            contentDescription = ""
        )
    }
}
```

# 5.カスタムシェイプ
### 手軽さ☆★★ 正確さ☆☆☆
<img width="160" alt="無題3_20241204125856.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2919458/07ebb9d0-e029-e08c-4113-695d1c4ff1ef.png">

画像通りの完璧なタップエリアを表現するのであれば、Pathを使ってカスタムシェイプを作成します。しかし、複雑な図形になればなるほど実装が大変になります...。画像をSwifUIのPathに変換するライブラリはあるので、[こちら](https://github.com/kyome22/SVG2Path)を使ってSwiftUIのPathを参考に作成すると少し楽かもしれません。
```Kotlin
@Composable
fun HogeButton(action: () -> Unit) {
    val color = colorResource(id = R.color.hogeColor)
    Box(
        modifier = Modifier
            .width(80.dp)
            .height(160.dp),
    ) {
        Canvas(
            modifier = Modifier
                .clickable { action() }
        ) {
            val path = Path().apply {
                // ここに描画命令を書く
            }
            drawPath(
                path = path,
                color = color,
            )
        }
        Image(
            modifier = Modifier
                .width(80.dp)
                .height(160.dp),
            painter = painterResource(id = R.drawable.img_hoge),
            contentDescription = null
        )
    }
}
```

# 6.番外編
以下のコードでは、画像をBitmapとして一度変換し、タップした座標のピクセルが透明か確認し、透明ではない場合のみactionを実行します。画像通りのタップエリアを表現できますが、透明な部分のタップイベントを後ろのViewに伝えることができず、ボツとなりました(どうすればいいかご存知の方いれば教えてください)。
```Kotlin
@Composable
fun HogeButton(action: () -> Unit) {
    val context = LocalContext.current
    val bitmap = remember { BitmapFactory.decodeResource(context.resources, R.drawable.img_hoge) }
    Box(
        modifier = Modifier
            .width(80.dp)
            .height(160.dp)
            .pointerInput(Unit) {
                detectTapGestures { offset ->
                    val x = offset.x.toInt()
                    val y = offset.y.toInt()
                    if (x in 0 until bitmap.width && y in 0 until bitmap.height) {
                        if (bitmap.getPixel(x, y) != 0) {
                            action()
                        }
                    }
                }
            }
    ) {
        Image(
            modifier = Modifier
                .width(80.dp)
                .height(160.dp),
            painter = painterResource(id = R.drawable.img_hoge),
            contentDescription = null
        )
    }
}
```

# 7.さいごに
最後まで読んでいただきありがとうございました。
間違いや改善点等あれば、ご指摘いただけると幸いです。

引き続き、[NTTテクノクロス Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ntt-tx) をお楽しみください！