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

Хотя Material — наша рекомендуемая дизайн-система, а Jetpack Compose включает в себя её реализацию, вы не обязаны её использовать. 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 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 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 %}