Compose のマテリアル デザイン 2

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

Jetpack Compose は、MaterialTheme コンポーザブルを使用して、こうしたコンセプトを実装します。

MaterialTheme(
    colors = // ...
    typography = // ...
    shapes = // ...
) {
    // app content
}

MaterialTheme に渡すパラメータを構成して、アプリのテーマを設定します。

対照的な 2 つのスクリーンショット。1 つ目のスクリーンショットはデフォルトの MaterialTheme スタイリングを使用し、2 つ目のスクリーンショットは変更したスタイリングを使用しています。

図 1. 1 つ目のスクリーンショットは MaterialTheme を構成していないアプリを示しています。そのため、デフォルトのスタイリングが使用されています。2 つ目のスクリーンショットは、スタイリングをカスタマイズするために MaterialTheme にパラメータを渡すアプリを示しています。

Compose では、色はシンプルなデータ保持クラスである Color クラスによってモデル化されます。

val Red = Color(0xffff0000)
val Blue = Color(red = 0f, green = 0f, blue = 1f)

これらはどのように整理してもかまいませんが(最上位の定数として、シングルトン内で、またはインラインで定義)、テーマで色を指定し、そこから色を取得することを強くおすすめします。この方法により、ダークテーマやネストされたテーマを簡単にサポートできます。

テーマのカラーパレットの例

図 2. マテリアル カラーシステム。

Compose には、マテリアル カラーシステムをモデル化するための Colors クラスが用意されています。Colors は、明るい色または暗い色のセットを作成するためのビルダー関数を提供します。

private val Yellow200 = Color(0xffffeb46)
private val Blue200 = Color(0xff91a4fc)
// ...

private val DarkColors = darkColors(
    primary = Yellow200,
    secondary = Blue200,
    // ...
)
private val LightColors = lightColors(
    primary = Yellow500,
    primaryVariant = Yellow400,
    secondary = Blue700,
    // ...
)

Colors を定義したら、MaterialTheme に渡すことができます。

MaterialTheme(
    colors = if (darkTheme) DarkColors else LightColors
) {
    // app content
}

テーマ色の使用

MaterialTheme.colors を使用すると、MaterialTheme コンポーザブルに提供されている Colors を取得できます。

Text(
    text = "Hello theming",
    color = MaterialTheme.colors.primary
)

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

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

Surface(
    color = MaterialTheme.colors.surface,
    contentColor = contentColorFor(color),
    // ...
) { /* ... */ }

TopAppBar(
    backgroundColor = MaterialTheme.colors.primarySurface,
    contentColor = contentColorFor(backgroundColor),
    // ...
) { /* ... */ }

これにより、コンポーザブルの色を設定するだけでなく、コンテンツ(コンポーザブルの中に含まれるコンポーザブル)のデフォルト色も指定できます。多くのコンポーザブルは、デフォルトでこのコンテンツ色を使用します。たとえば、Text の色は親のコンテンツ色に基づき、Icon はその色を使用して色合いを設定します。

同じバナーで色が異なる 2 つの例

図 3. 異なる背景色を設定すると、テキストとアイコンが異なる色になります。

contentColorFor() メソッドは、テーマカラーに適した「on」色を取得します。たとえば、Surfaceprimary の背景色を設定すると、この関数を使用して onPrimary がコンテンツ色として設定されます。テーマ以外の背景色を設定する場合は、適切なコンテンツ色も指定する必要があります。現在の背景で優先するコンテンツ色を取得するには、階層内の特定の位置で LocalContentColor を使用します。

コンテンツのアルファ版

重要度を伝え、視覚的な階層を提供するために、コンテンツを強調する度合いを変化させることがよくあります。マテリアル デザインのテキストの読みやすさに関する推奨事項では、異なるレベルの透明度を利用してさまざまな重要度レベルを示すことを推奨しています。

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

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

CompositionLocal の詳細については、CompositionLocal でローカルにスコープ設定されたデータについてのガイドをご覧ください。

さまざまなレベルのテキスト強調を示す、記事タイトルのスクリーンショット

図 4. テキストにさまざまなレベルの強調を適用して、情報の階層を視覚的に伝えます。テキストの最初の行はタイトルで、最も重要な情報があるため、ContentAlpha.high を使用します。2 行目にはそれよりも重要度の低いメタデータが含まれているため、ContentAlpha.medium を使用します。

ダークテーマ

Compose では、MaterialTheme コンポーザブルにさまざまな Colors のセットを指定することで、ライトテーマとダークテーマを実装します。

@Composable
fun MyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
        /*...*/
        content = content
    )
}

この例では、MaterialTheme が独自のコンポーズ可能な関数でラップされています。この関数は、ダークテーマを使用するかどうかを指定するパラメータを受け入れます。この場合、関数はデバイスのテーマ設定をクエリすることで、darkTheme のデフォルト値を取得します。

