Jetpack Compose のテーマ設定

1. はじめに

この Codelab では、Jetpack Compose のテーマ設定 API を使用してアプリのスタイルを設定する方法を学びます。アプリ全体で一貫性を持つように色、シェイプ、タイポグラフィをカスタマイズし、ライトモードやダークモードなどの複数のテーマをサポートする方法について説明します。

学習内容

この Codelab では、以下について学びます。

  • マテリアル デザインの基本と、ブランドに合わせてカスタマイズする方法
  • Compose によるマテリアル デザイン システムの実装方法
  • アプリ全体で色、タイポグラフィ、シェイプを定義して使用する方法
  • コンポーネントのスタイルを設定する方法
  • ライトモードとダークモードをサポートする方法

作成するアプリの概要

この Codelab では、ニュース リーダー アプリのスタイルを設定します。スタイル設定なしのアプリから始め、学んだことを活かしてアプリのテーマを設定し、ダークモードをサポートします。

スタイルを適用する前のニュース リーダー アプリ Jetnews の画像。

スタイルを適用した後のニュース リーダー アプリ Jetnews の画像。

ダークモードでのスタイルを設定したニュース リーダー アプリ Jetnews の画像。

変更前: スタイルを適用していないアプリ

変更後: スタイルを適用したアプリ

変更後: ダークモード

前提条件

2. 設定方法

このステップでは、スタイルを設定するシンプルなニュース リーダー アプリを構成するコードをダウンロードします。

必要なもの

コードをダウンロードする

git がインストールされている場合は、以下のコマンドをそのまま実行できますgit がインストールされているかどうかを確認するには、ターミナルまたはコマンドラインで「git --version」と入力し、正しく実行されることを確認します。

git clone https://github.com/googlecodelabs/android-compose-codelabs.git
cd android-compose-codelabs/ThemingCodelabM2

git がない場合は、次のボタンをクリックして、この Codelab のすべてのコードをダウンロードできます。

Android Studio でプロジェクトを開き、[File] > [Import Project] を選択して、ThemingCodelabM2 ディレクトリを参照します。

プロジェクトには次の 3 つの主要パッケージが含まれています。

  • com.codelab.theming.data: モデルクラスとサンプルデータが含まれています。この Codelab では、このパッケージを編集する必要はありません。
  • com.codelab.theming.ui.start: Codelab の出発点であり、この Codelab で求められる変更はすべて、このパッケージで行う必要があります。
  • com.codelab.theming.ui.finish: この Codelab の最終状態(参考)

アプリをビルドして実行する

このアプリには、Codelab の開始状態と終了状態を反映した 2 つの実行構成があります。いずれかの構成を選択して実行ボタンを押すと、コードがデバイスまたはエミュレータにデプロイされます。

a43ae3c4fa75836e.png

このアプリには Compose のレイアウト プレビューも含まれています。start / finish パッケージの Home.kt に移動し、デザインビューを開くと、UI コードを迅速に反復処理できるプレビューが複数表示されます。

758a285ad8a6cd51.png

3. マテリアル テーマ設定

Jetpack Compose には、デジタル インターフェースを作成するための包括的なデザイン システムであるマテリアル デザインの実装が用意されています。マテリアル デザインのコンポーネント(ボタン、カード、スイッチなど)は、プロダクトのブランドをより良く反映するようにマテリアル デザインをカスタマイズする体系的な方法である、マテリアル テーマ設定に基づいて構築されています。マテリアル テーマは、タイポグラフィシェイプの属性で構成されています。これらの属性をカスタマイズすると、その変更内容は、アプリのビルドに使用するコンポーネントに自動的に反映されます。

マテリアル テーマ設定について理解しておくと Jetpack Compose アプリのテーマ設定方法を理解しやすくなるため、ここではそのコンセプトについて簡単に説明します。すでにマテリアル テーマ設定についてよくご存じの場合は、読み飛ばしても構いません。

マテリアル デザインでは、アプリ全体で使用できる、意味的に名付けられたが複数定義されています。

62ccfe5761fd9eda.png

