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

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

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

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

Чтобы узнать больше о низкоуровневых конструкциях и 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:

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

  • Colors , Typography и Shapes : системы тематизации материалов
  • 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
}

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

Если 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.

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}