Material Design 2 no Compose

O Jetpack Compose oferece uma implementação do Material Design, um sistema de design abrangente para criar interfaces digitais. Os componentes do Material Design (botões, cards, chaves e assim por diante) são baseados nos Temas do Material Design, uma maneira sistemática de personalizar o Material Design para refletir melhor a marca do seu produto. Um tema do Material Design contém atributos de cor, tipografia e forma. Quando você personaliza esses atributos, as mudanças são refletidas automaticamente nos componentes usados para criar o app.

O Jetpack Compose implementa esses conceitos com o elemento MaterialTheme que pode ser composto:

MaterialTheme(
    colors = // ...
    typography = // ...
    shapes = // ...
) {
    // app content
}

Configure os parâmetros transmitidos a MaterialTheme para aplicar o tema no aplicativo.

Duas capturas de tela contrastantes. A primeira usa o estilo padrão do MaterialTheme, e a segunda usa um estilo modificado.
Figura 1. A primeira captura de tela mostra um app que não configura o `MaterialTheme` e, portanto, usa o estilo padrão. A segunda captura de tela mostra um app que transmite parâmetros para "MaterialTheme" para personalizar o estilo.

Cor

As cores são modeladas no Compose com a classe Color, uma classe de retenção de dados.

val Red = Color(0xffff0000)
val Blue = Color(red = 0f, green = 0f, blue = 1f)

Embora você possa organizá-las da forma que quiser (como constantes de nível superior, em um singleton ou definidas in-line), é altamente recomendável especificar as cores no seu tema e recuperar as cores nele. Essa abordagem permite oferecer suporte ao tema escuro e a temas aninhados.

Exemplo da paleta de cores do tema
Figura 2. O sistema de cores do Material.

O Compose oferece a classe Colors para modelar o sistema de cores do Material. O Colors oferece funções de builder para criar conjuntos de cores claras ou escuras:

private val Yellow200 = Color(0xffffeb46)
private val Blue200 = Color(0xff91a4fc)
// ...

private val DarkColors = darkColors(
    primary = Yellow200,
    secondary = Blue200,
    // ...
)
private val LightColors = lightColors(
    primary = Yellow500,
    primaryVariant = Yellow400,
    secondary = Blue700,
    // ...
)

Depois de definir seus Colors, você pode transmiti-los a um MaterialTheme:

MaterialTheme(
    colors = if (darkTheme) DarkColors else LightColors
) {
    // app content
}

Usar cores de tema

É possível recuperar as Colors fornecidas ao MaterialTheme que pode ser composto usando MaterialTheme.colors.

Text(
    text = "Hello theming",
    color = MaterialTheme.colors.primary
)

Cor da superfície e do conteúdo

Muitos componentes aceitam um par de cores e de cores de conteúdo:

Surface(
    color = MaterialTheme.colors.surface,
    contentColor = contentColorFor(color),
    // ...
) { /* ... */ }

TopAppBar(
    backgroundColor = MaterialTheme.colors.primarySurface,
    contentColor = contentColorFor(backgroundColor),
    // ...
) { /* ... */ }

Isso permite não apenas definir a cor de um elemento combinável, mas também fornecer uma cor padrão para o conteúdo,os elementos combináveis contidos nele. Muitos elementos que podem ser compostos usam essa cor de conteúdo por padrão. Por exemplo, Text baseia a cor dele na cor do conteúdo do pai, e Icon usa essa cor para definir a própria tonalidade.

Dois exemplos do mesmo banner, com cores diferentes
Figura 3. Definir cores de plano de fundo diferentes produz cores de texto e ícone diferentes.

O método contentColorFor() extrai a cor "on" adequada para todas as cores do tema. Por exemplo, se você definir uma cor de fundo primary em Surface, essa função será usada para definir onPrimary como a cor do conteúdo. Se você definir uma cor que não seja do tema para o plano de fundo, também vai precisar especificar uma cor para o conteúdo adequada. Use LocalContentColor para extrair a cor de conteúdo preferida para o plano de fundo atual, em uma determinada posição na hierarquia.

