Собственные системы проектирования в Compose

Мы рекомендуем использовать систему проектирования Material, и в Jetpack Compose есть ее реализация, но вы не обязаны это делать. Material полностью основан на общедоступных API, поэтому вы можете создать собственную систему дизайна аналогичным образом.

Вот несколько вариантов:

Вы также можете использовать компоненты Material с собственной системой дизайна. Это возможно, но нужно учитывать некоторые нюансы.

Чтобы узнать больше о конструкциях более низкого уровня и API, используемых MaterialTheme и пользовательскими системами дизайна, ознакомьтесь с руководством Анатомия темы в Compose.

Как расширить возможности тематического оформления Material Design

Compose Material тесно связан с тематическим оформлением Material Design, поэтому следовать рекомендациям Material Design легко и типобезопасно. Однако наборы цветов, шрифтов и форм можно расширить, добавив в них дополнительные значения. Проще всего добавить свойства расширения:

// 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)

Это обеспечивает согласованность с API использования MaterialTheme. Примером такого определения в Compose является surfaceColorAtElevation, которое определяет цвет поверхности в зависимости от высоты.

Другой подход заключается в том, чтобы определить расширенную тему, которая "обертывает" MaterialTheme и его значения.

Предположим, вы хотите добавить два дополнительных цвета – caution и onCaution, желтый цвет, используемый для действий, которые являются полуопасными, – сохранив при этом существующие цвета Material:

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

Это похоже на API использования MaterialTheme. Кроме того, он поддерживает несколько тем, поскольку теги ExtendedTheme можно вкладывать друг в друга так же, как и теги MaterialTheme.

Как использовать компоненты Material

При расширении тематического оформления Material Design сохраняются существующие значения MaterialTheme, а у компонентов Material Design остаются разумные значения по умолчанию.

Если вы хотите использовать расширенные значения в компонентах, оберните их в собственные composable-функции, непосредственно задав значения, которые нужно изменить, и предоставив другие значения в качестве параметров для содержащей composable-функции:

@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 = { /* ... */ }) {
            /* ... */
        }
    }
}

Замена подсистем материалов

Вместо того чтобы расширять возможности тематического оформления Material Design, вы можете заменить одну или несколько систем (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
}

Как использовать компоненты Material

Если один или несколько системных компонентов MaterialTheme были заменены, использование компонентов Material в том виде, в котором они есть, может привести к нежелательным значениям цвета, типа или формы Material.

Если вы хотите использовать в компонентах значения для замены, оберните их в собственные composable-функции, задав значения для нужной системы и предоставив другие значения в качестве параметров для содержащей composable-функции.

@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 = { /* ... */ }) {
            /* ... */
        }
    }
}

Как реализовать полностью настраиваемую систему проектирования

Вы можете заменить тематическое оформление Material Design полностью настраиваемой системой дизайна. Компания MaterialTheme предоставляет следующие системы:

  • Colors, Typography и Shapes – системы тематического оформления Material Design.
  • TextSelectionColors – цвета, используемые для выделения текста в Text и TextField.
  • Ripple и RippleTheme – материальная реализация Indication.

Если вы хотите продолжать использовать компоненты Material, вам нужно заменить некоторые из этих систем в своих темах или обработать системы в своих компонентах, чтобы избежать нежелательного поведения.

Однако дизайн-системы не ограничиваются концепциями, на которых основан Material. Вы можете изменять существующие системы и создавать совершенно новые, добавляя новые классы и типы, чтобы другие концепции были совместимы с темами.

В следующем коде мы моделируем собственную систему цветов, которая включает градиенты (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
}

Как использовать компоненты Material

Если MaterialTheme отсутствует, использование компонентов Material в том виде, в котором они есть, приведет к нежелательным значениям цвета, типа и формы, а также к нежелательному поведению индикаторов.

Если вы хотите использовать в компонентах собственные значения, оберните их в свои composable-функции, напрямую задав значения для нужной системы и предоставив другие значения в качестве параметров для содержащей composable-функции.

Мы рекомендуем получать доступ к заданным значениям из собственной темы. Если в вашей теме нет 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> для градиентов, то лучше реализовать компоненты с нуля, а не оборачивать их. Например, посмотрите на файл JetsnackButton из примера Jetsnack.