プライマリはメインのブランドカラーです。セカンダリはアクセントに使用します。コントラストが強い部分には、さらに暗いまたは明るいバリエーションを提供できます。背景色とサーフェス色は、アプリの「サーフェス」に理論上存在するコンポーネントを保持するコンテナに使用します。マテリアルでは「on」色(指定した色の上に配置されるコンテンツに使用する色)も定義します。たとえば「サーフェス」色のコンテナ内のテキストは、「サーフェス上」の色になります。マテリアル コンポーネントは、こうしたテーマの色を使用するように設定されます。たとえばデフォルトでは、フローティング アクション ボタンの色は secondaryカードのデフォルトは surface などです。

名前付きの色を定義すると、ライトモードとダークモードの両方など、代替のカラーパレットを提供できるようになります。

1a9b78141ddfa87b.png

小さなカラーパレットを定義し、アプリ全体で一貫して使用することもおすすめします。マテリアル カラーツールを使用すると、色を選択してカラーパレットを作成でき、その組み合わせにもアクセスできるようになります。

タイポグラフィ

同様に、マテリアルでは、意味する内容に基づいて名付けられたタイプスタイルがいくつか定義されています。

1d44de3ff2f7fd1c.png

テーマによってタイプスタイルを変えることはできませんが、タイプスケールを使用すると、アプリ内での一貫性が向上します。独自のフォントや他のタイプ カスタマイズを指定すると、アプリで使用するマテリアル コンポーネントに反映されます(アプリバーはデフォルトで h6 スタイルを使用し、ボタンbutton を使用するなど)。タイプスケールを作成するには、マテリアル タイプスケール生成ツールを使用すると便利です。

シェイプ

マテリアルでは、シェイプを使用して体系的にブランドを表現できます。コンポーネントについて小、中、大という 3 つのカテゴリを定義します。それぞれに使用するシェイプを定義でき、角のスタイル(切り落としまたは丸)とサイズをカスタマイズできます。

886b811cc9cad18e.png

シェイプテーマのカスタマイズは多くのコンポーネントに反映されます(ボタンテキスト フィールドは小、カードダイアログは中、シートは大のシェイプテーマをデフォルトで使用するなど)。コンポーネントとシェイプテーマの対応関係について詳しくはこちらをご覧ください。シェイプテーマを生成するには、マテリアル シェイプ カスタマイズ ツールを使用すると便利です。

ベースライン

マテリアルのデフォルトは「ベースライン」テーマです。カラーパターンは紫色、タイプスケールは Roboto であり、シェイプは上の図のようにやや丸みを帯びています。テーマを指定しない場合、またはカスタマイズしない場合、コンポーネントはベースライン テーマを使用します。

4. テーマを定義する

MaterialTheme

Jetpack Compose でテーマ設定を実装するための主な要素は、MaterialTheme コンポーザブルです。このコンポーザブルを Compose 階層に配置すると、その中にあるすべてのコンポーネントの色、タイプ、シェイプのカスタマイズを指定できます。このコンポーザブルは、ライブラリで次のように定義されています。

