Sistemas de design personalizados no Compose

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:

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 ExtendedThemes 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 de composição, definindo diretamente os valores que você quer mudar e expondo outros como parâmetros:

@Composable
fun ExtendedButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = 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, você pode substituir um ou mais sistemas, Colors, Typography ou Shapes, por uma implementação personalizada, mantendo os outros.

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 de composição, definindo diretamente os valores do sistema relevante e expondo outros como parâmetros.

@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 e Shapes: sistemas de Temas do Material Design
  • ContentAlpha: níveis de opacidade para transmitir ênfase em Text e Icon
  • TextSelectionColors: cores usadas para seleção de texto por Text e TextField
  • Ripple e RippleTheme: implementação de Indication pelo Material Design

Para continuar usando os componentes do Material Design, substitua alguns desses sistemas nos seus temas personalizados ou processe-os 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 comportamento de indicação e valores de cor, tipo e forma indesejados.

Para usar valores personalizados em componentes, envolva-os nas suas próprias funções de composição, definindo diretamente os valores do sistema relevante e expondo outros como parâmetros:

É 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(
            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 = ContentAlpha.disabled)
        ),
        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)

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, consulte JetsnackButton no exemplo do Jetsnack.