Jetpack Compose入門(18) テーマカラーの適用

この記事をシェア


今回は、Jetpack Composeアプリでテーマカラーを適用する方法について確認していきます。ダークテーマとライトテーマの切り替えについても扱います。

これまでJetpack Compose入門では、あえてテーマを無効にしてサンプルを動作させてきました。「Hello World! 手動設定編」などでも説明したように、意図せずにコンポーネントの色が設定されてしまうことによる混乱を避けるためでした。今回は、テーマのふるまいを理解して、コンポーネントの色を思い通りにコントロールできるようになりたいと思います。

マテリアルテーマ

実際のテーマの実装に入る前に、マテリアルデザインについて簡単に理解しておきましょう。

Jetpack Composeライブラリが提供しているコンポーネントは、マテリアルデザインの原則に基づいて設計されています。マテリアルデザインでは、光や影といった物理的なふるまいを取り入れて、様々なコンポーネントが設計されています。

そして、マテリアルデザインの原則に従いつつ個別のアプリやブランドの個性を表現する手段として、マテリアルテーマという仕組みが用意されています。マテリアルテーマでは、色、タイポグラフィ(フォント、大きさ、間隔など文字の体裁)、シェイプ(角の丸め方など)の3つの要素をカスタマイズできます。その中から今回は、最もアプリの見た目に与える影響が大きい、色について扱います。

カラーテーマ

マテリアルデザインのカラーテーマは、こちらに情報があります。Primary / Secondary / Background / Surface / Error の5色と、それぞれの色の上に文字やアイコンを置く場合に使う On Primary / On Secondary / On Background / On Surface / On Error の5色を定義します。さらに、PrimaryとSecondaryのバリエーションの Primary Variant / Secondary Variant を定義することもできます。

Primaryは、アプリのブランドイメージを決定づける色です。ツールバーなどに使います。Variantには、少し暗め、または明るめの同系色を指定して、システムバーなどのPrimaryと隣り合った要素に使います。Secondaryはアクセントカラーで、フローティングボタンなどに使います。Backgroundは背景、とくにスクロールするコンテンツの背景に使います。Surfaceも背景色ですが、カードやメニューなどの色として使います。したがって、Backgroundの上にSurfaceが置かれるというのはよくあるパターンになります。Errorは、入力結果が間違っていた時などに使います。

なお、ここで紹介したのはマテリアルテーマの本当に概要だけですので、実際のアプリの配色を決めるにあたってはマテリアルカラーの説明に目を通すことをお勧めします。

テーマを定義する

では、Jetpack Composeでマテリアルテーマを定義する方法を見ていきましょう。

Android StudioのNew Project作成時にEmpty Compose Activityを選択すると、マテリアルテーマのひな型が適用された状態でプロジェクトが作成されます。

プロジェクトを作成すると、MainActivitysetContent()の一番上の階層に、アプリのテーマを適用するためのコンポーザブル関数が自動で追加されます。ここではTestAppというプロジェクトを作成したので、TestAppTheme()という関数が作成されています。このTestAppThemeのスロット(関数の後ろのラムダ式のことです)内に書いたコンポーネントに、テーマが適用される仕組みになっています。

TestAppTheme {
    // A surface container using the 'background' color from the theme
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = MaterialTheme.colors.background
    ) {
        Greeting("Android")
    }
}

では、TestAppTheme()の中身を確認していきます。TestAppTheme()Theme.ktに自動的に作成されています。

@Composable
fun TestAppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}

TestAppTheme()の中では、MaterialTheme()を呼び出しています。MaterialTheme()はJetpack Composeでマテリアルテーマを実現するための根幹になっている関数です。androidx.compose.materialパッケージに定義されていますcontent引数に渡したコンポーザブルに対して、colorstypographyshapesで指定したテーマを適用する関数です。

自動作成されたTestAppTheme()をもう一度見ると、TestAppTheme()content引数がそのままMaterialTheme()contentに渡されています。最初にMainActivityのところで確認したように、TestAppTheme()MainActivityのUI階層の最上位に位置しているので、そのスロット内に書かれたUIコンポーネントすべてにマテリアルテーマが適用される、という構造です。

ダークモード

さて、TestAppTheme()はダークモードとライトモードの切り替えも行っています。darkTheme引数にはisSystemInDarkTheme()の結果がデフォルト値として渡されます。isSystemInDarkTheme()androidx.compose.foundationパッケージに定義されています。関数名から想像できる通り、Androidシステム設定がダークモードになっているかどうかを取得できます。システム設定の問い合わせは時間がかかる処理なので、UI階層のあちこちで呼び出すことは推奨されません。TestAppTheme()の実装のように、UI階層の最上位で一度だけ呼び出すのが望ましいです。

