マテリアルは Google が推奨するデザイン システムであり、Jetpack Compose にはマテリアルの実装が同梱されていますが、これを使用するように強制されているわけではありません。マテリアルは完全に公開 API に基づいて構築されているため、同じ手法で独自のデザイン システムを作成できます。
独自のデザイン システムは、以下の複数のアプローチで作成できます。
- テーマ値を追加して
MaterialTheme
を拡張する - 1 つ以上のマテリアル システム(
Colors
、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 つ以上のシステム(Colors
、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
が次のシステムを提供しているとします。
Colors
、Typography
、Shapes
: マテリアル テーマ設定システムTextSelectionColors
:Text
とTextField
によるテキスト選択に使用される色Ripple
、RippleTheme
: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 内のテーマの構造