次のようなコードを使用すると、現在の Colors がライトテーマかダークテーマかを確認できます。

val isLightTheme = MaterialTheme.colors.isLight
Icon(
    painterResource(
        id = if (isLightTheme) {
            R.drawable.ic_sun_24
        } else {
            R.drawable.ic_moon_24
        }
    ),
    contentDescription = "Theme"
)

エレベーション オーバーレイ

マテリアルでは、エレベーションの高いダークテーマのサーフェスは、背景を明るくするエレベーション オーバーレイを受け取ります。サーフェスのエレベーションが高くなるほど(暗黙的な光源に近づくほど)、サーフェスは明るくなります。

こうしたオーバーレイは、暗い色を使用している場合や、サーフェスを使用するその他のマテリアル コンポーザブルの場合、Surface コンポーザブルによって自動的に実装されます。

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

エレベーション レベルの異なる要素に使用される、微妙に異なる色を示すアプリのスクリーンショット

図 5. カードとボトム ナビゲーションはどちらも背景として surface 色が使用されています。カードとボトム ナビゲーションは背景よりレベルが高い異なるエレベーションに存在するため、色がわずかに異なります。カードは背景よりも明るく、ボトム ナビゲーションはカードよりも明るくなります。

Surface を含まないカスタム シナリオの場合、Surface コンポーネントによって使用される ElevationOverlay を含む CompositionLocalLocalElevationOverlay を使用します。

// Elevation overlays
// Implemented in Surface (and any components that use it)
val color = MaterialTheme.colors.surface
val elevation = 4.dp
val overlaidColor = LocalElevationOverlay.current?.apply(
    color, elevation
)

エレベーション オーバーレイを無効にするには、コンポーザブル階層内の適切なポイントで null を指定します。

MyTheme {
    CompositionLocalProvider(LocalElevationOverlay provides null) {
        // Content without elevation overlays
    }
}

限定的なカラー アクセント

マテリアルでは、ほとんどの場面で primary カラーよりも surface カラーの使用を優先することで、ダークテーマに限定的なカラー アクセントを適用することをおすすめします。TopAppBarBottomNavigation などのマテリアル コンポーザブルは、デフォルトでこの動作を実装しています。

図 6. 限定的なカラー アクセントを使用したマテリアル ダークテーマ。上部のアプリバーは、ライトテーマでプライマリ色を使用し、ダークテーマでサーフェス色を使用しています。

カスタム シナリオでは、primarySurface 拡張プロパティを使用します。

Surface(
    // Switches between primary in light theme and surface in dark theme
    color = MaterialTheme.colors.primarySurface,
    /*...*/
) { /*...*/ }

タイポグラフィ

マテリアルはタイプシステムを定義し、意味的に名前をつけたスタイルを少数使用するよう推奨しています。

さまざまなスタイルの各種書体の例

図 7. マテリアルのタイプシステム。

Compose は、TypographyTextStyleフォント関連のクラスでタイプシステムを実装しています。Typography コンストラクタは各スタイルのデフォルトを提供するため、カスタマイズしないものは省略できます。

val raleway = FontFamily(
    Font(R.font.raleway_regular),
    Font(R.font.raleway_medium, FontWeight.W500),
    Font(R.font.raleway_semibold, FontWeight.SemiBold)
)

val myTypography = Typography(
    h1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    )
    /*...*/
)
MaterialTheme(typography = myTypography, /*...*/) {
    /*...*/
}

全体を通じて同じ書体を使用する場合は、defaultFontFamily parameter を指定し、TextStyle 要素の fontFamily を省略します。

val typography = Typography(defaultFontFamily = raleway)
MaterialTheme(typography = typography, /*...*/) {
    /*...*/
}

テキスト スタイルの使用

TextStyle へのアクセスには MaterialTheme.typography が使用されます。次のように TextStyle を取得します。

Text(
    text = "Subtitle2 styled",
    style = MaterialTheme.typography.subtitle2
)

目的が異なるさまざまな書体の混在を示すスクリーンショット

図 8. 書体とスタイルを使い分けてブランドを表現します。

シェイプ

マテリアルはシェイプ システムを定義しており、大、中、小のコンポーネントのシェイプを定義できます。

さまざまなマテリアル デザイン シェイプを示しています

図 9. マテリアルのシェイプ システム。

Compose は Shapes クラスでシェイプ システムを実装しており、サイズカテゴリごとに CornerBasedShape を指定できます。

val shapes = Shapes(
    small = RoundedCornerShape(percent = 50),
    medium = RoundedCornerShape(0f),
    large = CutCornerShape(
        topStart = 16.dp,
        topEnd = 0.dp,
        bottomEnd = 0.dp,
        bottomStart = 16.dp
    )
)

MaterialTheme(shapes = shapes, /*...*/) {
    /*...*/
}