Conteúdo Alfa

Muitas vezes, você quer variar o nível de ênfase no conteúdo para comunicar a importância e apresentar uma hierarquia visual. As recomendações de legibilidade de texto do Material Design (link em inglês) aconselham o uso de diferentes níveis de opacidade para transmitir níveis de importância variados.

O Jetpack Compose implementa isso usando LocalContentAlpha. É possível especificar um Alfa de conteúdo para uma hierarquia fornecendo um valor para esse CompositionLocal. Os elementos combináveis aninhados podem usar esse valor para aplicar o tratamento alfa ao conteúdo. Por exemplo, Text e Icon, por padrão, usam a combinação de LocalContentColor ajustada para usar LocalContentAlpha. O Material Design especifica alguns valores Alfa padrão (high, medium e disabled), que são modelados pelo objeto ContentAlpha.

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(
        // ...
    )
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(
        // ...
    )
    Text(
        // ...
    )
}

Para saber mais sobre CompositionLocal, consulte Dados com escopo local com CompositionLocal.

Captura de tela do título de um artigo mostrando diferentes níveis de ênfase no texto
Figura 4. Aplique diferentes níveis de ênfase ao texto para comunicar visualmente a hierarquia das informações. A primeira linha de texto é o título e tem as informações mais importantes. Por isso, ela usa ContentAlpha.high. A segunda linha contém metadados menos importantes e, portanto, usa ContentAlpha.medium.

Tema escuro

No Compose, você pode implementar temas claros e escuros ao fornecer conjuntos diferentes de Colors para o elemento MaterialTheme que pode ser composto:

@Composable
fun MyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
        /*...*/
        content = content
    )
}

Neste exemplo, o MaterialTheme é encapsulado na própria função que pode ser composta, que aceita um parâmetro que especifica se é necessário ou não usar um tema escuro. Nesse caso, a função recebe o valor padrão para darkTheme consultando a configuração do tema do dispositivo.

Você pode usar um código como este para conferir se o Colors atual é claro ou escuro:

val isLightTheme = MaterialTheme.colors.isLight
Icon(
    painterResource(
        id = if (isLightTheme) {
            R.drawable.ic_sun_24
        } else {
            R.drawable.ic_moon_24
        }
    ),
    contentDescription = "Theme"
)

Sobreposições de elevação

No Material Design, as superfícies em temas escuros com elevações mais altas recebem sobreposições de elevação, que clareiam o plano de fundo. Quanto maior a elevação de uma superfície (aproximando-a de uma fonte de luz implícita), mais clara ela fica.

O elemento combinável Surface aplica essas sobreposições automaticamente ao usar cores escuras, assim como qualquer outro elemento combinável do Material que usa uma superfície:

Surface(
    elevation = 2.dp,
    color = MaterialTheme.colors.surface, // color will be adjusted for elevation
    /*...*/
) { /*...*/ }

Captura de tela de um app mostrando as cores sutilmente diferentes usadas para elementos em diferentes níveis de elevação
Figura 5. Os cards e a navegação na parte de baixo estão usando a cor surface como plano de fundo. Como os cards e a navegação na parte de baixo estão em diferentes níveis de elevação acima do plano de fundo, eles têm cores ligeiramente diferentes. Os cards são mais claros que o plano de fundo, e a navegação na parte de baixo é mais clara que os cards.

Para cenários personalizados que não envolvem uma Surface, use LocalElevationOverlay, um CompositionLocal que contém a ElevationOverlay usada por componentes Surface:

// Elevation overlays
// Implemented in Surface (and any components that use it)
val color = MaterialTheme.colors.surface
val elevation = 4.dp
val overlaidColor = LocalElevationOverlay.current?.apply(
    color, elevation
)

Para desativar as sobreposições de elevação, forneça null no ponto escolhido em uma hierarquia de elementos combináveis:

MyTheme {
    CompositionLocalProvider(LocalElevationOverlay provides null) {
        // Content without elevation overlays
    }
}

Tons de cores limitados