@Composable
fun MaterialTheme(
    colors: Colors,
    typography: Typography,
    shapes: Shapes,
    content: @Composable () -> Unit
) { ...

このコンポーザブルに渡されたパラメータは、colorstypographyshapes のプロパティを公開する MaterialTheme object を使用して、後で取得できます。これらについては、後ほど詳しく説明します。

Home.kt を開き、コンポーズ可能な関数 Home(アプリのメイン エントリ ポイント)を探します。MaterialTheme を宣言していますが、パラメータを指定していないため、デフォルトの「ベースライン」スタイルが適用されます。

@Composable
fun Home() {
  ...
  MaterialTheme {
    Scaffold(...

色、タイプ、シェイプのパラメータを作成して、アプリのテーマを実装しましょう。

テーマを作成する

スタイル設定を一元管理するために、MaterialTheme をラップして設定する独自のコンポーザブルを作成することをおすすめします。これにより、テーマのカスタマイズを 1 か所で指定し、複数の画面や @Preview など、さまざまな場所で簡単に再利用できるようになります。たとえば、アプリのセクションごとに異なるスタイルをサポートする場合など、必要に応じて複数のテーマ コンポーザブルを作成できます。

com.codelab.theming.ui.start.theme パッケージで、Theme.kt という新しいファイルを作成します。他のコンポーザブルをコンテンツとして受け入れて MaterialTheme をラップする、JetnewsTheme という新しいコンポーズ可能な関数を追加します。

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(content = content)
}

Home.kt に戻り、MaterialThemeJetnewsTheme に置き換えてインポートします。

-  MaterialTheme {
+  JetnewsTheme {
    ...

この画面の @Preview には、まだ変更が反映されていません。プレビューに新しいテーマが使用されるように、PostItemPreviewFeaturedPostPreview を更新して新しい JetnewsTheme コンポーザブルでコンテンツをラップします。

@Preview("Featured Post")
@Composable
private fun FeaturedPostPreview() {
  val post = remember { PostRepo.getFeaturedPost() }
+ JetnewsTheme {
    FeaturedPost(post = post)
+ }
}

アプリに実装するカラーパレットは次のとおりです(まずはライトパレットのみ。ダークモードは後ほどサポートします)。

b2635ed3ec4bfc8f.png

Compose では、Color クラスを使用して色を定義します。コンストラクタが複数あり、ULong として、または個別のカラー チャンネルで色を指定できます。

theme パッケージに新しいファイル Color.kt を作成します。このファイルに、最上位の公開プロパティとして次の色を追加します。

val Red700 = Color(0xffdd0d3c)
val Red800 = Color(0xffd00036)
val Red900 = Color(0xffc20029)

アプリの色を定義したので、今度はそれを MaterialTheme で必要となる Colors オブジェクトにまとめ、マテリアルの名前付きの色に具体的な色を割り当てます。Theme.kt に戻り、次のコードを追加します。

private val LightColors = lightColors(
    primary = Red700,
    primaryVariant = Red900,
    onPrimary = Color.White,
    secondary = Red700,
    secondaryVariant = Red900,
    onSecondary = Color.White,
    error = Red800
)

lightColors 関数を使用して Colors を作成します。適切なデフォルト値が提供されるため、マテリアル カラーパレットの構成色をすべて指定する必要はありません。たとえば、background 色や「on」色の多くを指定していないため、デフォルトを使用します。

それでは、これらの色をアプリに使用しましょう。新しい Colors を使用するように JetnewsTheme コンポーザブルを更新します。

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
+   colors = LightColors,
    content = content
  )
}

Home.kt を開いてプレビューを更新します。TopAppBar などのコンポーネントに新しいカラーパターンが反映されます。

タイポグラフィ

アプリに実装するタイプスケールは次のとおりです。

54c420f78529b77d.png

Compose では、TextStyle オブジェクトを定義して、あるテキストのスタイルを設定するために必要な情報を定義できます。その属性のサンプルを次に示します。

data class TextStyle(
    val color: Color = Color.Unset,
    val fontSize: TextUnit = TextUnit.Inherit,
    val fontWeight: FontWeight? = null,
    val fontStyle: FontStyle? = null,
    val fontFamily: FontFamily? = null,
    val letterSpacing: TextUnit = TextUnit.Inherit,
    val background: Color = Color.Unset,
    val textAlign: TextAlign? = null,
    val textDirection: TextDirection? = null,
    val lineHeight: TextUnit = TextUnit.Inherit,
    ...
)

目的のタイプスケールでは、タイトルに Montserrat を使用し、本文に Domine を使用します。関連するフォント ファイルは、プロジェクトの res/fonts フォルダにすでに追加されています。

theme パッケージに新しいファイル Typography.kt を作成します。まず、FontFamily(各 Font のさまざまなウェイトの組み合わせ)を定義しましょう。

private val Montserrat = FontFamily(
    Font(R.font.montserrat_regular),
    Font(R.font.montserrat_medium, FontWeight.W500),
    Font(R.font.montserrat_semibold, FontWeight.W600)
)

private val Domine = FontFamily(
    Font(R.font.domine_regular),
    Font(R.font.domine_bold, FontWeight.Bold)
)

次に、MaterialTheme が受け入れる Typography オブジェクトを作成し、スケールのセマンティック スタイルごとに TextStyle を指定します。

val JetnewsTypography = Typography(
    h4 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 30.sp
    ),
    h5 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 24.sp
    ),
    h6 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 20.sp
    ),
    subtitle1 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    ),
    subtitle2 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    body1 = TextStyle(
        fontFamily = Domine,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    ),
    body2 = TextStyle(
        fontFamily = Montserrat,
        fontSize = 14.sp
    ),
    button = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    caption = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp
    ),
    overline = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 12.sp
    )
)

