Embora o Material Design seja o sistema de design recomendado e o Jetpack Compose use uma implementação do Material Design, você tem outras opções. O Material Design foi criado com base em APIs públicas. Por isso, é possível criar seu próprio sistema de design da mesma maneira.
Há várias abordagens possíveis:
- Extensão do
MaterialTheme
com outros valores de temas - Substituição de um ou mais sistemas do Material Design (
Colors
,Typography
ouShapes
) por implementações personalizadas, mantendo os outros - Implementação de um sistema de design totalmente personalizado para
substituir o
MaterialTheme
Talvez você também queira continuar usando os componentes do Material Design em um sistema de design personalizado. É possível fazer isso, mas há alguns detalhes que precisam ser lembrados para atender à abordagem adotada.
Para saber mais sobre as construções de nível inferior e as APIs usadas no MaterialTheme
e em sistemas de design personalizados, consulte o guia Anatomia de um tema no Compose.
Como estender os Temas do Material Design
O Compose modela os Temas do Material Design com cuidado para simplificar e seguir as diretrizes do Material Design com segurança de tipo. No entanto, é possível estender os conjuntos de cores, tipografias e formas com outros valores.
A abordagem mais simples é a adição de propriedades de extensão:
// 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)
Isso possibilita manter a consistência com as APIs de uso do MaterialTheme
. Um exemplo disso,
definido pelo próprio Compose, é o
primarySurface
,
que atua como um proxy entre primary
e surface
, dependendo do
Colors.isLight
.
Outra abordagem possível é a definição de um tema estendido que "envolva" MaterialTheme
e
os valores dele.
Você pode querer adicionar mais duas cores (tertiary
e onTertiary
), por exemplo,
mantendo as cores do Material Design existentes:
@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
}
Isso é semelhante às APIs de uso do MaterialTheme
. Essa abordagem também é compatível com vários temas,
já que você pode aninhar ExtendedTheme
s da mesma forma que MaterialTheme
.
Como usar componentes do Material Design
Ao estender os Temas do Material Design, os valores MaterialTheme
existentes são mantidos
e os componentes do Material Design mantêm padrões razoáveis.
Para usar valores estendidos em componentes, envolva-os nas suas próprias funções que podem ser compostas, definindo diretamente os valores que você quer alterar e expondo outros como parâmetros à função que pode ser composta:
@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
)
}
É necessário substituir os usos de Button
por ExtendedButton
sempre que for
adequado.
@Composable
fun ExtendedApp() {
ExtendedTheme {
/*...*/
ExtendedButton(onClick = { /* ... */ }) {
/* ... */
}
}
}
Como substituir sistemas do Material Design
Em vez de estender os temas do Material Design, talvez você queira substituir um ou mais
sistemas, Colors
, Typography
ou Shapes
, por uma implementação personalizada,
mantendo os outros ao mesmo tempo.
Você pode querer, por exemplo, substituir os sistemas de tipo e forma e, ao mesmo tempo, manter o sistema de cores:
@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
}
Como usar componentes do Material Design
Quando um ou mais sistemas de MaterialTheme
forem substituídos, o uso de componentes
do Material Design inalterados poderá resultar em valores indesejados de cor, tipo ou forma.
Para usar valores de substituição em componentes, envolva-os nas suas próprias funções que podem ser compostas, definindo diretamente os valores do sistema relevante e expondo outros como parâmetros à função que pode ser composta.
@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()
}
}
)
}
Será necessário substituir os usos de Button
por ReplacementButton
sempre que for
adequado.
@Composable
fun ReplacementApp() {
ReplacementTheme {
/*...*/
ReplacementButton(onClick = { /* ... */ }) {
/* ... */
}
}
}
Como implementar um sistema de design totalmente personalizado
É possível substituir os Temas do Material Design por um sistema de design totalmente personalizado.
O MaterialTheme
oferece os seguintes sistemas:
Colors
,Typography
eShapes
: sistemas de Temas do Material DesignContentAlpha
: níveis de opacidade para transmitir ênfase emText
eIcon
TextSelectionColors
: cores usadas para seleção de texto porText
eTextField
Ripple
eRippleTheme
: implementação do Material Design deIndication
Se você quiser continuar usando os componentes do Material Design, precisará substituir alguns desses sistemas nos seus temas personalizados ou processá-los nos seus componentes para evitar comportamentos indesejados.
No entanto, os sistemas de design não estão limitados aos conceitos de que o Material Design depende. Você pode modificar os sistemas existentes e introduzir sistemas totalmente novos, com novas classes e tipos, para tornar outros conceitos compatíveis com temas.
No código a seguir, modelamos um sistema de cores personalizado com gradientes
(List<Color>
), incluímos um sistema de tipos, apresentamos um novo sistema de elevação
e excluímos outros sistemas fornecidos pelo 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
}
Como usar componentes do Material Design
Quando não houver MaterialTheme
, o uso de componentes do Material Design inalterados resultará
em valores de cor, tipo e forma indesejados e comportamento de indicação.
Para usar valores personalizados em componentes, envolva-os nas suas próprias funções que podem ser compostas, definindo diretamente os valores do sistema relevante e expondo outros como parâmetros à função que pode ser composta:
É recomendado acessar os valores definidos usando o tema personalizado. Se
o tema não fornecer Color
, TextStyle
, Shape
ou outros sistemas, você
também poderá fixá-lo no código.
@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)
Se você introduziu novos tipos de classe, como List<Color>
para representar gradientes, pode ser melhor implementar componentes do zero em vez de envolvê-los. Por exemplo, dê uma olhada em
JetsnackButton
na amostra do Jetsnack.