今回は、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
を選択すると、マテリアルテーマのひな型が適用された状態でプロジェクトが作成されます。
プロジェクトを作成すると、MainActivity
のsetContent()
の一番上の階層に、アプリのテーマを適用するためのコンポーザブル関数が自動で追加されます。ここでは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
引数に渡したコンポーザブルに対して、colors
、typography
、shapes
で指定したテーマを適用する関数です。
自動作成されたTestAppTheme()
をもう一度見ると、TestAppTheme()
のcontent
引数がそのままMaterialTheme()
のcontent
に渡されています。最初にMainActivity
のところで確認したように、TestAppTheme()
はMainActivity
のUI階層の最上位に位置しているので、そのスロット内に書かれたUIコンポーネントすべてにマテリアルテーマが適用される、という構造です。
ダークモード
さて、TestAppTheme()
はダークモードとライトモードの切り替えも行っています。darkTheme
引数にはisSystemInDarkTheme()
の結果がデフォルト値として渡されます。isSystemInDarkTheme()
はandroidx.compose.foundation
パッケージに定義されています。関数名から想像できる通り、Androidシステム設定がダークモードになっているかどうかを取得できます。システム設定の問い合わせは時間がかかる処理なので、UI階層のあちこちで呼び出すことは推奨されません。TestAppTheme()
の実装のように、UI階層の最上位で一度だけ呼び出すのが望ましいです。
ダークモードかどうかを判定したら、DarkColorPalette
またはLightColorPalette
をMaterialTheme()
の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
オブジェクトを作成して返します。これらの関数を使って、Primary
やSecondary
などの色を指定して、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で色を指定する仕組みです。説明は省略しますが、typography
とshapes
もそれぞれType.kt
とShepe.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
コンポーネントを使います。Surface
のcolor
引数にテーマカラーを指定することによって、そのSurface
内のコンテンツの色が自動的に設定されます。例えば以下のようにSurface
のcolor
にMaterialTheme.colors.background
を指定すると、その内部のText
の文字色はMaterialTheme.colors.onBackground
になります。
Surface(
color = MaterialTheme.colors.background
) {
Text("Text color is `onBackground`")
}
注意点としては、Column
やRow
などのレイアウト用のコンポーネントのmodifier
にModifier.background()
でMaterialTheme.colors.background
などを設定しても、その内部のコンテンツはonBackground
などにはなりません。その場合はSurface()
で色を指定し、その内部にColumn
やRow
などを配置することで、Column
やRow
の内部コンテンツにもテーマカラーを適用できます。
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カラーが適用されていることが分かります。上で確認したように、ButtonDefault
がMaterialTheme.colors.primary
を参照しているためです。
二番目はFloatingActionButton
です。FloatingActionButton
の定義を見ると、MaterialTheme.colors.secondary
が参照されています。そのため、こちらもソースコードで指定しなくてもSecondaryカラーとOn Secondaryカラーが適用されます。
三番目はCard
の中にText
を配置しています。Card
の定義を見ると、backgroundColor
にMaterialTheme.colors.surface
が指定され、contentColor
にcontentColorFor(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入門