Theme.kt を開き、新しい Typography を使用するように JetnewsTheme コンポーザブルを更新します。

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
+   typography = JetnewsTypography,
    content = content
  )
}

Home.kt を開いてプレビューを更新し、新しいタイポグラフィが有効になることを確認します。

シェイプ

アプリでシェイプを使用してブランドを表現します。一部の要素には、角を切り落としたシェイプを使用します。

9b60c78a78c61570.png

Compose には、シェイプテーマを定義するために使用できる RoundedCornerShape クラスと CutCornerShape クラスが用意されています。

theme パッケージに新しいファイル Shape.kt を作成し、次のコードを追加します。

val JetnewsShapes = Shapes(
    small = CutCornerShape(topStart = 8.dp),
    medium = CutCornerShape(topStart = 24.dp),
    large = RoundedCornerShape(8.dp)
)

Theme.kt を開き、これらの Shapes を使用するように JetnewsTheme コンポーザブルを更新します。

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
    typography = JetnewsTypography,
+   shapes = JetnewsShapes,
    content = content
  )
}

Home.kt を開いてプレビューを更新し、注目の投稿を表示する Card に、新しく適用したシェイプテーマがどのように反映されるかを確認します。

ダークモード

アプリでダークモードをサポートすると、ユーザーのデバイス(Android 10 以降のグローバルなダークモードの切り替えがある)にアプリを適切に統合できるだけでなく、電力使用量を削減したり、ユーザー補助ニーズに対応したりできます。マテリアルには、ダークモードの作成に関するデザイン ガイダンスが用意されています。ダークモードに実装する別のカラーパレットを次に示します。

21768b33f0ccda5f.png

Color.kt を開き、次の色を追加します。

val Red200 = Color(0xfff297a2)
val Red300 = Color(0xffea6d7e)

Theme.kt を開き、次のコードを追加します。

private val DarkColors = darkColors(
    primary = Red300,
    primaryVariant = Red700,
    onPrimary = Color.Black,
    secondary = Red300,
    onSecondary = Color.Black,
    error = Red200
)

JetnewsTheme を更新します。

@Composable
fun JetnewsTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  MaterialTheme(
+   colors = if (darkTheme) DarkColors else LightColors,
    typography = JetnewsTypography,
    shapes = JetnewsShapes,
    content = content
  )
}

ダークモードを使用するかどうかを指定する新しいパラメータが追加され、デフォルトでデバイスに全般設定をクエリするようになりました。これだけでも適切なデフォルトになりますが、特定の画面を常に暗くするかまったく暗くしないようにする、あるいはダークモードの @Preview を作成する場合にも、簡単にオーバーライドできます。

Home.kt を開き、ダークモードで表示する FeaturedPost コンポーザブルの新しいプレビューを作成します。

@Preview("Featured Post • Dark")
@Composable
private fun FeaturedPostDarkPreview() {
    val post = remember { PostRepo.getFeaturedPost() }
    JetnewsTheme(darkTheme = true) {
        FeaturedPost(post = post)
    }
}

プレビュー ペインを更新すると、ダークモードのプレビューが表示されます。

84f93b209ce4fd46.png

5. 色の取り扱い

前のステップでは、独自のテーマを作成して、アプリの色、タイプスタイル、シェイプを設定する方法を確認しました。こうしたカスタマイズは、すべてのマテリアル コンポーネントですぐに使用できます。たとえば、FloatingActionButton コンポーザブルはデフォルトでテーマの secondary の色を使用しますが、このパラメータに別の値を指定することで代替色を設定できます。

