Jetpack Compose の Modifier.drawBehind でかんたんなドロップシャドウ技

Jetpack Compose の Modifier には [`Modifier.drawBehind`](https://developer.android.com/reference/kotlin/androidx/compose/ui/draw/package-summary#(androidx.compose.ui.Modifier).drawBehind(kotlin.Function1)) というのがあって、
Composable の裏に Canvas を使って描画するものがあります。

[`Modifier.drawWithContent`](https://developer.android.com/reference/kotlin/androidx/compose/ui/draw/package-summary#(androidx.compose.ui.Modifier).drawWithContent(kotlin.Function1)) もあって、
これは内容を明示的に描画することで全景にも描画できるようになっているものです。

これをつかってかんたんなドロップシャドウを描画しましょう。

![Notes_230227_152708-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359439/1d7de57b-3fda-0348-3f8c-0a4357fb86fb.jpeg)


## まずそのまま描画する

今回は例としてこういう Composable を用意します:

```kotlin:MyComposable.kt
@Composable
fun MyComposable(name: String) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier.padding(16.dp),
    ) {
        MyImage()
        Spacer(modifier = Modifier.width(16.dp))
        Text(text = "Hello $name!")
    }
}

@Composable
fun MyImage(
    modifier: Modifier = Modifier,
) {
    Image(
        painter = painterResource(id = R.drawable.my_thumbnail),
        contentDescription = null,
        modifier = modifier,
    )
}
```

![MyComposablePreview](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359439/bcdeae48-99ca-6805-8d57-22960a179234.png)


こういうテンプレートにちょっとサムネイル画像を追加したみたいなものがあるとします。


## ドロップシャドウを加える

ここでこのサムネイル画像にドロップシャドウを加えたいと思います。

単純に `Box` の中に二つの同じ composable を配置してズラすとこでも可能かもしれませんがすこしやっかいかもしれません。

ここで [`Modifier.drawWithCache`](https://developer.android.com/reference/kotlin/androidx/compose/ui/draw/package-summary#(androidx.compose.ui.Modifier).drawWithCache(kotlin.Function1)) 内で`onDrawBehind` により、composable の後ろに Canvas で影を描画することで実現してみたいと思います:

```diff_kotlin:MyComposable.kt
+ import androidx.compose.material3.MaterialTheme
+ import androidx.compose.ui.draw.drawWithCache
+ import androidx.compose.ui.geometry.Offset

 fun MyImage(
     modifier: Modifier = Modifier,
 ) {
+    val color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.10f)
     Image(
         painter = painterResource(id = R.drawable.my_thumbnail),
         contentDescription = null,
-        modifier = modifier,
+        modifier = modifier.drawWithCache {
+            onDrawBehind {
+                drawRect(
+                    color = color,
+                    topLeft = Offset(
+                        x = 8.dp.toPx(),
+                        y = 8.dp.toPx(),
+                    ),
+                    size = size,
+                )
+            }
+        },
     )
 }
```

この結果はこのようになり影が落ちているように見えます:

![MyComposablePreview (ドロップシャドウ版)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359439/2ce1b099-c1bf-5b70-b03b-73f7077f58de.png)

ここで描画された canvas は composable の領域をはみでてもクリップされないのでスペースは少しとってあげるのがよいでしょう。

※`Modifier.clipToBounds` すると領域でクリップされます。


## `drawWithContent`

そして `Modifier.drawWithContent` を使うと、内容の上のレイヤーにも描画できます:

```kotlin
val color = Color.Red
onDrawWithContent {
    drawContent()
    drawRect(
        color = color,
        topLeft = Offset(
            x = 8.dp.toPx(),
            y = 8.dp.toPx(),
        ),
        size = size,
    )
}
```

![drawWithContent](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359439/c020334e-7459-f9f4-8a82-27386a9ccddf.png)

装飾を上に追加したい場合はよさそうですね。


## ポイント: 影の色

影の色は `onSurface` の 10% alpha で塗っていますが、当然上に塗っていくと乗算されていくので、alpha をもった図形を重ねる場合は少し工夫が必要です。

![alpha rect on alpha](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359439/b20b38b0-5c57-71a7-cfaf-12859b0dd4b6.png)

```kotlin
onDrawBehind {
    drawRect(
        color = color,
        topLeft = Offset(
            x = 8.dp.toPx(),
            y = 8.dp.toPx(),
        ),
        size = size,
    )
    drawRect(
        color = color,
        topLeft = Offset(
            x = 16.dp.toPx(),
            y = 16.dp.toPx(),
        ),
        size = size,
    )
}
```

とはいえアルファブレンドしないで固定色指定にすると Dark Theme のときの表示が微妙になります:

![static color](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359439/b1bfa2b1-f099-855e-b883-fbc60f3ec592.png)

```kotlin
val color = Color(0xFF555555)
```

こういうときは [`Color.compositeOver`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/package-summary#(androidx.compose.ui.graphics.Color).compositeOver(androidx.compose.ui.graphics.Color)) をキメます:

```kotlin
val color = MaterialTheme.colorScheme.onSurface
    .copy(alpha = 0.10f)
    .compositeOver(MaterialTheme.colorScheme.surface)
```

そうすると `surface` に `onSurface` を乗算したあとの色で描画できるので、現在のカラースキームに応じた色を使えるのでいい感じですね:

![compositeOver on Light](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359439/2ead6569-806e-6667-3dd4-f6eec63af948.png) ![compositeOver on Dark](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359439/294f97a3-85ac-ccf1-f2f6-018f6fd4e91d.png)

これで色を混ぜることができるので応用が効きそうな感じがしてきましたね。


## 最後に

以上です。

意外と簡単に composable に装飾を加えることができるので、これ以外でも応用が効きそうですね。活用法をお待ちしております。