たくさんの自由帳

Jetpack ComposeにMaterial3を入れてダイナミックカラー使う

投稿日 : | 0 日前

文字数(だいたい) : 10484

どうもこんばんわ。

アインシュタインより愛を込めて 攻略しました。

なんか色々思うことはあるけど最後のタイトル回収とED曲で許した感ある。
あとこの子かわいい。

Imgur

サブヒロインの扱い雑すぎて悲しくなっちゃった



アインシュタインより愛を込めて APOLLOCRISIS 攻略しました。
今月のエロゲの日から無料配信されるみたいなので前作やった人は絶対やろう。やってロミちゃんを助けてあげて、、、

Imgur

野上さんいい人

本題

Jetpack ComposeにもMaterial3ライブラリがリリースされました。
ダイナミックカラーが使えるようになります。ちなみにBottomNavigationBarの高さは56.dp?から80.dpと大きくなりました。

Imgur

環境

なまえあたい
Android12 (ダイナミックカラー使うなら必須)
Android StuidoArctic Fox 2020.3.1 Patch 3
targetSdk31

公式ドキュメント

導入

https://developer.android.com/jetpack/compose/themes/material#material3

Material3

https://m3.material.io/

1. 導入

app/build.gradleに書き足すだけです。(新世界の)α版なので安定しないかもらしいです。

dependencies {

    // Jetpack Compose
    implementation("androidx.compose.ui:ui:1.1.0-beta01")
    implementation("androidx.compose.material:material:1.1.0-beta01")
    implementation("androidx.compose.ui:ui-tooling-preview:1.1.0-beta01")
    androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.1.0-beta01")
    debugImplementation("androidx.compose.ui:ui-tooling:1.1.0-beta01")

    // ↓これ
    implementation("androidx.compose.material3:material3:1.0.0-alpha01")

}

2. Theme.kt (MaterialTheme)を書き換える

次にTheme.ktMaterialThemeを書き換えます。
プロジェクト作成時からComposeプロジェクトを選んだ場合はTheme.ktって名前でMaterialThemeをカスタマイズしている関数があるはずです。

以下は一例です。
なんか色の扱いが変わってるので以下のサイトで色を吐き出してもらってもいいかも?

https://material-foundation.github.io/material-theme-builder/

val PrimaryColor = Color(0xff8c9bde)
val DarkColor = Color(0xff7873c2)
val LightColor = Color(0xffd1f7e3)

@SuppressLint("NewApi")
@Composable
fun ComposeDynamicColorTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    isEnableDynamicColor: Boolean = true,
    content: @Composable () -> Unit,
) {
    val context = LocalContext.current
    // ダイナミックカラー使う?
    val isUseDynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
            && isEnableDynamicColor

    // Android 12以降で
    val colorScheme = when {
        isUseDynamicColor && darkTheme -> dynamicDarkColorScheme(context)
        isUseDynamicColor && !darkTheme -> dynamicLightColorScheme(context)
        darkTheme -> darkColorScheme(
            primary = PrimaryColor,
            secondary = LightColor,
            tertiary = DarkColor,
        )
        else -> lightColorScheme(
            primary = PrimaryColor,
            secondary = LightColor,
            tertiary = DarkColor,
        )
    }

    MaterialTheme(
        colorScheme = colorScheme,
        content = content
    )
}

MaterialThemeandroidx.compose.material3.MaterialThemeをインポートするようにしてください。

import android.annotation.SuppressLint
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext

注意点?

(androidx.compose.material.)MaterialTheme.colorsは使えなくなります。
(androidx.compose.material3.)MaterialTheme.colorSchemeを使ってください。ない場合はimport間違えてます。

backgroundColorcontainerColorに変わった模様?

3. ひたすら import androidx.compose.materialからimport androidx.compose.material3に書き換えてく

対応しているコンポーネントは以下です。

https://developer.android.com/jetpack/androidx/releases/compose-material3

TextIconなんかもMaterial3で用意されている方を使う必要があります。

選ぶ際はmaterial3って書いてある方を。

Imgur

以下例です。
コピペする際は、ic_android_black_24dpAndroid StudioVector Assetから追加してください。

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3Api::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeDynamicColorTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colorScheme.background) {

                    Scaffold(
                        topBar = { MediumTopAppBar(title = { Text(text = "タイトルバー") }) },
                        bottomBar = {
                            NavigationBar {
                                NavigationBarItem(
                                    selected = true,
                                    icon = { Icon(painter = painterResource(id = R.drawable.ic_android_black_24dp), contentDescription = null) },
                                    label = { Text(text = "Android") },
                                    onClick = { }
                                )
                            }
                        },
                        floatingActionButton = {
                            ExtendedFloatingActionButton(
                                text = { Text(text = "ExtendedFAB") },
                                icon = { Icon(painter = painterResource(id = R.drawable.ic_android_black_24dp), contentDescription = null) },
                                onClick = { }
                            )
                        },
                        content = {
                            Button(onClick = { }) {
                                Text(text = "Hello World")
                            }
                        },
                    )

                }
            }
        }
    }
}

よーこそMaterial3の世界へ...

