0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-07-31

概要

スクリーンショット 2023-07-31 10.34.47.png
「Sign in with Google」ボタンを作りたかったんです。

このボタンにはガイドライン(ガイドライン)がありまして、このサイトから画像をダウンロードすると、android用は9patchイメージなんです。

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

ファイルをプロジェクトにインポートする

上記のガイドラインページから画像をダウンロードすると、サイズごとに8個のファイルが入っています。
4つのボタン状態(disable, focus, pressed, normal)x(light or dark)で8個ですね。
スクリーンショット 2023-07-31 10.37.54.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

③drawableフォルダに「sign_in_with_google.xml」「sign_in_with_google_disable.xml」「sign_in_with_google_press.xml」の3つを作ります。

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>
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>
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フォルダがない人はそのフォルダも作ります。

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

準備完了です

AndroidStudioで見ると、こんな感じの表示になります。
スクリーンショット 2023-07-31 10.56.14.png

kotlin側の実装

①Drawableをオブジェクトとして読み込む

9patchは、painterの引数にとれないので、Drawableとして読み込みます。

@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します

いったんボタンの状態は忘れて、通常モードの時だけ表示できる様にします。

@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によって変えましょう。

@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を定義して、描画状態を変えていきます。

@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とか配置します */
    }
}

完成ソース

これで完成になります!

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) {}
    }
}
0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?