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

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

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

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

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

対照的な 2 つのスクリーンショット。1 つ目のスクリーンショットはデフォルトの MaterialTheme スタイルを使用し、2 つ目のスクリーンショットは変更されたスタイルを使用しています。
図 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」色を取得します。たとえば、Surface に primary の背景色を設定すると、この関数を使用して onPrimary をコンテンツ色として設定します。テーマ以外の背景色を設定する場合は、適切なコンテンツ色も指定する必要があります。LocalContentColor を使用して、階層内の特定の位置にある現在の背景の推奨コンテンツ色を取得します。

ないため、注意が必要です。

コンテンツのアルファ版

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

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

// 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. テキストにさまざまなレベルの強調を適用して、情報の階層を視覚的に伝えます。テキストの 1 行目はタイトルであり、最も重要な情報が含まれているため、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 を含まないカスタム シナリオの場合は、LocalElevationOverlay を使用します。これは、Surface コンポーネントで使用される ElevationOverlay を含む CompositionLocal です。

// 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 色を優先して使用することで、ダークモードに限定的な色のアクセントを適用することを推奨しています。TopAppBar や BottomNavigation などのマテリアル コンポーザブルは、デフォルトでこの動作を実装しています。

マテリアル ダークモードのスクリーンショット。色のアクセントを限定するために、プライマリ カラーではなくサーフェス カラーを使用している上部のアプリバーが表示されている
図 6. 色のアクセントが限定されたマテリアル ダークモード。ライトモードではプライマリ カラー、ダークモードではサーフェス カラーがトップアプリバーに使用されます。

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

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

タイポグラフィ

マテリアルでは、タイプシステムが定義されており、意味する内容に基づいて名付けられたスタイルを少数使用することが推奨されています。

さまざまなスタイルの各種書体の例
図 7. マテリアル タイプ システム。

Compose は、Typography クラス、TextStyle クラス、フォント関連クラスを使用して型システムを実装します。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 パラメータを指定し、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, /*...*/) {
    /*...*/
}

多くのコンポーネントで、こうしたシェイプがデフォルトで使用されます。たとえば、Button、TextField、FloatingActionButton はデフォルトで small、AlertDialog はデフォルトで medium、ModalDrawer はデフォルトで large になります。完全なマッピングについては、シェイプ スキームの参照をご覧ください。

図形を使用する

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

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

マテリアル シェイプを使用して要素の状態を伝えるアプリのスクリーンショット
図 10. 図形を使用してブランドや状態を表します。

デフォルト スタイル

Compose には、Android View のデフォルト スタイルに相当するコンセプトはありません。Material コンポーネントをラップする独自の overload コンポーザブル関数を作成することで、同様の機能を提供できます。たとえば、ボタンのスタイルを作成するには、独自のコンポーズ可能な関数でボタンをラップし、変更したいパラメータを直接設定して、他のパラメータをコンテナ コンポーザブルのパラメータとして公開します。

@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
    )
}

テーマ オーバーレイ

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

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

この例では、詳細画面の大部分に PinkTheme を使用し、関連セクションに BlueTheme を使用しています。次のスクリーンショットとコードは、このコンセプトを示しています。

ネストされたテーマを示すアプリのスクリーンショット。メイン画面はピンクのテーマ、関連セクションは青のテーマが適用されている
図 11. ネストされたテーマ。

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

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

コンポーネントの状態

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

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

有効なボタンと無効なボタンの 2 つのボタンのスクリーンショット。それぞれの視覚的な状態が示されている
図 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
    )
) { /* ... */ }

有効状態と無効状態の 2 つのボタンの色と標高が調整されたスクリーンショット
図 13. enabled = true(左)と enabled = false(右)のボタン。色と標高の値を調整しています。

Ripples

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

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

RippleTheme を拡張して、defaultRippleColor ユーティリティ関数と defaultRippleAlpha ユーティリティ関数を利用できます。次に、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
    )
}

タップしたときにさまざまなリップル効果が表示されるボタンを示す GIF アニメーション
図 14. RippleConfiguration を使用して異なるリップル値を指定したボタン。

詳細

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

Codelab

動画