Imgur

つまずいた点

ScaffoldにSnackbarHostが無い

Scaffoldを使うといい感じにSnackbarを表示できるんですけど、Material3ScaffoldにはSnackbarを表示する機能が現状ないです。 SnackbarをComposeで表示させるには、SnackbarHostを追加すればいいので、ScaffoldSnackbarHostを追加したコンポーネントを作ります。

/**
 * Material3のScaffoldにSnackBar無いので適当に作る
 *
 * [Scaffold]に[SnackbarHost]を追加してるだけです。
 * */
@ExperimentalMaterial3Api
@Composable
fun M3Scaffold(
    modifier: Modifier = Modifier,
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
    topBar: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    floatingActionButton: @Composable () -> Unit = {},
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
    drawerGesturesEnabled: Boolean = true,
    drawerShape: Shape = RoundedCornerShape(16.dp),
    drawerTonalElevation: Dp = DrawerDefaults.Elevation,
    drawerContainerColor: Color = MaterialTheme.colorScheme.surface,
    drawerContentColor: Color = contentColorFor(drawerContainerColor),
    drawerScrimColor: Color = DrawerDefaults.scrimColor,
    containerColor: Color = MaterialTheme.colorScheme.background,
    contentColor: Color = contentColorFor(containerColor),
    content: @Composable (PaddingValues) -> Unit,
) {
    Scaffold(
        modifier = modifier,
        scaffoldState = scaffoldState,
        topBar = topBar,
        bottomBar = bottomBar,
        floatingActionButton = floatingActionButton,
        floatingActionButtonPosition = floatingActionButtonPosition,
        drawerContent = drawerContent,
        drawerGesturesEnabled = drawerGesturesEnabled,
        drawerShape = drawerShape,
        drawerTonalElevation = drawerTonalElevation,
        drawerContainerColor = drawerContainerColor,
        drawerContentColor = drawerContentColor,
        drawerScrimColor = drawerScrimColor,
        containerColor = containerColor,
        contentColor = contentColor,
        content = {
            Box {
                content(it)
                SnackbarHost(
                    modifier = Modifier
                        .padding(it)
                        .align(Alignment.BottomCenter),
                    hostState = snackbarHostState
                )
            }
        }
    )
}

使う際はこんなふうに

val scaffoldState = rememberScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }

LaunchedEffect(key1 = Unit, block = {
    snackbarHostState.showSnackbar("Hello World")
})

M3Scaffold(
    scaffoldState = scaffoldState,
    snackbarHostState = snackbarHostState,
    content = {
        // components...
    },
)

@ExperimentalMaterial3Api vs @OptIn(ExperimentalMaterial3Api::class)

どっち使うかって話

  • @ExperimentalMaterial3Api

    • 呼び出し先にも書かないといけない(伝搬する)
    • ライブラリ開発者が警告を出したいときに使う
  • @OptIn(ExperimentalMaterial3Api::class)

    • 呼び出し先には書かなくていい(伝搬しない)
    • ライブラリ利用者が警告を飲んだ場合はこっちを使う

多分こんな感じだと思う。

Material3版Surface の tonalElevation

Surfaceの引数の説明見るとこうです。

カラーがColorScheme.surfaceの場合、標高が高いほどライトテーマでは色が濃くなり、ダークテーマでは色が薄くなります。

DeepL翻訳

というわけでSurfaceの背景色がColorScheme.surfaceの場合はtonalElevationを考慮する必要があるわけです。え?そんなのどうでもいいだって?

NavigationBarの色はColorScheme.surfaceな訳ですが、tonalElevationが設定されているのでそのままでは同じ色には出来ません。

val context = LocalContext.current
val surfaceColor = MaterialTheme.colorScheme.surface
LaunchedEffect(key1 = surfaceColor, block = {
    (context as? Activity)?.window?.navigationBarColor = Color.argb(
        surfaceColor.toArgb().alpha,
        surfaceColor.toArgb().red,
        surfaceColor.toArgb().green,
        surfaceColor.toArgb().blue,
    )
})

そのためにColorScheme.surfaceColorAtElevationの実装をお借りします。
surfaceColorAtElevation自体を使えればいいんですけどinternalで保護されていて無理でした。

fun ColorScheme.applyTonalElevation(
    elevation: Dp,
): androidx.compose.ui.graphics.Color {
    if (elevation == 0.dp) return surface
    val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
    return primary.copy(alpha = alpha).compositeOver(surface)
}

あとは書き換えて

val context = LocalContext.current
val bottomNavColor = MaterialTheme.colorScheme.applyTonalElevation(3.dp)
LaunchedEffect(key1 = bottomNavColor, block = {
    (context as? Activity)?.window?.navigationBarColor = Color.argb(
        bottomNavColor.toArgb().alpha,
        bottomNavColor.toArgb().red,
        bottomNavColor.toArgb().green,
        bottomNavColor.toArgb().blue,
    )
})

これでナビゲーションバーの色を設定できました。

Imgur

終わりに

Android 12L ってのが発表されたみたいですね。え?Android 3.0?知らないですね?(国産全盛期?)