ダークモードかどうかを判定したら、DarkColorPaletteまたはLightColorPaletteMaterialTheme()colors引数に渡しています。これらのカラーパレットは、同じTheme.ktの中に自動作成されています。

private val DarkColorPalette = darkColors(
    primary = Purple200,
    primaryVariant = Purple700,
    secondary = Teal200
)

private val LightColorPalette = lightColors(
    primary = Purple500,
    primaryVariant = Purple700,
    secondary = Teal200
)

darkColors()lightColors()はどちらもandroidx.compose.materialパッケージに定義されている関数で、マテリアルカラーのセットを保持するColorsオブジェクトを作成して返します。これらの関数を使って、PrimarySecondaryなどの色を指定して、Colorsオブジェクトを作成します。ちなみにdarkColors()lightColors()の定義は以下のようになっていて、すべてのパラメータに初期値がセットされていますので、自分が変更したい色だけ指定すればOKです。

fun darkColors(
    primary: Color? = Color(0xFFBB86FC),
    primaryVariant: Color? = Color(0xFF3700B3),
    secondary: Color? = Color(0xFF03DAC6),
    secondaryVariant: Color? = secondary,
    background: Color? = Color(0xFF121212),
    surface: Color? = Color(0xFF121212),
    error: Color? = Color(0xFFCF6679),
    onPrimary: Color? = Color.Black,
    onSecondary: Color? = Color.Black,
    onBackground: Color? = Color.White,
    onSurface: Color? = Color.White,
    onError: Color? = Color.Black
): Colors

fun lightColors(
    primary: Color? = Color(0xFF6200EE),
    primaryVariant: Color? = Color(0xFF3700B3),
    secondary: Color? = Color(0xFF03DAC6),
    secondaryVariant: Color? = Color(0xFF018786),
    background: Color? = Color.White,
    surface: Color? = Color.White,
    error: Color? = Color(0xFFB00020),
    onPrimary: Color? = Color.White,
    onSecondary: Color? = Color.Black,
    onBackground: Color? = Color.Black,
    onSurface: Color? = Color.Black,
    onError: Color? = Color.White
): Colors

Theme.ktの説明に戻りますが、darkColors()lightColors()の引数に設定している色は、Color.ktに定数として定義されています。

val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

これが直接的に色を指定している部分になります。200や500といった数値は、マテリアルカラーLight and dark variantsに対応した数値です。コード上は単なる定数名なので必ずしもこのような数値にする必要はないですが、マテリアルテーマのガイドラインに沿って色を決めるのであれば、このような名付けにしておくと後から色を変更するときも分かりやすそうです。

以上がJetpack Composeで色を指定する仕組みです。説明は省略しますが、typographyshapesもそれぞれType.ktShepe.ktに定義され、MaterialTheme()に渡されています。

テーマを適用する

さて次は、ここまでで定義したテーマを実際のUIにどのように適用するのかを見ていきます。コンポーネントの種類によって、自動的にカラーテーマが適用される部分と、自分で色を指定する部分がありますので、順に見ていきます。

自動的に適用されるコンポーネント

Jetpack Composeの主要なコンポーネントは、デフォルトの動作としてMaterialThemeから色を取得しているため、特に何もしなくてもテーマが適用されます。

例えばButtonの定義を見てみると、colorsの初期値は以下のように定義されています。

colors: ButtonColors? = ButtonDefaults.buttonColors()

さらに、BottonDefaults.buttonColors()は以下のように定義されています。

@Composable
fun buttonColors(
    backgroundColor: Color? = MaterialTheme.colors.primary,
    contentColor: Color? = contentColorFor(backgroundColor),
    disabledBackgroundColor: Color? = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
            .compositeOver(MaterialTheme.colors.surface),
    disabledContentColor: Color? = MaterialTheme.colors.onSurface
            .copy(alpha = ContentAlpha.disabled)
): ButtonColors

これを見ると、Buttonの背景色はMaterialTheme.colors.primaryと定義されています。また前景色(contentColor)はcontentColorFor(backgroundColor)で取得していますので、背景がPrimaryの場合は、文字などのコンテンツはOnPrimaryの色になります。

このように、様々なコンポーネントで初期値としてMaterialThemeが参照されているので、プログラマが一つ一つ色を指定しなくても、カラーテーマが適用されるようにできています。

自分で色を指定する場合

一方で、自分で色を指定する必要がある場合もあります。例えば画面全体の背景は、デフォルトでMaterialTheme.colors.backgroundが適用されてほしいところですが、そのようにはなっていません。したがって自分で背景色を指定する必要があります。

画面全体や、特定のエリアの背景色を指定するには、Surfaceコンポーネントを使います。Surfacecolor引数にテーマカラーを指定することによって、そのSurface内のコンテンツの色が自動的に設定されます。例えば以下のようにSurfacecolorMaterialTheme.colors.backgroundを指定すると、その内部のTextの文字色はMaterialTheme.colors.onBackgroundになります。