多くのコンポーネントで、こうしたシェイプがデフォルトで使用されます。たとえば、ButtonTextFieldFloatingActionButton のデフォルトは small、AlertDialog のデフォルトは medium、ModalDrawer のデフォルトは large です。マッピングの詳細については、シェイプ スキームのリファレンスをご覧ください。

シェイプの使用

Shape へのアクセスには MaterialTheme.shapes が使用されます。次のようなコードを使用して Shape を取得します。

Surface(
    shape = MaterialTheme.shapes.medium, /*...*/
) {
    /*...*/
}

マテリアル シェイプを使用して要素の状態を伝えるアプリのスクリーンショット

図 10. シェイプを使用してブランドや状態を表します。

デフォルト スタイル

Android View のデフォルト スタイルの Compose には、同等の概念はありません。マテリアル コンポーネントをラップする独自の「オーバーロード」したコンポーズ可能な関数を作成することで、同様の機能を実現できます。たとえば、ボタンのスタイルを作成するには、独自のコンポーズ可能な関数でボタンをラップし、変更するパラメータを直接設定します。他のパラメータは包含コンポーザブルにパラメータとして公開します。

@Composable
fun MyButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

テーマ オーバーレイ

MaterialTheme コンポーザブルをネストすることで、Android View のテーマ オーバーレイと同等の機能を Compose で実現できます。MaterialTheme では色、タイポグラフィ、シェイプが現在のテーマ値にデフォルトで設定されるため、テーマによってこれらのパラメータのうちの 1 つしか設定されていない場合、他のパラメータはデフォルト値を保持します。

さらに、View ベースの画面を Compose に移行する場合は、android:theme 属性の使い方に注意してください。Compose UI ツリーの該当部分に新しい MaterialTheme が必要になる場合があります。

Owl サンプルでは、詳細画面にはほとんどの画面で PinkTheme が使用され、関連するセクションには BlueTheme が使用されています。以下のスクリーンショットとコードをご覧ください。

図 11. Owl サンプルのネストされたテーマ。

@Composable
fun DetailsScreen(/* ... */) {
    PinkTheme {
        // other content
        RelatedSection()
    }
}

@Composable
fun RelatedSection(/* ... */) {
    BlueTheme {
        // content
    }
}

コンポーネントの状態

クリックや切り替えなど、操作可能なマテリアル コンポーネントには、さまざまな視覚状態が存在します。状態には「enabled」、「disabled」、「press」などがあります。

コンポーザブルには、多くの場合 enabled パラメータが含まれます。false に設定すると、操作ができなくなり、色やエレベーションなどのプロパティが変更され、コンポーネントの状態を視覚的に伝えることができます。

図 12. enabled = true(左)と enabled = false(右)を設定したボタン。

ほとんどの場合、色やエレベーションなどの値にはデフォルト値を使用できます。それぞれ異なる状態で使用される値を構成する場合は、クラスと便利な関数を利用できます。下のボタンの例をご覧ください。

Button(
    onClick = { /* ... */ },
    enabled = true,
    // Custom colors for different states
    colors = ButtonDefaults.buttonColors(
        backgroundColor = MaterialTheme.colors.secondary,
        disabledBackgroundColor = MaterialTheme.colors.onBackground
            .copy(alpha = 0.2f)
            .compositeOver(MaterialTheme.colors.background)
        // Also contentColor and disabledContentColor
    ),
    // Custom elevation for different states
    elevation = ButtonDefaults.elevation(
        defaultElevation = 8.dp,
        disabledElevation = 2.dp,
        // Also pressedElevation
    )
) { /* ... */ }

図 13. 色とエレベーションの値が調整された enabled = true(左)と enabled = false(右)を設定したボタン。

リップル

マテリアル コンポーネントは、リップルを使用して、操作されていることを示します。階層内で MaterialTheme を使用している場合、修飾子内(clickableindication など)で Ripple がデフォルトの Indication として使用されます。

ほとんどの場合、デフォルトの Ripple を使用できます。リップルの外観を構成するには、RippleTheme を使用して色やアルファなどのプロパティを変更します。

RippleTheme を拡張することで、defaultRippleColordefaultRippleAlpha ユーティリティ関数を使用できます。その後、LocalRippleTheme を使用することで、階層内でカスタム リップルテーマを指定できます。

@Composable
fun MyApp() {
    MaterialTheme {
        CompositionLocalProvider(
            LocalRippleTheme provides SecondaryRippleTheme
        ) {
            // App content
        }
    }
}

@Immutable
private object SecondaryRippleTheme : RippleTheme {
    @Composable
    override fun defaultColor() = RippleTheme.defaultRippleColor(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )

    @Composable
    override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )
}

alt_text

図 14. RippleTheme で異なるリップル値を指定したボタン

詳細

Compose のマテリアル テーマ設定の詳細については、以下の参考情報をご覧ください。

Codelab

動画