JetpackComposeで9patchイメージを描画する

# 概要
![スクリーンショット 2023-07-31 10.34.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/004acf7e-eee5-bdaa-bc28-0347156c1df8.png)
「Sign in with Google」ボタンを作りたかったんです。

このボタンにはガイドライン（[ガイドライン](https://developers.google.com/identity/branding-guidelines?hl=ja)）がありまして、このサイトから画像をダウンロードすると、android用は9patchイメージなんです。

さて、これをJetpackComposeでどうやるか？という考察です。

## ファイルをプロジェクトにインポートする
上記のガイドラインページから画像をダウンロードすると、サイズごとに8個のファイルが入っています。
4つのボタン状態（disable, focus, pressed, normal）x(light or dark)で8個ですね。
![スクリーンショット 2023-07-31 10.37.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/65f16201-7ae6-b4e5-e2de-ff97c3ad2150.png)

まずは、これをkotlin側から呼びやすい構成に変更していきます。

#### ①すべてのファイル名から、サイズの部分を削除します。
「btn_google_signin_dark_disabled_xhdpi.9.png」
「btn_google_signin_dark_disabled.9.png」
※mac使っている方はFinderの「名前を変更」を使うと楽かもです。

#### ②focus以外の3つの状態x(light or dark)をmipmap(drawableでもいいと思います)にサイズことに格納していきます。
![スクリーンショット 2023-07-31 10.42.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/fc4dc775-190c-9add-413f-8fe3d359e69c.png)

#### ③drawableフォルダに「sign_in_with_google.xml」「sign_in_with_google_disable.xml」「sign_in_with_google_press.xml」の3つを作ります。
```xml:sign_in_with_google.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@mipmap/btn_google_signin_light_normal"/>
</selector>
```
```xml:sign_in_with_google_disable.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@mipmap/btn_google_signin_light_disabled" />
</selector>
```
```xml:sign_in_with_google_press.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@mipmap/btn_google_signin_light_pressed" />
</selector>
```

#### ④drawable-nightフォルダにも同じファイル名で中身がdarkのファイルを3つ作ります。
drawable-nightフォルダがない人はそのフォルダも作ります。
```xml:sign_in_with_google.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@mipmap/btn_google_signin_dark_normal" />
</selector>```
```xml:sign_in_with_google_disable.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@mipmap/btn_google_signin_dark_disabled" />
</selector>
```
```xml:sign_in_with_google_press.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@mipmap/btn_google_signin_dark_pressed" />
</selector>
```

![スクリーンショット 2023-07-31 10.52.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/f18ba283-1351-158a-7a70-5dd9570a640e.png)

#### 準備完了です
AndroidStudioで見ると、こんな感じの表示になります。
![スクリーンショット 2023-07-31 10.56.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/ca82cccf-5f08-b399-c827-04dbb6ccec46.png)

## kotlin側の実装

#### ①Drawableをオブジェクトとして読み込む
9patchは、painterの引数にとれないので、Drawableとして読み込みます。
```kotlin
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SignInWithGoogleButton(enabled: Boolean, onClick: () -> Unit) {
    val buttonImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google)
    val buttonDisabledImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google_disable)
    val buttonPressImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google_press)
}
```

#### ②Boxの背景としてdrawします
いったんボタンの状態は忘れて、通常モードの時だけ表示できる様にします。
```diff_kotlin
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SignInWithGoogleButton(enabled: Boolean, onClick: () -> Unit) {
    val buttonImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google)
    val buttonDisabledImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google_disable)
    val buttonPressImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google_press)
+    Box(
+        modifier = Modifier
+            .size(height = 40.dp, width = 220.dp)
+            .drawBehind {
+                buttonImage?.updateBounds(0, 0, size.width.toInt(), size.height.toInt())
+                buttonImage?.draw(drawContext.canvas.nativeCanvas)
+            },
+    ) {
+    /* Text()Composeとか配置します */
+    }
}
```

#### ③画像をenableによって変えます
描画する画像をenableによって変えましょう。
```diff_kotlin
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SignInWithGoogleButton(enabled: Boolean, onClick: () -> Unit) {
    val buttonImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google)
    val buttonDisabledImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google_disable)
    val buttonPressImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google_press)
    Box(
        modifier = Modifier
            .size(height = 40.dp, width = 220.dp)
            .drawBehind {
-                buttonImage?.updateBounds(0, 0, size.width.toInt(), size.height.toInt())
-                buttonImage?.draw(drawContext.canvas.nativeCanvas)
+                if (!enabled) {
+                    buttonDisabledImage?.updateBounds(0, 0, size.width.toInt(), size.height.toInt())
+                    buttonDisabledImage?.draw(drawContext.canvas.nativeCanvas)
+                } else {
+                    buttonImage?.updateBounds(0, 0, size.width.toInt(), size.height.toInt())
+                    buttonImage?.draw(drawContext.canvas.nativeCanvas)
+                }
            },
    ) {
    /* Text()Composeとか配置します */
    }
}
```

#### ④画像をpressによって変えます
pressの状態を保持するStateの定義、pressのイベントを拾うpointerInteropFilterを定義して、描画状態を変えていきます。
```diff_kotlin
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SignInWithGoogleButton(enabled: Boolean, onClick: () -> Unit) {
    val buttonImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google)
    val buttonDisabledImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google_disable)
    val buttonPressImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google_press)
+    val pressedState = remember { mutableStateOf(false) }
    
    Box(
        modifier = Modifier
            .size(height = 40.dp, width = 220.dp)
+            .pointerInteropFilter { motionEvent ->
+                when (motionEvent.actionMasked) {
+                    MotionEvent.ACTION_DOWN -> pressedState.value = true
+                    MotionEvent.ACTION_UP -> pressedState.value = false
+                    MotionEvent.ACTION_CANCEL -> pressedState.value = false
+                }
+                false
+            }
            .drawBehind {
                if (!enabled) {
                    buttonDisabledImage?.updateBounds(0, 0, size.width.toInt(), size.height.toInt())
                    buttonDisabledImage?.draw(drawContext.canvas.nativeCanvas)
+                } else if (pressedState.value) {
+                    buttonPressImage?.updateBounds(0, 0, size.width.toInt(), size.height.toInt())
+                    buttonPressImage?.draw(drawContext.canvas.nativeCanvas)
                } else {
                    buttonImage?.updateBounds(0, 0, size.width.toInt(), size.height.toInt())
                    buttonImage?.draw(drawContext.canvas.nativeCanvas)
                }
            },
    ) {
    /* Text()Composeとか配置します */
    }
}
```

## 完成ソース
これで完成になります！
```kotlin:SignInWithGoogleButton.kt
package com.jozu.compose.planfun.presentation.screen.signin

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.view.MotionEvent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.updateBounds
import com.jozu.compose.planfun.R
import com.jozu.compose.planfun.infra.log.Logger
import com.jozu.compose.planfun.presentation.theme.robotoFamily

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SignInWithGoogleButton(enabled: Boolean, onClick: () -> Unit) {
    val buttonImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google)
    val buttonDisabledImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google_disable)
    val buttonPressImage = ContextCompat.getDrawable(LocalContext.current, R.drawable.sign_in_with_google_press)
    val pressedState = remember { mutableStateOf(false) }

    Box(
        modifier = Modifier
            .size(height = 40.dp, width = 220.dp)
            .pointerInteropFilter { motionEvent ->
                when (motionEvent.actionMasked) {
                    MotionEvent.ACTION_DOWN -> pressedState.value = true
                    MotionEvent.ACTION_UP -> pressedState.value = false
                    MotionEvent.ACTION_CANCEL -> pressedState.value = false
                }
                false
            }
            .drawBehind {
                if (!enabled) {
                    buttonDisabledImage?.updateBounds(0, 0, size.width.toInt(), size.height.toInt())
                    buttonDisabledImage?.draw(drawContext.canvas.nativeCanvas)
                } else if (pressedState.value) {
                    buttonPressImage?.updateBounds(0, 0, size.width.toInt(), size.height.toInt())
                    buttonPressImage?.draw(drawContext.canvas.nativeCanvas)
                } else {
                    buttonImage?.updateBounds(0, 0, size.width.toInt(), size.height.toInt())
                    buttonImage?.draw(drawContext.canvas.nativeCanvas)
                }
            }
            .clickable {
                onClick.invoke()
            },
    ) {
        Row(
            modifier = Modifier.fillMaxHeight(),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Spacer(modifier = Modifier.width(54.dp))
            Text(
                stringResource(id = R.string.sign_in_with_google),
                fontFamily = robotoFamily,
                fontWeight = FontWeight.Medium,
                fontSize = TextUnit(14f, TextUnitType.Sp),
                color = getTextColor(enabled),
            )
        }
    }
}

@Composable
private fun getTextColor(enabled: Boolean): Color {
    return if (isSystemInDarkTheme()) {
        if (enabled) {
            Color.White
        } else {
            Color.Gray
        }
    } else {
        if (enabled) {
            Color.Black
        } else {
            Color.Gray
        }
    }
}

@Preview(name = "Light Mode", showBackground = true)
@Preview(name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun PreviewSignInWithGoogleButton() {
    Column {
        SignInWithGoogleButton(enabled = true) {}
        SignInWithGoogleButton(enabled = false) {}
    }
}
```