O Material recomenda aplicar tons de cores limitados para temas escuros, preferindo o uso da cor surface em vez da cor primary na maioria dos casos. Os elementos que podem ser compostos do Material Design, como TopAppBar e BottomNavigation, implementam esse comportamento por padrão.

Captura de tela de um tema escuro do Material, mostrando a barra de apps superior usando a cor da superfície em vez da cor principal para tons de cores limitados
Figura 6. Tema escuro do Material Design com tons de cor limitados. A barra de apps na parte de cima usa a cor primária no tema claro e a cor da superfície no tema escuro.

Para cenários personalizados, use a propriedade de extensão primarySurface:

Surface(
    // Switches between primary in light theme and surface in dark theme
    color = MaterialTheme.colors.primarySurface,
    /*...*/
) { /*...*/ }

Tipografia

O Material define um sistema de tipos (link em inglês), incentivando o uso de um pequeno número de estilos com nomes semânticos.

Exemplo de várias fontes diferentes em vários estilos
Figura 7. O sistema de tipografia do Material Design.

O Compose implementa o sistema de tipos com as classes Typography, TextStyle e relacionadas a fontes. O construtor Typography oferece padrões para cada estilo. Assim, você pode omitir os que não quiser personalizar:

val raleway = FontFamily(
    Font(R.font.raleway_regular),
    Font(R.font.raleway_medium, FontWeight.W500),
    Font(R.font.raleway_semibold, FontWeight.SemiBold)
)

val myTypography = Typography(
    h1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    )
    /*...*/
)
MaterialTheme(typography = myTypography, /*...*/) {
    /*...*/
}

Se você quiser usar o mesmo tipo de letra em todo o texto, especifique o parâmetro defaultFontFamily e omita o fontFamily de qualquer elemento TextStyle:

val typography = Typography(defaultFontFamily = raleway)
MaterialTheme(typography = typography, /*...*/) {
    /*...*/
}

Usar estilos de texto

Os elementos TextStyle são acessados usando MaterialTheme.typography. Recupere os elementos TextStyle da seguinte maneira:

Text(
    text = "Subtitle2 styled",
    style = MaterialTheme.typography.subtitle2
)

Captura de tela mostrando uma mistura de fontes diferentes para finalidades distintas
Figura 8. Use uma seleção de famílias tipográficas e estilos para expressar sua marca.

Forma

O Material define um sistema de formas, permitindo que você defina formas para componentes grandes, médios e pequenos.

Mostra uma variedade de formas do Material Design
Figura 9. O sistema de formas do Material.

O Compose implementa o sistema de formas com a classe Shapes, que permite especificar um CornerBasedShape para cada categoria de tamanho:

val shapes = Shapes(
    small = RoundedCornerShape(percent = 50),
    medium = RoundedCornerShape(0f),
    large = CutCornerShape(
        topStart = 16.dp,
        topEnd = 0.dp,
        bottomEnd = 0.dp,
        bottomStart = 16.dp
    )
)

MaterialTheme(shapes = shapes, /*...*/) {
    /*...*/
}

Muitos componentes usam essas formas por padrão. Por exemplo, Button, TextField e FloatingActionButton são pequenos por padrão, AlertDialog é médio por padrão e ModalDrawer é grande por padrão. Consulte a referência do esquema de forma para o mapeamento completo.

Usar formas

Os elementos Shape são acessados usando MaterialTheme.shapes. Recupere os elementos Shape com um código como este:

Surface(
    shape = MaterialTheme.shapes.medium, /*...*/
) {
    /*...*/
}

Captura de tela de um app que usa formas do Material Design para transmitir o estado em que um elemento se encontra
Figura 10. Use formas para expressar a marca ou o estado.

Estilos padrão

Não há um conceito equivalente no Compose de estilos padrão do Android Views. Você pode oferecer uma funcionalidade semelhante criando suas próprias funções combináveis overload que encapsulam componentes do Material. Por exemplo, para criar um estilo de botão, envolva um botão na sua própria função combinável, definindo diretamente os parâmetros que você quer ou precisa alterar e expondo outros como parâmetros para o elemento combinável que o contém.

