Системы индивидуального дизайна в Compose

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

Вы можете использовать несколько подходов:

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

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

Расширение тематического оформления материалов

Compose Material очень точно воспроизводит Material Theming , что делает его простым и типобезопасным для следования рекомендациям 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)

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

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

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

@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 Theming существующие значения MaterialTheme сохраняются, и компоненты Material по-прежнему имеют разумные значения по умолчанию.

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

@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 Theming вы можете заменить одну или несколько систем — 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 использование компонентов Material в неизменном виде может привести к нежелательным значениям цвета, типа или формы, характерным для Material.

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

@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 Theme на полностью настраиваемую систему дизайна. Учтите, что MaterialTheme предоставляет следующие возможности:

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

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

Однако системы проектирования не ограничиваются концепциями, на которых основан 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
}

Используйте материальные компоненты

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

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

Мы рекомендуем использовать значения, заданные в вашей пользовательской теме. В качестве альтернативы, если ваша тема не поддерживает 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.

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}