@Composable
fun FloatingActionButton(
  backgroundColor: Color = MaterialTheme.colors.secondary,
  ...
) {

常にデフォルトの設定を使用するとは限らないため、このセクションではアプリで色を扱う方法について説明します。

未加工の色

前述のとおり、Compose には Color クラスが用意されています。これらはローカルで作成して object などに保持できます。

Surface(color = Color.LightGray) {
  Text(
    text = "Hard coded colors don't respond to theme changes :(",
    textColor = Color(0xffff00ff)
  )
}

Color には、copy など、アルファ、赤、緑、青の値が異なる新しい色を作成できる便利なメソッドが複数用意されています。

テーマの色

さらに柔軟なアプローチとして、テーマから色を取得することが挙げられます。

Surface(color = MaterialTheme.colors.primary)

ここでは、MaterialTheme コンポーザブルに設定された Colorscolors プロパティが返す MaterialTheme object を使用しています。つまり、テーマに異なる色のセットを指定するだけで、アプリコードを編集することなく、さまざまなデザインをサポートできます。たとえば、AppBarprimary の色を使用し、画面の背景は surface です。テーマの色を変更すると、これらのコンポーザブルに反映されます。

b0b0ca02b52453a7.png

253ab041d7ea904e.png

テーマの各色は Color インスタンスであるため、copy メソッドを使用して簡単に色を導出できます。

val derivedColor = MaterialTheme.colors.onSurface.copy(alpha = 0.1f)

ここでは、onSurface の色のコピーを不透明度 10% で作成しています。この方法は、静的な色をハードコードするのではなく、さまざまなテーマで色を機能させます。

サーフェスとコンテンツの色

多くのコンポーネントは、色と「コンテンツ色」のペアを受け入れます。

Surface(
  color: Color = MaterialTheme.colors.surface,
  contentColor: Color = contentColorFor(color),
  ...

TopAppBar(
  backgroundColor: Color = MaterialTheme.colors.primarySurface,
  contentColor: Color = contentColorFor(backgroundColor),
  ...

これにより、コンポーザブルの色を設定するだけでなく、「コンテンツ」(つまりコンポーザブルの中にあるコンポーザブル)にデフォルト色を指定できます。多くのコンポーザブルは、デフォルトでこのコンテンツ色を使用します(Text の色や Icon の色合いなど)。contentColorFor メソッドは、テーマカラーに適した「on」色を取得します。たとえば primary の背景を設定すると、コンテンツ色として onPrimary が返されます。テーマ以外の背景色を設定する場合は、適切なコンテンツ色をご自身で指定する必要があります。

Surface(color = MaterialTheme.colors.primary) {
  Text(...) // default text color is 'onPrimary'
}
Surface(color = MaterialTheme.colors.error) {
  Icon(...) // default tint is 'onError'
}

LocalContentColor CompositionLocal を使用すると、現在の背景と対照的な色を取得できます。

BottomNavigationItem(
  unselectedContentColor = LocalContentColor.current ...

要素の色を設定する際は、適切なコンテンツ色である CompositionLocal 値が設定されるため、Surface を使用することをおすすめします。Modifier.background を直接呼び出すと適切なコンテンツ色が設定されないため、注意が必要です。

-Row(Modifier.background(MaterialTheme.colors.primary)) {
+Surface(color = MaterialTheme.colors.primary) {
+  Row(
...

現在、Header コンポーネントの背景は常に Color.LightGray です。ライトモードでは問題ないようですが、ダークモードでは背景に対して高コントラストになります。また、特定のテキスト色が指定されていないため、現在のコンテンツ色が継承されますが、これは背景と対照的ではない可能性があります。

7329ac6ead5097eb.png

この問題を修正しましょう。Home.ktHeader コンポーザブルで、ハードコードされた色を指定している background 修飾子を削除します。代わりに、Text をテーマ由来の色の Surface でラップし、コンテンツを primary の色にする必要があることを指定します。

+ Surface(
+   color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
+   contentColor = MaterialTheme.colors.primary,
+   modifier = modifier
+ ) {
  Text(
    text = text,
    modifier = Modifier
      .fillMaxWidth()
-     .background(Color.LightGray)
      .padding(horizontal = 16.dp, vertical = 8.dp)
  )
+ }

コンテンツのアルファ

重要性を伝え、視覚的な階層を持たせるために、コンテンツを強調または抑制することはよくあります。マテリアル デザインでは、異なるレベルの透明度を利用してさまざまな重要度レベルを示すことを推奨しています

Jetpack Compose は LocalContentAlpha を介して、これを実装します。この CompositionLocal に値を指定することで、階層のコンテンツのアルファを指定できます。子のコンポーザブルは、この値を使用できます。たとえば、TextIcon は、LocalContentAlpha を使用するように調整された LocalContentColor の組み合わせをデフォルトで使用します。マテリアルは、ContentAlpha オブジェクトによってモデル化される一部の標準的なアルファ値(highmediumdisabled)を指定します。なお、MaterialTheme では LocalContentAlpha のデフォルトが ContentAlpha.high に設定されます。

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting a different content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(...)
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(...)
    Text(...)
}

こうして、コンポーネントの重要性を簡単かつ一貫した方法で伝えることができます。

コンテンツのアルファを使用して、注目の投稿の情報階層を明確にします。Home.ktPostMetadata コンポーザブルで、メタデータ medium を強調します。

+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
  Text(
    text = text,
    modifier = modifier
  )
+ }

103ff62c71744935.png

ダークモード

前述のとおり、Compose でダークモードを実装するには、さまざまな色のセットを指定し、テーマを通じて色をクエリします。ただし、注目すべき例外がいくつかあります。

ライトモードで動作しているかどうかを確認できます。

val isLightTheme = MaterialTheme.colors.isLight

この値は、ビルダー関数 lightColors / darkColors によって設定されます。

マテリアルでは、ダークモードでエレベーションの高いサーフェスにエレベーション オーバーレイが適用されます(背景が明るくなります)。これは、暗いカラーパレットを使用する場合に自動的に実装されます。

Surface(
  elevation = 2.dp,
  color = MaterialTheme.colors.surface, // color will be adjusted for elevation
  ...

アプリのこの自動的な動作は、使用している TopAppBar コンポーネントと Card コンポーネントの両方で発生します。エレベーションはデフォルトで 4 dp と 1 dp に設定されているため、このエレベーションが伝わりやすくなるよう、ダークモードで背景が自動的に明るくなります。

cb8c617b8c151820.png

マテリアル デザインは、ダークモードでは明るい色が大きな面積を占めないようにすることを提案しています。一般的なパターンでは、コンテナをライトモードでは primary の色に、ダークモードでは surface の色にします。アプリバーボトム ナビゲーションなど、多くのコンポーネントがデフォルトでこの戦略を使用します。これを簡単に実装できるように、Colors には、この動作を実現する primarySurface の色が用意されています。これらのコンポーネントはデフォルトでこれを使用します。

このアプリは、アプリバーが現時点では primary の色に設定されています。これを primarySurface に切り替えるか、このデフォルトのパラメータを削除すると、ガイダンスに沿わせることができます。AppBar コンポーザブルで、TopAppBarbackgroundColor パラメータを変更します。

@Composable
private fun AppBar() {
  TopAppBar(
    ...
-   backgroundColor = MaterialTheme.colors.primary
+   backgroundColor = MaterialTheme.colors.primarySurface
  )
}

6. テキストの取り扱い

テキストを扱う場合、Text コンポーザブルを使用してテキストを表示します。テキスト入力には TextFieldOutlinedTextField を使用し、テキストに単一のスタイルを適用するには TextStyle を使用します。AnnotatedString を使用すると、テキストに複数のスタイルを適用できます。

色の場合と同様に、テキストを表示するマテリアル コンポーネントには、テーマのタイポグラフィのカスタマイズが反映されます。

Button(...) {
  Text("This text will use MaterialTheme.typography.button style by default")
}

これを実現することは、色の場合のようにデフォルトのパラメータを使用するよりも、やや複雑です。コンポーネントは、それ自体ではテキストを表示しない傾向があり、Text コンポーザブルを渡せるように「スロット API」を提供するためです。では、コンポーネントはどのようにしてテーマのタイポグラフィ スタイルを設定するのでしょうか。内部では、ProvideTextStyle コンポーザブル(これ自体は CompositionLocal を使用します)を使用して、「現在の」TextStyle が設定されています。具体的な textStyle パラメータを指定しない場合、Text コンポーザブルはデフォルトでこの「現在の」スタイルをクエリします。

Compose の Button クラスと Text クラスの例を次に示します。

@Composable
fun Button(
    // many other parameters
    content: @Composable RowScope.() -> Unit
) {
  ...
  ProvideTextStyle(MaterialTheme.typography.button) { //set the "current" text style
    ...
    content()
  }
}

@Composable
fun Text(
    // many, many parameters
    style: TextStyle = LocalTextStyle.current // get the value set by ProvideTextStyle
) { ...

テーマのテキスト スタイル

色と同様に、現在のテーマから TextStyle を取得することをおすすめします。管理しやすくするために、一貫性のある小規模なスタイルセットを使用しましょう。MaterialTheme.typography は、MaterialTheme コンポーザブルに設定された Typography インスタンスを取得し、定義したスタイルを使用できるようにします。

Text(
  style = MaterialTheme.typography.subtitle2
)

TextStyle をカスタマイズする必要がある場合は、copy によってプロパティ(単なる data class)をオーバーライドします。または Text コンポーザブルが複数のスタイル設定パラメータを受け入れ、TextStyle の上にオーバーレイさせます。

Text(
  text = "Hello World",
  style = MaterialTheme.typography.body1.copy(
    background = MaterialTheme.colors.secondary
  )
)
Text(
  text = "Hello World",
  style = MaterialTheme.typography.subtitle2,
  fontSize = 22.sp // explicit size overrides the size in the style
)

アプリ内の多くの場所でテーマの TextStyle が自動的に適用されます。たとえば、TopAppBar では title のスタイルが h6 に設定され、ListItem ではメインテキストとセカンダリ テキストのスタイルがそれぞれ subtitle1body2 に設定されます。

テーマのタイポグラフィ スタイルをアプリの残りの部分に適用しましょう。subtitle2 を使用するように Header を設定し、タイトルに h6、著者とメタデータに body2 を使用するように FeaturedPost のテキストを設定します。

@Composable
fun Header(...) {
  ...
  Text(
    text = text,
+   style = MaterialTheme.typography.subtitle2

45dbf11d6c1013a0.png

複数のスタイル

あるテキストに複数のスタイルを適用する必要がある場合は、AnnotatedString クラスを使用してマークアップを適用し、テキストの範囲に SpanStyle を追加します。これらを動的に追加することも、DSL 構文を使用してコンテンツを作成することもできます。

val text = buildAnnotatedString {
  append("This is some unstyled text\n")
  withStyle(SpanStyle(color = Color.Red)) {
    append("Red text\n")
  }
  withStyle(SpanStyle(fontSize = 24.sp)) {
    append("Large text")
  }
}

アプリ内の各投稿を記述するタグのスタイルを設定しましょう。現在は、他のメタデータと同じテキスト スタイルを使用しています。overline テキスト スタイルと背景色を使用して区別します。PostMetadata コンポーザブルを次のようにします。

+ val tagStyle = MaterialTheme.typography.overline.toSpanStyle().copy(
+   background = MaterialTheme.colors.primary.copy(alpha = 0.1f)
+ )
post.tags.forEachIndexed { index, tag ->
  ...
+ withStyle(tagStyle) {
    append(" ${tag.toUpperCase()} ")
+ }
}

3f504aaa0a94599a.png

7. シェイプの取り扱い

色、タイポグラフィと同様に、シェイプのテーマを設定すると、マテリアル コンポーネントに反映されます。たとえば Button には、小さなコンポーネントのシェイプセットが反映されます。

@Composable
fun Button( ...
  shape: Shape = MaterialTheme.shapes.small
) {

色と同様に、マテリアル コンポーネントでもデフォルトのパラメータを使用するため、コンポーネントで使用するシェイプのカテゴリの確認や代替手段の提供を簡単に行えます。コンポーネントとシェイプ カテゴリのマッピングの一覧については、ドキュメントをご覧ください。

なお一部のコンポーネントでは、コンテキストに合わせて変更したテーマシェイプが使用されています。たとえばデフォルトで、TextField は小さいシェイプテーマを使用しますが、下の角のサイズを 0 にします。

@Composable
fun FilledTextField(
  // other parameters
  shape: Shape = MaterialTheme.shapes.small.copy(
    bottomStart = ZeroCornerSize, // overrides small theme style
    bottomEnd = ZeroCornerSize // overrides small theme style
  )
) {

1f5fa6cf1355e7a6.png

テーマのシェイプ

コンポーザブル、またはシェイプを受け入れる ModifierSurfaceModifier.clipModifier.backgroundModifier.border など)を使用して独自のコンポーネントを作成するときに、ご自身でシェイプを使用することはもちろん可能です。

@Composable
fun UserProfile(
  ...
  shape: Shape = MaterialTheme.shapes.medium
) {
  Surface(shape = shape) {
    ...
  }
}

PostItem に表示される画像にシェイプのテーマ設定を追加しましょう。clip Modifier でテーマの small シェイプを適用して、左上の角を切り落とします。

@Composable
fun PostItem(...) {
  ...
  Image(
    painter = painterResource(post.imageThumbId),
+   modifier = Modifier.clip(shape = MaterialTheme.shapes.small)
  )

2f989c7c1b8d9e63.png

8. コンポーネント「スタイル」

Compose には、Android View スタイルや CSS スタイルのような、コンポーネントのスタイル設定を抽出する明確な方法がありません。Compose コンポーネントはすべて Kotlin で作成されていることから、同じ目標を達成するには他の方法があります。代わりに、カスタマイズされたコンポーネントの独自のライブラリを作成し、アプリ全体で使用します。

これは、今回のアプリですでに行っています。

@Composable
fun Header(
  text: String,
  modifier: Modifier = Modifier
) {
  Surface(
    color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
    contentColor = MaterialTheme.colors.primary,
    modifier = modifier.semantics { heading() }
  ) {
    Text(
      text = text,
      style = MaterialTheme.typography.subtitle2,
      modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 16.dp, vertical = 8.dp)
    )
  }
}

基本的に Header コンポーザブルはスタイル設定された Text であり、アプリ全体で使用できます。

コンポーネントはすべて下位のビルディング ブロックから構成されていることを確認しましたが、同じビルディング ブロックを使用して、マテリアルのコンポーネントをカスタマイズできます。たとえば、Button では ProvideTextStyle コンポーザブルを使用して、渡されるコンテンツのデフォルト テキスト スタイルを設定していました。まったく同じメカニズムを使用して、独自のテキスト スタイルを設定できます。

@Composable
fun LoginButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonConstants.defaultButtonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier
    ) {
        ProvideTextStyle(...) { // set our own text style
            content()
        }
    }
}

この例では、標準の Button クラスをラップして LoginButton の独自の「スタイル」を作成し、別の backgroundColor やテキスト スタイルのような特定のプロパティを指定しています。

また、デフォルトのスタイル設定という概念(つまりコンポーネント タイプのデフォルトの外観をカスタマイズする方法)もありません。この場合も、ライブラリ コンポーネントをラップしてカスタマイズする独自のコンポーネントを作成することで実現できます。たとえば、アプリ全体ですべての Button のシェイプをカスタマイズするとします。ただし、他の(Button 以外の)コンポーネントに影響する小さなシェイプのテーマは変更しません。これを実現するには、独自のコンポーザブルを作成し、全体で使用します。

@Composable
fun AcmeButton(
  // expose Button params consumers should be able to change
) {
  val acmeButtonShape: Shape = ...
  Button(
    shape = acmeButtonShape,
    // other params
  )
}

9. 完了

これで、この Codelab は終了です。Jetpack Compose アプリのスタイルを設定できました。

マテリアル テーマを実装し、アプリ全体で使用する色、タイポグラフィ、シェイプをカスタマイズすることで、ブランドを表現し、一貫性を高めました。ライトモードとダークモードの両方のサポートを追加しました。

次のステップ

Compose パスウェイに関する他の Codelab をご確認ください。

参考資料

サンプルアプリ

  • 複数のテーマのデモを行う Owl
  • 動的テーマ設定のデモを行う Jetcaster
  • カスタム デザイン システムの実装のデモを行う Jetsnack

リファレンス ドキュメント