@Composable
fun MyButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

Sobreposições de tema

Para conseguir o equivalente às sobreposições de tema do Android Views no Compose, aninhe elementos combináveis MaterialTheme. Como MaterialTheme define as cores, a tipografia e as formas com o valor do tema atual, todos os outros parâmetros mantêm os valores padrão quando um tema define apenas um desses parâmetros.

Além disso, ao migrar telas baseadas em visualização para o Compose, preste atenção aos usos do atributo android:theme. É provável que você precise de um novo MaterialTheme nessa parte da árvore de interface do Compose.

Neste exemplo, a tela de detalhes usa um PinkTheme para a maior parte da tela e um BlueTheme para a seção relacionada. A captura de tela e o código a seguir ilustram esse conceito:

Captura de tela de um app demonstrando temas aninhados, com um tema rosa para a tela principal e um tema azul para uma seção relacionada
Figura 11. Temas aninhados.

@Composable
fun DetailsScreen(/* ... */) {
    PinkTheme {
        // other content
        RelatedSection()
    }
}

@Composable
fun RelatedSection(/* ... */) {
    BlueTheme {
        // content
    }
}

Estados dos componentes

Os componentes do Material Design com que você pode interagir (clicar, alternar etc.) podem estar em diferentes estados visuais. Alguns estados: ativado, desativado, pressionado etc.

Os elementos que podem ser compostos geralmente têm um parâmetro enabled. Defini-lo como false impede a interação e muda propriedades como cor e elevação para expressar visualmente o estado do componente.

Captura de tela de dois botões: um ativado e outro desativado, mostrando seus diferentes estados visuais
Figura 12. Botão com enabled = true (esquerda) e enabled = false (direita).

Na maioria dos casos, você pode confiar nos valores padrão de cor e elevação. Se você precisar configurar valores usados em estados diferentes, há classes e funções de conveniência disponíveis. Confira este exemplo de botão:

Button(
    onClick = { /* ... */ },
    enabled = true,
    // Custom colors for different states
    colors = ButtonDefaults.buttonColors(
        backgroundColor = MaterialTheme.colors.secondary,
        disabledBackgroundColor = MaterialTheme.colors.onBackground
            .copy(alpha = 0.2f)
            .compositeOver(MaterialTheme.colors.background)
        // Also contentColor and disabledContentColor
    ),
    // Custom elevation for different states
    elevation = ButtonDefaults.elevation(
        defaultElevation = 8.dp,
        disabledElevation = 2.dp,
        // Also pressedElevation
    )
) { /* ... */ }

Captura de tela de dois botões com cor e elevação ajustadas para estados ativados e desativados
Figura 13. Botão com enabled = true (esquerda) e enabled = false (direita), com valores de cor e elevação ajustados.

Ondulações

Os componentes do Material usam ondulações para indicar que estão sendo interagidos. Se você estiver usando MaterialTheme na sua hierarquia, um Ripple será usado como o Indication padrão em modificadores como clickable e indication.

Na maioria dos casos, você pode confiar na Ripple padrão. Se você precisar configurar a aparência, use RippleTheme para mudar propriedades como cor e alfa.

É possível estender RippleTheme e usar as funções utilitárias defaultRippleColor e defaultRippleAlpha. Em seguida, forneça seu tema de ondulação personalizado na hierarquia usando LocalRippleTheme:

@Composable
fun MyApp() {
    MaterialTheme {
        CompositionLocalProvider(
            LocalRippleTheme provides SecondaryRippleTheme
        ) {
            // App content
        }
    }
}

@Immutable
private object SecondaryRippleTheme : RippleTheme {
    @Composable
    override fun defaultColor() = RippleTheme.defaultRippleColor(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )

    @Composable
    override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )
}

GIF animado mostrando botões com diferentes efeitos de ondulação ao serem tocados
Figura 14. Botões com diferentes valores de ondulação fornecidos usando RippleConfiguration.

Saiba mais

Para saber mais sobre os temas do Material Design no Compose, consulte os recursos a seguir.

Codelabs

Vídeos