マテリアルは Google が推奨するデザイン システムであり、Jetpack Compose にはマテリアルの実装が同梱されていますが、これを使用するように強制されているわけではありません。マテリアルは完全に公開 API に基づいて構築されているため、同じ方法で独自のデザイン システムを作成できます。
独自のデザイン システムは、以下の複数のアプローチで作成できます。
- 追加のテーマ設定値を使用して
MaterialThemeを拡張します。 - 1 つ以上のマテリアル システム(
ColorScheme、Typography、Shapes)をカスタム実装に置き換え、他はそのままにします。 MaterialThemeに代わる完全なカスタム デザイン システムを実装する。
また、カスタム デザイン システムでマテリアル コンポーネントを引き続き使用することもできます。これは可能ですが、採用したアプローチに合わせて留意すべき点があります。
MaterialTheme およびカスタム デザイン システムで使用される下位の構造と API の詳細については、Compose のテーマの構造ガイドをご覧ください。
マテリアル テーマ設定を拡張する
Compose Material は、マテリアル テーマ設定を厳密にモデル化し、マテリアル ガイドラインに沿って、わかりやすく、型安全性に作成できるようにしています。ただし、色、タイポグラフィ、シェイプのセットに値を追加して拡張することもできます。最も簡単なアプローチは、拡張プロパティを追加する方法です。
// Use with MaterialTheme.colorScheme.snackbarAction val ColorScheme.snackbarAction: Color @Composable get() = if (isSystemInDarkTheme()) Red300 else Red700 // Use with MaterialTheme.typography.textFieldInput val Typography.textFieldInput: TextStyle get() = TextStyle(/* ... */) // Use with MaterialTheme.shapes.card val Shapes.card: Shape get() = RoundedCornerShape(size = 20.dp)
これにより、MaterialTheme を使用する API との整合性が確保されます。Compose 自体で定義されているこの例としては、surfaceColorAtElevation があります。これは、エレベーションに応じて使用するサーフェスの色を決定します。
別のアプローチとして、MaterialTheme とその値を「ラップ」する拡張テーマを定義する方法があります。
既存のマテリアル カラーを維持しながら、caution と onCaution(準危険なアクションに使用される黄色)の 2 つの色を追加するとします。
@Immutable data class ExtendedColors( val caution: Color, val onCaution: Color ) val LocalExtendedColors = staticCompositionLocalOf { ExtendedColors( caution = Color.Unspecified, onCaution = Color.Unspecified ) } @Composable fun ExtendedTheme( /* ... */ content: @Composable () -> Unit ) { val extendedColors = ExtendedColors( caution = Color(0xFFFFCC02), onCaution = Color(0xFF2C2D30) ) CompositionLocalProvider(LocalExtendedColors provides extendedColors) { MaterialTheme( /* colors = ..., typography = ..., shapes = ... */ content = content ) } } // Use with eg. ExtendedTheme.colors.caution object ExtendedTheme { val colors: ExtendedColors @Composable get() = LocalExtendedColors.current }
これは、MaterialTheme を使用する API に似ています。また、複数のテーマをサポートしており、MaterialTheme と同じ方法で ExtendedTheme をネストできます。
マテリアル コンポーネントを使用する
マテリアル テーマを拡張する際、既存の MaterialTheme 値は維持され、マテリアル コンポーネントには引き続き適切なデフォルトが適用されます。
コンポーネントで拡張値を使用する場合は、独自のコンポーズ可能な関数でラップし、変更する値を直接設定して、他の値をコンテナのコンポーザブルにパラメータとして公開します。
@Composable fun ExtendedButton( onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit ) { Button( colors = ButtonDefaults.buttonColors( containerColor = ExtendedTheme.colors.caution, contentColor = ExtendedTheme.colors.onCaution /* Other colors use values from MaterialTheme */ ), onClick = onClick, modifier = modifier, content = content ) }
その後、必要に応じて Button の使用を ExtendedButton に置き換えます。
@Composable fun ExtendedApp() { ExtendedTheme { /*...*/ ExtendedButton(onClick = { /* ... */ }) { /* ... */ } } }
マテリアル サブシステムを置き換える
マテリアル テーマ設定を拡張する代わりに、1 つ以上のシステム(ColorScheme、Typography、Shapes)をカスタム実装に置き換え、他のシステムはそのままにすることもできます。
カラーシステムを維持しながら、タイプシステムとシェイプ システムを置き換える場合は、次のようにします。
@Immutable data class ReplacementTypography( val body: TextStyle, val title: TextStyle ) @Immutable data class ReplacementShapes( val component: Shape, val surface: Shape ) val LocalReplacementTypography = staticCompositionLocalOf { ReplacementTypography( body = TextStyle.Default, title = TextStyle.Default ) } val LocalReplacementShapes = staticCompositionLocalOf { ReplacementShapes( component = RoundedCornerShape(ZeroCornerSize), surface = RoundedCornerShape(ZeroCornerSize) ) } @Composable fun ReplacementTheme( /* ... */ content: @Composable () -> Unit ) { val replacementTypography = ReplacementTypography( body = TextStyle(fontSize = 16.sp), title = TextStyle(fontSize = 32.sp) ) val replacementShapes = ReplacementShapes( component = RoundedCornerShape(percent = 50), surface = RoundedCornerShape(size = 40.dp) ) CompositionLocalProvider( LocalReplacementTypography provides replacementTypography, LocalReplacementShapes provides replacementShapes ) { MaterialTheme( /* colors = ... */ content = content ) } } // Use with eg. ReplacementTheme.typography.body object ReplacementTheme { val typography: ReplacementTypography @Composable get() = LocalReplacementTypography.current val shapes: ReplacementShapes @Composable get() = LocalReplacementShapes.current }
マテリアル コンポーネントを使用する
MaterialTheme の 1 つ以上のシステムが置き換えられたときにマテリアル コンポーネントをそのまま使用すると、不要なマテリアルの色、タイプ、またはシェイプの値が返されることがあります。
コンポーネントで置換値を使用する場合は、独自のコンポーズ可能な関数でラップし、関連するシステムに値を直接設定して、他の値をコンテナ コンポーズ可能な関数のパラメータとして公開します。
@Composable fun ReplacementButton( onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit ) { Button( shape = ReplacementTheme.shapes.component, onClick = onClick, modifier = modifier, content = { ProvideTextStyle( value = ReplacementTheme.typography.body ) { content() } } ) }
その後、必要に応じて Button の使用を ReplacementButton に置き換えます。
@Composable fun ReplacementApp() { ReplacementTheme { /*...*/ ReplacementButton(onClick = { /* ... */ }) { /* ... */ } } }
完全にカスタムのデザイン システムを実装する
マテリアル テーマ設定を完全にカスタムのデザイン システムに置き換えることもできます。MaterialTheme が次のシステムを提供しているとします。
ColorScheme、Typography、Shapes: マテリアル テーマ設定システムTextSelectionColors:TextとTextFieldによるテキスト選択に使用される色ripple(LocalIndicationとして提供):Indicationのマテリアル実装
マテリアル コンポーネントを引き続き使用する場合は、カスタムテーマでこれらのシステムの一部を置き換えるか、コンポーネントでシステムを処理して、望ましくない動作を回避する必要があります。
ただし、デザイン システムはマテリアルが依存するコンセプトに限定されません。既存のシステムを変更して、新しいクラスと型を備えたまったく新しいシステムを導入し、他のコンセプトをテーマに対応させることができます。
次のコードでは、グラデーション(List<Color>)を含むカスタム カラーシステムをモデル化し、タイプシステムを含め、新しいエレベーション システムを導入して、MaterialTheme で提供される他のシステムを除外しています。
@Immutable data class CustomColors( val content: Color, val component: Color, val background: List<Color> ) @Immutable data class CustomTypography( val body: TextStyle, val title: TextStyle ) @Immutable data class CustomElevation( val default: Dp, val pressed: Dp ) val LocalCustomColors = staticCompositionLocalOf { CustomColors( content = Color.Unspecified, component = Color.Unspecified, background = emptyList() ) } val LocalCustomTypography = staticCompositionLocalOf { CustomTypography( body = TextStyle.Default, title = TextStyle.Default ) } val LocalCustomElevation = staticCompositionLocalOf { CustomElevation( default = Dp.Unspecified, pressed = Dp.Unspecified ) } @Composable fun CustomTheme( /* ... */ content: @Composable () -> Unit ) { val customColors = CustomColors( content = Color(0xFFDD0D3C), component = Color(0xFFC20029), background = listOf(Color.White, Color(0xFFF8BBD0)) ) val customTypography = CustomTypography( body = TextStyle(fontSize = 16.sp), title = TextStyle(fontSize = 32.sp) ) val customElevation = CustomElevation( default = 4.dp, pressed = 8.dp ) CompositionLocalProvider( LocalCustomColors provides customColors, LocalCustomTypography provides customTypography, LocalCustomElevation provides customElevation, content = content ) } // Use with eg. CustomTheme.elevation.small object CustomTheme { val colors: CustomColors @Composable get() = LocalCustomColors.current val typography: CustomTypography @Composable get() = LocalCustomTypography.current val elevation: CustomElevation @Composable get() = LocalCustomElevation.current }
マテリアル コンポーネントを使用する
MaterialTheme が存在しない場合、マテリアル コンポーネントをそのまま使用すると、不要なマテリアルの色、タイプ、シェイプの値および表示動作が発生します。
コンポーネントでカスタム値を使用する場合は、独自のコンポーズ可能な関数でラップし、関連するシステムに値を直接設定して、他の値をパラメータとしてコンテナ コンポーズ可能な関数に公開します。
カスタムテーマから設定した値にアクセスすることをおすすめします。また、テーマで Color、TextStyle、Shape などのシステムが提供されていない場合は、ハードコードすることもできます。
@Composable fun CustomButton( onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit ) { Button( colors = ButtonDefaults.buttonColors( containerColor = CustomTheme.colors.component, contentColor = CustomTheme.colors.content, disabledContainerColor = CustomTheme.colors.content .copy(alpha = 0.12f) .compositeOver(CustomTheme.colors.component), disabledContentColor = CustomTheme.colors.content .copy(alpha = 0.38f) ), shape = ButtonShape, elevation = ButtonDefaults.elevatedButtonElevation( defaultElevation = CustomTheme.elevation.default, pressedElevation = CustomTheme.elevation.pressed /* disabledElevation = 0.dp */ ), onClick = onClick, modifier = modifier, content = { ProvideTextStyle( value = CustomTheme.typography.body ) { content() } } ) } val ButtonShape = RoundedCornerShape(percent = 50)
グラデーションを表す List<Color> などの新しいクラスタイプを導入した場合は、コンポーネントをラップするのではなく、ゼロから実装する方がよいでしょう。例として、Jetsnack サンプルの JetsnackButton をご覧ください。
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- Compose のマテリアル デザイン 3
- Compose でマテリアル 2 からマテリアル 3 に移行する
- Compose のテーマの構造