Surface(
    color = MaterialTheme.colors.background
) {
    Text("Text color is `onBackground`")
}

注意点としては、ColumnRowなどのレイアウト用のコンポーネントのmodifierModifier.background()MaterialTheme.colors.backgroundなどを設定しても、その内部のコンテンツはonBackgroundなどにはなりません。その場合はSurface()で色を指定し、その内部にColumnRowなどを配置することで、ColumnRowの内部コンテンツにもテーマカラーを適用できます。

Surface(
    color = MaterialTheme.colors.background
) {
    Column {
        Text("Text color is `onBackground`")
        Text("Text color is `onBackground`")
    }
}

試してみよう

ではここまで説明してきたことを実際に試してみたいと思います。

まったく美しくはありませんが、分かりやすいように以下のようなテーマカラーを設定しました。

このテーマカラーがUIコンポーネントに適用されることを確認します。ソースコードと実行結果は以下の通りです。

TestAppTheme {
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = MaterialTheme.colors.background
    ) {
        Column(
            verticalArrangement = Arrangement.spacedBy(30.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier.padding(30.dp)
        ) {
            Button(onClick = {}) {
                Text( text = "Button", fontSize = 20.sp )
            }
            FloatingActionButton( onClick = {}, ) {
                Icon(
                    imageVector = Icons.Filled.Favorite,
                    contentDescription = null
                )
            }
            Card(
                modifier = Modifier.fillMaxWidth() .height(60.dp)
            ) {
                Text(
                    text = "Text on Card",
                    fontSize = 30.sp,
                    textAlign = TextAlign.Center
                )
            }
            Text(
                text = "Text on background",
                fontSize = 30.sp
            )
        }
    }
}

全体をテーマ関数のスロット内に記述し、最上位にSurfaceでBackgroundカラーを指定しました。その上にColumnで4つのコンポーネントを配置しています。

一番上はButtonです。特にソースコード内では色は指定していませんが、PrimaryカラーとOn Primaryカラーが適用されていることが分かります。上で確認したように、ButtonDefaultMaterialTheme.colors.primaryを参照しているためです。

二番目はFloatingActionButtonです。FloatingActionButtonの定義を見ると、MaterialTheme.colors.secondaryが参照されています。そのため、こちらもソースコードで指定しなくてもSecondaryカラーとOn Secondaryカラーが適用されます。

三番目はCardの中にTextを配置しています。Cardの定義を見ると、backgroundColorMaterialTheme.colors.surfaceが指定され、contentColorcontentColorFor(backgroundColor)が指定されています。そのため、CardはSurfaceカラーが適用され、Card内のTextにはOn Surfaceカラーが適用されます。

最後は上位のSurfaceに直接Textを配置しています。SurfaceのcolorにはソースコードでMaterialTheme.colors.backgroundを指定しているため、画面全体にはBackgroundカラーが適用され、TextにはOn Backgroundカラーが適用されます。ちなみに、Surfaceの定義を見ると、colorのデフォルト値はMaterialTheme.colors.surfaceです。したがって、呼び出し側でcolorを指定しなかった場合は画面全体がSurfaceカラーになります。

ステータスバーの色は別途指定が必要

ここまでで、画面を構成するUIコンポーネントや背景にテーマカラーを適用する方法を見てきました。デフォルトでMaterialTheme.colorsが設定されているコンポーネントは、特に何もしなくてもテーマカラーが設定され、別の色を設定したい場合はその都度MaterialTheme.colorsから色を選択して指定してやればよいということが分かりました。

ところで、上の例で一か所、テーマカラーが適用されていない箇所がありました。画面上部の時刻などが表示されている部分、ステータスバーです。

ステータスバーにはテーマカラーが適用されていない

実は、ステータスバーの色は標準ではCompose側からは指定できず、従来通りXMLで指定します。自動作成されるthemes.xmlの中にstatusBarColorが定義されています。

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="Theme.TestApp" parent="android:Theme.Material.Light.NoActionBar">
        <item name="android:statusBarColor">@color/purple_700</item>
    </style>
</resources>

せっかくComposeでスマートにテーマを定義できるのに、一番目立つステータスバーだけはXMLで指定する必要があるというのは、なんとも中途半端というか、片手落ちのような感じがしてしまいます。ただ、accompanistというライブラリを使うとComposeからステータスバーの色を指定できるらしいので、今後試してみようと思います。

まとめ

今回はマテリアルテーマの概要を確認し、Jetpack Composeでテーマカラーを使う方法を確認しました。ダークテーマとライトテーマの切り替え方法についても確認しました。MaterialThemeをうまく使えば、ソースコードをシンプルに保ったままカラーテーマを適用することが可能です。テーマを活用して、見た目も美しいUIを作っていきましょう。


Jetpack Compose入門


この記事をシェア