マテリアルは Google が推奨するデザイン システムであり、Jetpack Compose にはマテリアルの実装が同梱されていますが、これを使用するように強制されているわけではありません。マテリアルは完全に公開 API に基づいて構築されているため、同じ手法で独自のデザイン システムを作成できます。
独自のデザイン システムは、以下の複数のアプローチで作成できます。
- テーマ値を追加して
MaterialTheme
を拡張する - 1 つ以上のマテリアル システム(
Colors
、Typography
、Shapes
)をカスタム実装に置き換えながら、他のシステムはそのまま使用する - フルカスタム デザイン システムを実装し、
MaterialTheme
を置き換える
カスタム デザイン システムでも引き続きマテリアル コンポーネントを使用することもできます。ただしその場合、採用したアプローチに合わせて留意すべき点があります。
MaterialTheme
およびカスタム デザイン システムで使用される下位の構造と API の詳細については、Compose のテーマの構造ガイドをご覧ください。
マテリアル テーマを拡張する
Compose Material は、マテリアル テーマ設定を綿密にモデル化し、マテリアル ガイドラインに沿った、シンプルかつタイプセーフなものにします。ただし、色、タイポグラフィ、シェイプのセットに値を追加して拡張することは可能です。
最も簡単なアプローチは、拡張プロパティを追加する方法です。
// Use with MaterialTheme.colors.snackbarAction
val Colors.snackbarAction: Color
get() = if (isLight) 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 自体によって定義される拡張プロパティの例として、primarySurface
があります。これは、Colors.isLight
に応じて primary
と surface
の間のプロキシとして機能します。
もう一つのアプローチは、MaterialTheme
とその値を「ラップ」する拡張テーマを定義することです。
既存のマテリアル カラーを維持しながら、tertiary
と onTertiary
の 2 つのカラーを追加する場合は、以下のようになります。
@Immutable
data class ExtendedColors(
val tertiary: Color,
val onTertiary: Color
)
val LocalExtendedColors = staticCompositionLocalOf {
ExtendedColors(
tertiary = Color.Unspecified,
onTertiary = Color.Unspecified
)
}
@Composable
fun ExtendedTheme(
/* ... */
content: @Composable () -> Unit
) {
val extendedColors = ExtendedColors(
tertiary = Color(0xFFA8EFF0),
onTertiary = Color(0xFF002021)
)
CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
MaterialTheme(
/* colors = ..., typography = ..., shapes = ... */
content = content
)
}
}
// Use with eg. ExtendedTheme.colors.tertiary
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(
backgroundColor = ExtendedTheme.colors.tertiary,
contentColor = ExtendedTheme.colors.onTertiary
/* 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
: マテリアル テーマ設定システムContentAlpha
:Text
とIcon
で強調を表現する不透明度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(
backgroundColor = CustomTheme.colors.component,
contentColor = CustomTheme.colors.content,
disabledBackgroundColor = CustomTheme.colors.content
.copy(alpha = 0.12f)
.compositeOver(CustomTheme.colors.component),
disabledContentColor = CustomTheme.colors.content
.copy(alpha = ContentAlpha.disabled)
),
shape = ButtonShape,
elevation = ButtonDefaults.elevation(
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
をご覧ください。