Layouts no Jetpack Compose

1. Introdução

No codelab de noções básicas do Jetpack Compose, você aprendeu a criar IUs simples com o Compose usando elementos de composição (como Text) e elementos de composição de layout flexíveis (como Column e Row), que permitem posicionar itens de forma vertical e horizontal na tela, respectivamente, além de configurar o alinhamento dos elementos dentro dele. Já com a Box, é possível posicionar itens atrás ou à frente de outros, caso você não queira que eles sejam mostrados na vertical nem na horizontal.

fbd450e8eab10338.png

Você pode usar esses componentes de layout padrão para criar IUs como esta:

d2c39f3c2416c321.png

@Composable
fun PhotographerProfile(photographer: Photographer) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(...)
        Column {
            Text(photographer.name)
            Text(photographer.lastSeenOnline, ...)
        }
    }
}

Graças à capacidade de composição e reutilização do Compose, você pode criar seus próprios elementos combinando as diferentes partes necessárias no nível correto de abstração em uma nova função de composição.

Neste codelab, você vai aprender a usar o nível de abstração de IU mais alto do Compose, o Material Design (link em inglês) e os elementos de composição de baixo nível, como o Layout, que permite medir e posicionar elementos na tela.

Se você quiser criar uma IU baseada no Material Design, o Compose inclui componentes do Material (links em inglês) integrados que podem ser usados, conforme veremos neste codelab. Caso seu objetivo não seja usar o Material Design ou você queira criar algo que não esteja dentro das especificações dele, também vamos mostrar como criar layouts personalizados.

O que você vai aprender

Neste codelab, você vai aprender o seguinte:

  • Como usar componentes do Material (link em inglês).
  • O que são modificadores e como eles podem ser usados em layouts.
  • Como criar um layout personalizado.
  • Quando pode ser necessário usar intrínsecos.

Pré-requisitos

O que será necessário

2. Iniciar um novo projeto do Compose

Para iniciar um novo projeto do Compose, abra o Android Studio Bumblebee e selecione Start a new Android Studio project, conforme mostrado a seguir:

ec53715fe31913e6.jpeg

Caso a tela acima não seja mostrada, acesse File > New > New Project.

Ao criar um novo projeto, selecione a opção Empty Compose Activity nos modelos disponíveis.

a67ba73a4f06b7ac.png

Clique em Next e configure seu projeto normalmente. Selecione uma minimumSdkVersion com API de nível 21 ou posterior. Esse é o nível mínimo de API com suporte no Compose.

Ao selecionar o modelo Empty Compose Activity, o seguinte código será gerado no seu projeto:

  • O projeto já está configurado para usar o Compose.
  • O arquivo AndroidManifest.xml foi criado.
  • O arquivo app/build.gradle (ou build.gradle (Module: YourApplicationName.app)) importa dependências do Compose e permite que o Android Studio funcione com ele usando a sinalização buildFeatures { compose true }.
android {
    ...
    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
    }
}

dependencies {
    ...
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation 'androidx.activity:activity-compose:1.4.0'
    implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling:$compose_version"
    ...
}

Solução do codelab

O código da solução deste codelab está disponível no GitHub:

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

Se preferir, faça o download do repositório como um arquivo ZIP:

O código da solução está disponível no projeto LayoutsCodelab. Recomendamos que você siga o codelab passo a passo em seu próprio ritmo e, se necessário, consulte a solução. Durante o codelab, você verá snippets de código que precisam ser adicionados ao projeto.

3. Modificadores

Os modificadores permitem decorar um elemento de composição. Você pode mudar o comportamento e a aparência dos elementos, adicionar informações (como marcadores de acessibilidade), processar entradas do usuário ou até mesmo adicionar interações de alto nível, como fazer com que um elemento clicável, rolável, arrastável ou redimensionável. Modificadores são objetos normais do Kotlin. Eles podem ser atribuídos a variáveis e reutilizados. Também é possível encadear vários modificadores um após o outro para fazer a composição.

Vamos implementar o layout de perfil mostrado na introdução:

d2c39f3c2416c321.png

Abra o arquivo MainActivity.kt e adicione o seguinte:

@Composable
fun PhotographerCard() {
    Column {
        Text("Alfred Sisley", fontWeight = FontWeight.Bold)
        // LocalContentAlpha is defining opacity level of its children
        CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
            Text("3 minutes ago", style = MaterialTheme.typography.body2)
        }
    }
}

@Preview
@Composable
fun PhotographerCardPreview() {
    LayoutsCodelabTheme {
        PhotographerCard()
    }
}

Esta é a prévia:

bf29f2c3f5d6a27.png

Enquanto a imagem carrega, talvez você queira mostrar um marcador de posição. Para isso, use uma Surface, onde especificamos o formato de círculo e a cor do marcador. Para especificar o tamanho, podemos usar um modificador size:

@Composable
fun PhotographerCard() {
    Row {
        Surface(
            modifier = Modifier.size(50.dp),
            shape = CircleShape,
            color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
        ) {
            // Image goes here
        }
        Column {
            Text("Alfred Sisley", fontWeight = FontWeight.Bold)
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("3 minutes ago", style = MaterialTheme.typography.body2)
            }
        }
    }
}

84f2bb229d67987b.png

Agora, vamos fazer algumas melhorias:

  1. Adicionar uma separação entre o marcador de posição e o texto
  2. Centralizar o texto verticalmente

Para o primeiro caso, podemos usar Modifier.padding na Column que contém o texto de modo a adicionar um espaço no start do elemento de composição e separar a imagem do texto. Já para o segundo caso, alguns layouts oferecem modificadores que podem ser aplicados apenas a si próprios e às características de layout. Por exemplo, os elementos de composição em uma Row podem acessar alguns modificadores do receptor RowScope de conteúdo da linha que funcionam nesse tipo de layout, como weight ou align. Essa definição de escopo proporciona segurança de tipo para que você não use acidentalmente um modificador que não faz sentido em outro layout. Por exemplo, weight não funciona em uma Box, então um erro de tempo de compilação impediria esse uso.

@Composable
fun PhotographerCard() {
    Row {
        Surface(
            modifier = Modifier.size(50.dp),
            shape = CircleShape,
            color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
        ) {
            // Image goes here
        }
        Column(
            modifier = Modifier
                .padding(start = 8.dp)
                .align(Alignment.CenterVertically)
        ) {
            Text("Alfred Sisley", fontWeight = FontWeight.Bold)
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("3 minutes ago", style = MaterialTheme.typography.body2)
            }
        }
    }
}

Esta é a prévia:

1542fadc7f68feb2.png

A maioria dos elementos de composição aceitam um parâmetro de modificador opcional para se tornarem mais flexíveis, autorizando o autor da chamada a fazer modificações neles. Caso você esteja criando seu próprio elemento de composição, considere usar um modificador como parâmetro, definir Modifier como o padrão dele, ou seja, um modificador vazio que não faz nada, e aplicá-lo ao elemento raiz da sua função. Neste caso:

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
    Row(modifier) { ... }
}

A ordem dos modificadores é importante

No código, observe de que forma você pode encadear vários modificadores um após o outro usando as funções de extensão de fábrica (ou seja, Modifier.padding(start = 8.dp).align(Alignment.CenterVertically)).

Preste atenção ao encadear modificadores, porque a ordem é importante. Por estarem concatenados em um único argumento, a ordem deles afeta o resultado final.

Para fazer com que o perfil do fotógrafo seja clicável e adicionar padding a ele, você pode usar algo como:

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
    Row(modifier
        .padding(16.dp)
        .clickable(onClick = { /* Ignoring onClick */ })
    ) {
        ...
    }
}

Usando a prévia interativa ou executando o código em um emulador, o resultado será este:

c15a1050b051617f.gif

Observe que não é possível clicar em toda a área. Isso ocorre porque o modificador padding foi aplicado antes do modificador clickable. Se aplicarmos o padding depois do clickable, o padding será incluído na área clicável:

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
    Row(modifier
        .clickable(onClick = { /* Ignoring onClick */ })
        .padding(16.dp)
    ) {
        ...
    }
}

Usando a prévia interativa ou executando o código em um emulador, o resultado será este:

a1ea4c8e16d61ffa.gif

Deixe a imaginação fluir. Os modificadores permitem modificar os elementos de composição de forma muito flexível. Por exemplo, se você quisesse adicionar espaçamento externo, mudar a cor do plano de fundo do elemento e arredondar os cantos da Row, poderia usar este código:

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
    Row(modifier
        .padding(8.dp)
        .clip(RoundedCornerShape(4.dp))
        .background(MaterialTheme.colors.surface)
        .clickable(onClick = { /* Ignoring onClick */ })
        .padding(16.dp)
    ) {
        ...
    }
}

Usando a prévia interativa ou executando o código em um emulador, o resultado será este:

4c7652fc71ccf8dc.gif

Veremos mais sobre como os modificadores funcionam internamente posteriormente neste codelab.

4. APIs de slots

O Compose oferece componentes do Material (link em inglês) de alto nível, que podem ser usados para criar sua IU. Como eles são fundamentais para criar a IU, ainda será necessário fornecer as informações que serão mostradas na tela.

As APIs de slot são um padrão apresentado pelo Compose para oferecer uma camada de personalização junto aos elementos de composição, que neste caso de uso, são os componentes disponíveis do Material.

Vejamos um exemplo:

Os botões do Material (link em inglês) seguem diretrizes específicas com relação à aparência e ao conteúdo, que podem ser traduzidas em uma API simples para uso:

Button(text = "Button")

b3cb99320ec18268.png

Contudo, muitas vezes você pode querer personalizar os componentes de formas diferentes das já previstas. Nós poderíamos tentar adicionar um parâmetro para cada elemento que seria personalizado, mas perderíamos o controle disso rapidamente:

Button(
    text = "Button",
    icon: Icon? = myIcon,
    textStyle = TextStyle(...),
    spacingBetweenIconAndText = 4.dp,
    ...
)

ef5893f332864e28.png

Então, em vez de adicionar vários parâmetros para personalizar o componente de maneiras que não conseguimos prever, nós adicionamos slots. Os slots deixam um espaço vazio na IU, que o desenvolvedor pode preencher da maneira que quiser.

fccfb817afa8876e.png

No caso do botão, podemos deixar a parte interna dele vazia para ser preenchida por você, que pode optar por inserir uma coluna com um ícone e um texto:

Button {
    Row {
        MyImage()
        Spacer(4.dp)
        Text("Button")
    }
}

Para permitir isso, fornecemos uma API para o botão que usa um lambda do elemento filho (content: @Composable () -> Unit). Assim, você poderá definir um elemento de composição próprio que será emitido no botão.

@Composable
fun Button(
    modifier: Modifier = Modifier,
    onClick: (() -> Unit)? = null,
    ...
    content: @Composable () -> Unit
)

Observe que esse lambda, chamado de content, é o último parâmetro. Isso possibilita o uso de sintaxe de lambda final (link em inglês) para inserir o conteúdo no botão de modo estruturado.

O Compose usa muitos slots em componentes mais complexos, como a barra de apps da parte de cima da tela.

4365ce9b02ec2805.png

Aqui, é possível personalizar mais elementos além do título:

2decc9ec64c79a84.png

Exemplo de uso:

TopAppBar(
    title = {
        Text(text = "Page title", maxLines = 2)
    },
    navigationIcon = {
        Icon(myNavIcon)
    }
)

Ao criar seus próprios elementos de composição, você pode seguir o padrão da API de slots para eles sejam mais reutilizáveis.

Nas próximas seções, vamos ver os diferentes componentes do Material que estão disponíveis e como eles podem ser usados ao criar um app Android.

5. Componentes do Material Design

O Compose inclui componentes integrados do Material que podem ser usados ao criar seu app. O elemento de composição de mais alto nível é o Scaffold.

Scaffold

O Scaffold permite que você implemente uma IU com a estrutura básica de layout do Material Design. Ele fornece slots para os componentes de alto nível mais comuns do Material, como TopAppBar, BottomAppBar, FloatingActionButton e Drawer (links em inglês). Com o Scaffold, você consegue garantir que esses componentes sejam posicionados corretamente e funcionem juntos.

Partindo do modelo gerado pelo Android Studio, vamos modificar o exemplo de código para usar o Scaffold. Abra o arquivo MainActivity.kt e remova os elementos Greeting e GreetingPreview, já que eles não serão usados.

Crie um novo elemento de composição chamado LayoutsCodelab, que vamos modificar ao longo deste codelab:

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.codelab.layouts.ui.LayoutsCodelabTheme

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LayoutsCodelabTheme {
                LayoutsCodelab()
            }
        }
    }
}

@Composable
fun LayoutsCodelab() {
    Text(text = "Hi there!")
}

@Preview
@Composable
fun LayoutsCodelabPreview() {
    LayoutsCodelabTheme {
        LayoutsCodelab()
    }
}

Se a função de prévia do Compose que precisa da anotação @Preview for mostrada, você verá o LayoutsCodelab desta forma:

bd1c58d4497f523f.png

Vamos adicionar o elemento Scaffold ao nosso exemplo para ter uma estrutura típica do Material Design. Todos os parâmetros na Scaffold API são opcionais, exceto o conteúdo do corpo do tipo @Composable (InnerPadding) -> Unit: o lambda recebe um padding como parâmetro. Esse padding precisa ser aplicado ao elemento de composição raiz do conteúdo para restringir os itens na tela de modo adequado. Para começar de um jeito simples, vamos adicionar o Scaffold sem nenhum outro componente do Material:

@Composable
fun LayoutsCodelab() {
    Scaffold { innerPadding ->
        Text(text = "Hi there!", modifier = Modifier.padding(innerPadding))
    }
}

Esta é a prévia:

54b175d305766292.png

Para incluir uma Column com o principal conteúdo da tela, precisaríamos aplicar o modificador à Column:

@Composable
fun LayoutsCodelab() {
    Scaffold { innerPadding ->
        Column(modifier = Modifier.padding(innerPadding)) {
            Text(text = "Hi there!")
            Text(text = "Thanks for going through the Layouts codelab")
        }
    }
}

Esta é a prévia:

aceda77e27f25fe9.png

Para que o código seja mais reutilizável e possa ser testado, ele precisa ser estruturado em pequenos blocos. Então, vamos criar outra função de composição com o conteúdo da tela.

@Composable
fun LayoutsCodelab() {
    Scaffold { innerPadding ->
        BodyContent(Modifier.padding(innerPadding))
    }
}

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Column(modifier = modifier) {
        Text(text = "Hi there!")
        Text(text = "Thanks for going through the Layouts codelab")
    }
}

É comum ver uma barra na parte de cima de apps Android mostrando informações sobre a tela atual, a navegação e as ações. Vamos adicionar essa barra ao nosso exemplo.

TopAppBar

O Scaffold tem um slot para uma barra de apps na parte de cima da tela com o parâmetro topBar do tipo @Composable () -> Unit. Isso significa que podemos preencher esse slot com o elemento de composição que quisermos. Por exemplo, se quiséssemos incluir um texto no estilo h3, poderíamos usar o Text nos slots desta maneira:

@Composable
fun LayoutsCodelab() {
    Scaffold(
        topBar = {
            Text(
                text = "LayoutsCodelab",
                style = MaterialTheme.typography.h3
            )
        }
    ) { innerPadding ->
        BodyContent(Modifier.padding(innerPadding))
    }
}

Esta é a prévia:

6adf05bb92b48b76.png

Contudo, assim como ocorre na maioria dos componentes do Material, o Compose inclui um elemento TopAppBar que tem slots para um título, um ícone de navegação e ações. Além disso, ele apresenta configurações padrão que são ajustadas às especificações do Material, como a cor a ser usada em cada componente.

Seguindo o padrão da API de slots, o slot title de uma TopAppBar precisa conter um Text com o título da tela:

@Composable
fun LayoutsCodelab() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(text = "LayoutsCodelab")
                }
            )
        }
    ) { innerPadding ->
        BodyContent(Modifier.padding(innerPadding))
    }
}

Esta é a prévia:

c93d09851d6560c7.png

A barra de apps da parte de cima da tela geralmente inclui alguns itens de ação. Em nosso exemplo, vamos adicionar um botão de favorito, que pode ser pressionado quando você acreditar que aprendeu algo novo. O Compose também inclui alguns ícones predefinidos do Material (link em inglês) que você pode usar, como os ícones fechar, favorito e menu.

O slot para itens de ação na barra de apps é o parâmetro actions, que usa uma Row internamente para que várias ações sejam colocadas na horizontal. Se quisermos usar um dos ícones predefinidos, podemos implementar o elemento IconButton com um Icon dentro dele:

@Composable
fun LayoutsCodelab() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(text = "LayoutsCodelab")
                },
                actions = {
                    IconButton(onClick = { /* doSomething() */ }) {
                        Icon(Icons.Filled.Favorite, contentDescription = null)
                    }
                }
            )
        }
    ) { innerPadding ->
        BodyContent(Modifier.padding(innerPadding))
    }
}

Esta é a prévia:

b2d81ccec4667ef5.png

Normalmente, as ações modificam o estado do aplicativo de alguma forma. Para saber mais sobre os estados, estude as noções básicas de gerenciamento de estado no codelab de noções básicas do Compose.

Como posicionar modificadores

A prática recomendada ao criar um novo elemento de composição é incluir um parâmetro modifier que assume Modifier como padrão para que o elemento seja mais reutilizável. Nosso elemento BodyContent já considera um modificador como parâmetro. Se quiséssemos adicionar mais padding ao BodyContent, onde precisaríamos posicionar o modificador padding?

Temos duas possibilidades:

  1. Aplicar o modificador ao único filho direto dentro do elemento de composição para que ele chame o BodyContent e adicione mais padding:
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(8.dp)) {
        Text(text = "Hi there!")
        Text(text = "Thanks for going through the Layouts codelab")
    }
}
  1. Aplicar o modificador ao chamar o elemento de composição, que vai adicionar mais padding apenas quando necessário:
@Composable
fun LayoutsCodelab() {
    Scaffold(...) { innerPadding ->
        BodyContent(Modifier.padding(innerPadding).padding(8.dp))
    }
}

A escolha em relação ao local em que o modificador será usado depende do tipo de elemento de composição e do caso de uso. Se o modificador for intrínseco ao elemento de composição, ele vai precisar ser posicionado do lado de dentro. Caso contrário, posicione do lado de fora. Em nosso exemplo, a escolha correta seria a opção 2, já que o padding não será implementado sempre que BodyContent for chamado, e sim em casos específicos.

Para encadear os modificadores, você pode chamar cada função de modificador sucessiva dentro da anterior. Quando nenhum método de encadeamento estiver disponível, use .then(). No exemplo, começamos com modifier (com letra minúscula), o que significa que o encadeamento começa pela cadeia transmitida como parâmetro.

Mais ícones

Além dos ícones que apresentamos anteriormente, você pode usar a lista completa de ícones do Material adicionando uma nova dependência ao projeto. Caso você queira testar esses ícones, abra o arquivo app/build.gradle (ou build.gradle (Module: app)) e importe a dependência ui-material-icons-extended:

dependencies {
  ...
  implementation "androidx.compose.material:material-icons-extended:$compose_version"
}

Você pode mudar os ícones da TopAppBar da forma que quiser.

Aprofundamento

O Scaffold e TopAppBar são apenas alguns dos elementos de composição que podem ser usados para ter um aplicativo que segue os padrões do Material Design. É possível fazer o mesmo com componentes como BottomNavigation ou BottomDrawer. Como exercício, tente preencher os slots Scaffold com essas APIs da mesma maneira que fizemos até agora.

6. Como trabalhar com listas

Mostrar uma lista de itens é um padrão comum em aplicativos. O Jetpack Compose facilita a implementação desse padrão com os elementos de composição Column e Row. Além disso, ele oferece listas lentas que só compõe e apresentam os itens visíveis no momento.

Para praticar, vamos criar uma lista vertical com cem itens usando o elemento de composição Column:

@Composable
fun SimpleList() {
    Column {
        repeat(100) {
            Text("Item #$it")
        }
    }
}

Como o elemento Column não processa a rolagem por padrão, alguns itens não ficam visíveis, porque se encontram fora da tela atual. Adicione o modificador verticalScroll para habilitar a rolagem dentro de Column:

@Composable
fun SimpleList() {
    // We save the scrolling position with this state that can also
    // be used to programmatically scroll the list
    val scrollState = rememberScrollState()

    Column(Modifier.verticalScroll(scrollState)) {
        repeat(100) {
            Text("Item #$it")
        }
    }
}

Lista lenta

O elemento Column renderiza todos os itens da lista, incluindo os que não estão visíveis na tela. Esse padrão gera um problema de desempenho em casos em que a lista fica muito grande. Para evitar esse problema, use a função LazyColumn, que renderiza apenas os itens visíveis na tela e não precisa do modificador scroll, proporcionando ganhos de desempenho.

A LazyColumn usa uma DSL para descrever o conteúdo da lista. Você vai usar o elemento items, que processa um número como tamanho da lista. Ele também oferece suporte a matrizes e listas. Saiba mais na seção de documentação de listas.

@Composable
fun LazyList() {
    // We save the scrolling position with this state that can also
    // be used to programmatically scroll the list
    val scrollState = rememberLazyListState()

    LazyColumn(state = scrollState) {
        items(100) {
            Text("Item #$it")
        }
    }
}

1c747e54111e28c.gif

Mostrar imagens

Como vimos anteriormente com o PhotographCard, Image é um elemento de composição que pode ser usado para mostrar um Bitmap ou uma imagem vetorial. Caso seja necessário buscar a imagem remotamente, o processo vai envolver mais etapas, já que o app precisará fazer o download do recurso, decodificar para um bitmap e, por fim, renderizar em uma Image.

Para simplificar essas etapas, você vai usar a biblioteca Coil, que oferece elementos de composição que executam essas tarefas de maneira eficiente.

Adicione a dependência da Coil ao arquivo build.gradle do projeto:

// build.gradle
implementation 'io.coil-kt:coil-compose:1.4.0'

Como vamos buscar uma imagem remota, adicione a permissão INTERNET ao arquivo de manifesto:

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />

Agora, crie um elemento de composição de item em que a imagem será mostrada com um índice de item ao lado:

@Composable
fun ImageListItem(index: Int) {
    Row(verticalAlignment = Alignment.CenterVertically) {

        Image(
            painter = rememberImagePainter(
                data = "https://developer.android.com/images/brand/Android_Robot.png"
            ),
            contentDescription = "Android Logo",
            modifier = Modifier.size(50.dp)
        )
        Spacer(Modifier.width(10.dp))
        Text("Item #$index", style = MaterialTheme.typography.subtitle1)
    }
}

Em seguida, substitua o elemento de composição Text na sua lista por ImageListItem:

@Composable
fun ImageList() {
    // We save the scrolling position with this state
    val scrollState = rememberLazyListState()

    LazyColumn(state = scrollState) {
        items(100) {
            ImageListItem(it)
        }
    }
}

9c6a666c57a84211.gif

Rolagem de lista

Agora, vamos controlar manualmente a posição de rolagem da lista. Vamos adicionar dois botões que permitem rolar facilmente para o início e fim dela. Para evitar que a renderização da lista seja bloqueada durante a rolagem, as APIs de rolagem são funções suspensas. Portanto, elas vão precisar ser chamadas em uma corrotina. Para fazer isso, podemos criar um CoroutineScope (link em inglês) usando a função rememberCoroutineScope e implementar corrotinas com os manipuladores de eventos do botão. Esse CoroutineScope vai seguir o ciclo de vida do local de chamada. Para mais informações sobre ciclos de vida de elementos de composição, corrotinas e efeitos colaterais, leia este guia.

val listSize = 100
// We save the scrolling position with this state
val scrollState = rememberLazyListState()
// We save the coroutine scope where our animated scroll will be executed
val coroutineScope = rememberCoroutineScope()

Por fim, vamos adicionar os botões que controlam a rolagem:

Row {
    Button(onClick = {
        coroutineScope.launch {
            // 0 is the first item index
            scrollState.animateScrollToItem(0)
        }
    }) {
        Text("Scroll to the top")
    }

    Button(onClick = {
        coroutineScope.launch {
            // listSize - 1 is the last index of the list
            scrollState.animateScrollToItem(listSize - 1)
        }
    }) {
        Text("Scroll to the end")
    }
}

9bc52801a90401f3.gif

Código completo desta seção

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import coil.compose.rememberImagePainter
import kotlinx.coroutines.launch

@Composable
fun ImageListItem(index: Int) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(
            painter = rememberImagePainter(
                data = "https://developer.android.com/images/brand/Android_Robot.png"
            ),
            contentDescription = "Android Logo",
            modifier = Modifier.size(50.dp)
        )
        Spacer(Modifier.width(10.dp))
        Text("Item #$index", style = MaterialTheme.typography.subtitle1)
    }
}

@Composable
fun ScrollingList() {
    val listSize = 100
    // We save the scrolling position with this state
    val scrollState = rememberLazyListState()
    // We save the coroutine scope where our animated scroll will be executed
    val coroutineScope = rememberCoroutineScope()

    Column {
        Row {
            Button(onClick = {
                coroutineScope.launch {
                    // 0 is the first item index
                    scrollState.animateScrollToItem(0)
                }
            }) {
                Text("Scroll to the top")
            }

            Button(onClick = {
                coroutineScope.launch {
                    // listSize - 1 is the last index of the list
                    scrollState.animateScrollToItem(listSize - 1)
                }
            }) {
                Text("Scroll to the end")
            }
        }

        LazyColumn(state = scrollState) {
            items(listSize) {
                ImageListItem(it)
            }
        }
    }
}

7. Criar um layout personalizado

O Compose promove a reutilização dos elementos de composição na forma de pequenos blocos, que podem ser suficientes para criar alguns layouts personalizados com a combinação deles a elementos integrados, como Column, Row ou Box.

Contudo, talvez você queira criar um layout exclusivo para seu app e precise medir e dispor os filhos manualmente. Nesse caso, você pode usar o Layout de composição. Todos os layouts de nível mais elevado, como Column e Row, são criados com ele.

Antes de falarmos sobre as maneiras de criar layouts personalizados, precisamos saber mais sobre os princípios de layouts no Compose.

Princípios de layouts no Compose

Algumas funções de composição emitem uma parte da IU quando invocadas. Essa parte é adicionada a uma árvore de IU, que é renderizada na tela. Cada emissão (ou elemento) tem um pai e, possivelmente, muitos filhos, Além disso, ela tem uma localização dentro do pai: uma posição (x, y), e um tamanho: width e height.

Os elementos precisam se medir seguindo restrições que devem ser atendidas. As restrições determinam os valores mínimo e máximo de width e height dos elementos. Caso um elemento tenha elementos filhos, ele poderá medir cada um para ajudar a determinar o próprio tamanho. Depois que um elemento informa o próprio tamanho, ele tem a oportunidade de definir o posicionamento dos elementos filhos em relação a si próprio. Vamos explicar esse comportamento mais detalhadamente ao criar um layout personalizado.

A IU do Compose não permite a medição de várias transmissões. Isso significa que um elemento de layout não pode medir nenhum dos filhos mais de uma vez para testar diferentes configurações. A medição de uma única transmissão é boa para o desempenho porque permite que o Compose processe árvores de IU profundas com eficiência. Se um elemento de layout medisse o filho duas vezes e esse medisse o próprio filho duas vezes, e assim por diante, uma única tentativa de dispor uma IU inteira geraria muito trabalho, dificultando a manutenção do bom desempenho do app. No entanto, há momentos em que você realmente precisa de mais informações do que as que uma única medição do filho oferece. Existem maneiras de conseguir isso e elas serão explicadas mais adiante.

Como usar modificadores de layout

Use o modificador layout para controlar manualmente a forma de medir e posicionar um elemento. Geralmente, a estrutura comum de um modificador layout personalizado é a seguinte:

fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
  ...
})

Ao usar o modificador layout, você vai receber dois parâmetros lambda:

  • measurable: filho a ser medido e posicionado.
  • constraints: altura e largura mínima e máxima do filho.

Suponhamos que você queira mostrar um Text na tela e controlar a distância entre a parte de cima e a primeira linha de base dos textos. Para fazer isso, você precisaria posicionar manualmente o elemento de composição na tela usando o modificador layout. A imagem abaixo mostra o comportamento desejado, em que a distância entre a parte de cima e a primeira linha de base é 24.dp:

4ee1054702073598.png

Vamos começar criando um modificador firstBaselineToTop:

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        ...
    }
)

A primeira coisa que precisa ser feita é medir o elemento de composição. Como mencionamos na seção "Princípios de layouts no Compose", só é possível medir os filhos uma vez.

Para medir o elemento de composição, chame measurable.measure(constraints). Ao chamar measure(constraints), você pode transmitir as restrições já definidas do elemento, disponíveis no parâmetro lambda constraints, ou criar restrições próprias. O resultado de uma chamada measure() em Measurable é um Placeable que pode ser posicionado chamando placeRelative(x, y). Vamos fazer isso mais adiante.

Para esse caso de uso, não é necessário restringir mais a medição. Basta usar as restrições já definidas:

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        val placeable = measurable.measure(constraints)

        ...
    }
)

Depois de medir o elemento de composição, é necessário calcular o tamanho dele e o especificar chamando o método layout(width, height), que também aceita o uso de um lambda para posicionar o conteúdo.

Neste caso, a largura será a width do elemento de composição medido, e a altura será a height do elemento com a distância entre a parte de cima da tela e a base, menos a altura da primeira linha de base:

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        val placeable = measurable.measure(constraints)

        // Check the composable has a first baseline
        check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
        val firstBaseline = placeable[FirstBaseline]

        // Height of the composable with padding - first baseline
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
        val height = placeable.height + placeableY
        layout(placeable.width, height) {
            ...
        }
    }
)

Agora, você pode chamar placeable.placeRelative(x, y) para posicionar o elemento de composição na tela. Se você não chamar placeRelative, o elemento não vai ficar visível. O placeRelative ajusta automaticamente a posição do placeable de acordo com o layoutDirection atual.

Neste caso, a posição y do texto corresponde à parte de cima do padding menos a posição da primeira linha de base.

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        ...

        // Height of the composable with padding - first baseline
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
        val height = placeable.height + placeableY
        layout(placeable.width, height) {
            // Where the composable gets placed
            placeable.placeRelative(0, placeableY)
        }
    }
)

Para verificar se o posicionamento funciona da maneira esperada, use esse modificador em um Text, conforme mostrado na imagem acima:

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
  LayoutsCodelabTheme {
    Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
  }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview() {
  LayoutsCodelabTheme {
    Text("Hi there!", Modifier.padding(top = 32.dp))
  }
}

Esta é a prévia:

dccb4473e2ca09c6.png

Como usar o elemento de composição de layout

Em vez de controlar a medição e disposição na tela de um único elemento de composição, talvez você precise fazer isso para um grupo de elementos. Para isso, você pode usar o Layout e controlar manualmente a maneira como os filhos do layout serão medidos e posicionados. Geralmente, a estrutura comum de um elemento de composição que usa o Layout é a seguinte:

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    // custom layout attributes
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

Os parâmetros mínimos necessários para um CustomLayout são modifier e content. Na sequência, esses parâmetros são transmitidos para o Layout. No lambda final do Layout (do tipo MeasurePolicy), você vai receber alguns parâmetros lambda, da mesma forma que ocorre com o modificador layout.

Para mostrar o Layout em ação e entender a API, vamos começar a implementar uma Column muito básica usando o Layout. Posteriormente, vamos criar algo mais complexo para demonstrar a flexibilidade do elemento de composição Layout.

Como implementar uma coluna básica

A implementação personalizada da Column dispõe os itens verticalmente. Além disso, o layout ocupa todo o espaço possível no pai para simplificar.

Crie um novo elemento de composição chamado MyOwnColumn e adicione a estrutura comum de um Layout:

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

Como já mostrado, primeiro é necessário medir os filhos, que só podem ser medidos uma vez. De modo semelhante ao funcionamento do modificador de layout, você vai receber no parâmetro lambda measurables todo o content que for medido, chamando measurable.measure(constraints).

Para este caso de uso, não é necessário definir mais restrições para as visualizações filhas. Ao medir os filhos, é importante observar a width e a height máxima de cada linha, de modo que eles possam ser corretamente posicionados na tela:

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->

        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each child
            measurable.measure(constraints)
        }
    }
}

Agora que você tem uma lista dos filhos medidos em nossa lógica, é necessário calcular o tamanho da Column antes do posicionamento na tela. Como estabelecemos que os filhos vão ter o mesmo tamanho que o pai, o tamanho será definido pelas restrições transmitidas pelo pai. Especifique o tamanho da Column chamando o método layout(width, height), que também informa o lambda usado para posicionar os filhos:

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Measure children - code in the previous code snippet
        ...

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Place children
        }
    }
}

Por fim, chamamos placeable.placeRelative(x, y) para posicionar os filhos na tela. Se quiser posicionar os filhos verticalmente, será necessário observar a coordenada y em que colocamos os filhos. O código final de MyOwnColumn ficará assim:

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each child
            measurable.measure(constraints)
        }

        // Track the y co-ord we have placed children up to
        var yPosition = 0

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Place children in the parent layout
            placeables.forEach { placeable ->
                // Position item on the screen
                placeable.placeRelative(x = 0, y = yPosition)

                // Record the y co-ord placed up to
                yPosition += placeable.height
            }
        }
    }
}

MyOwnColumn em ação

Vamos usar o elemento BodyContent de composição para mostrar MyOwnColumn na tela. Substitua o conteúdo dentro de BodyContent pelo seguinte:

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    MyOwnColumn(modifier.padding(8.dp)) {
        Text("MyOwnColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

Esta é a prévia:

e69cdb015e4d8abe.png

8. Layout personalizado complexo

Já apresentamos as noções básicas do Layout. Agora, vamos criar um exemplo mais complexo para demonstrar a flexibilidade da API. Vamos criar a grade escalonada personalizada do app Owl de estudo do Material Design (link em inglês), que é mostrada no centro da imagem a seguir:

7a54fe8390fe39d2.png

A grade escalonada do Owl dispõe os itens verticalmente, preenchendo uma coluna por vez de acordo com o número n de linhas. Não é possível fazer o mesmo com uma Row de Columns, porque isso não geraria o escalonamento do layout. Ter uma Column de Rows pode ser possível se você preparar os dados para que sejam mostrados verticalmente.

O layout personalizado também oferece a possibilidade de restringir a altura de todos os itens na grade escalonada. Assim, para ter mais controle e aprender a criar um layout personalizado, vamos medir e posicionar os filhos manualmente.

Se quisermos fazer com que a grade seja reutilizável em orientações diferentes, podemos usar como parâmetro o número de linhas que queremos mostrar na tela. Já que essa informação precisa ser recebida ao invocar o layout, ela será transmitida como um parâmetro:

@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    rows: Int = 3,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

Como feito antes, primeiro é necessário medir os filhos. Lembre-se de que só é possível medir os filhos uma vez.

Para este caso de uso, não é necessário definir mais restrições para as visualizações filhas. Ao medir os filhos, é importante observar qual é a width e a height máxima de cada linha:

Layout(
    modifier = modifier,
    content = content
) { measurables, constraints ->

    // Keep track of the width of each row
    val rowWidths = IntArray(rows) { 0 }

    // Keep track of the max height of each row
    val rowHeights = IntArray(rows) { 0 }

    // Don't constrain child views further, measure them with given constraints
    // List of measured children
    val placeables = measurables.mapIndexed { index, measurable ->

        // Measure each child
        val placeable = measurable.measure(constraints)

        // Track the width and max height of each row
        val row = index % rows
        rowWidths[row] += placeable.width
        rowHeights[row] = Math.max(rowHeights[row], placeable.height)

        placeable
    }
    ...
}

Agora que já temos a lista de filhos medidos por essa lógica, precisamos calcular o tamanho da grade (width e height totais) antes de fazer o posicionamento na tela. Além disso, como já sabemos a altura máxima de cada linha, podemos calcular o local em que os elementos serão posicionados em cada uma delas na posição Y. As posições Y ficam salvas na variável rowY:

Layout(
    content = content,
    modifier = modifier
) { measurables, constraints ->
    ...

    // Grid's width is the widest row
    val width = rowWidths.maxOrNull()
        ?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ?: constraints.minWidth

    // Grid's height is the sum of the tallest element of each row
    // coerced to the height constraints
    val height = rowHeights.sumOf { it }
        .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))

    // Y of each row, based on the height accumulation of previous rows
    val rowY = IntArray(rows) { 0 }
    for (i in 1 until rows) {
        rowY[i] = rowY[i-1] + rowHeights[i-1]
    }

    ...
}

Por fim, chamamos placeable.placeRelative(x, y) para posicionar os filhos na tela. Neste caso de uso, também precisamos salvar a coordenada X de cada linha na variável rowX:

Layout(
    content = content,
    modifier = modifier
) { measurables, constraints ->
    ...

    // Set the size of the parent layout
    layout(width, height) {
        // x cord we have placed up to, per row
        val rowX = IntArray(rows) { 0 }

        placeables.forEachIndexed { index, placeable ->
            val row = index % rows
            placeable.placeRelative(
                x = rowX[row],
                y = rowY[row]
            )
            rowX[row] += placeable.width
        }
    }
}

Como usar o StaggeredGrid personalizado em um exemplo

Agora que já temos um layout de grade personalizado que sabe medir e posicionar os filhos, vamos fazer a implementação dele no app. Para simular os ícones do Owl na grade, podemos criar um elemento de composição semelhante de forma simples:

@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
    Card(
        modifier = modifier,
        border = BorderStroke(color = Color.Black, width = Dp.Hairline),
        shape = RoundedCornerShape(8.dp)
    ) {
        Row(
            modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(
                modifier = Modifier.size(16.dp, 16.dp)
                    .background(color = MaterialTheme.colors.secondary)
            )
            Spacer(Modifier.width(4.dp))
            Text(text = text)
        }
    }
}

@Preview
@Composable
fun ChipPreview() {
    LayoutsCodelabTheme {
        Chip(text = "Hi there")
    }
}

Esta é a prévia:

f1f8c6bb7f12cf1.png

Agora, vamos criar uma lista de temas que podemos incluir no BodyContent e os mostrar no StaggeredGrid:

val topics = listOf(
    "Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
    "Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
    "Religion", "Social sciences", "Technology", "TV", "Writing"
)

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    StaggeredGrid(modifier = modifier) {
        for (topic in topics) {
            Chip(modifier = Modifier.padding(8.dp), text = topic)
        }
    }
}

@Preview
@Composable
fun LayoutsCodelabPreview() {
    LayoutsCodelabTheme {
        BodyContent()
    }
}

Esta é a prévia:

e9861768e4e27dd4.png

Você pode mudar o número de linhas da grade e ela vai continuar funcionando normalmente:

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    StaggeredGrid(modifier = modifier, rows = 5) {
        for (topic in topics) {
            Chip(modifier = Modifier.padding(8.dp), text = topic)
        }
    }
}

Esta é a prévia:

555f88fd41e4dff4.png

Dependendo do número de linhas, os temas podem ficar fora da tela. Para corrigir isso, podemos fazer com que o BodyContent seja rolável, colocando o StaggeredGrid dentro de uma Row rolável e transmitindo o modificador para a linha, em vez do StaggeredGrid.

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Row(modifier = modifier.horizontalScroll(rememberScrollState())) {
        StaggeredGrid {
            for (topic in topics) {
                Chip(modifier = Modifier.padding(8.dp), text = topic)
            }
        }
    }
}

Ao usar o botão Interactive Preview bb4c8dfe4b8debaa.png ou executar o app no dispositivo, tocando no botão "Run" do Android Studio, você verá que agora é possível rolar o conteúdo horizontalmente.

9. Modificadores de layout em segundo plano

Agora que aprendemos as noções básicas dos modificadores, como criar elementos de composição personalizados e como medir e posicionar filhos manualmente, vamos entender melhor como é o funcionamento dos modificadores em segundo plano.

Como explicado, os modificadores permitem personalizar o comportamento de um elemento de composição. Você pode encadear vários modificadores para criar combinações. Existem vários tipos de modificadores, mas aqui vamos nos concentrar nos LayoutModifiers, porque eles mudam a forma como um componente de IU é medido e disposto.

Os elementos de composição são responsáveis pelo próprio conteúdo, que não pode ser inspecionado nem manipulado por um elemento pai, a menos que o autor do elemento exponha uma API explícita para fazer isso. De forma semelhante, os modificadores de um elemento de composição decoram o que eles modificam com o mesmo nível de opacidade, porque ficam encapsulados.

Como analisar um modificador

Modifier e LayoutModifier são interfaces públicas e, por isso, você pode criar modificadores próprios. Como usamos Modifier.padding anteriormente, vamos analisar a implementação dessa função para entender melhor os modificadores.

padding é uma função que tem como base uma classe que implementa a interface LayoutModifier e vai substituir o método measure. PaddingModifier é uma classe normal que implementa o equals() para que o modificador possa ser comparado entre as recomposições.

Como exemplo, veja o código-fonte que mostra de que forma a função padding modifica o tamanho e as restrições do elemento em que ela é aplicada:

// How to create a modifier
@Stable
fun Modifier.padding(all: Dp) =
    this.then(
        PaddingModifier(start = all, top = all, end = all, bottom = all, rtlAware = true)
    )

// Implementation detail
private class PaddingModifier(
    val start: Dp = 0.dp,
    val top: Dp = 0.dp,
    val end: Dp = 0.dp,
    val bottom: Dp = 0.dp,
    val rtlAware: Boolean,
) : LayoutModifier {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {

        val horizontal = start.roundToPx() + end.roundToPx()
        val vertical = top.roundToPx() + bottom.roundToPx()

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            if (rtlAware) {
                placeable.placeRelative(start.roundToPx(), top.roundToPx())
            } else {
                placeable.place(start.roundToPx(), top.roundToPx())
            }
        }
    }
}

A nova width do elemento será a width do filho mais os valores de início e fim do padding, convertidos em restrições de largura do elemento. A height será a height do filho mais os valores da parte de cima e de baixo do padding, convertidos em restrições de altura do elemento.

A ordem é importante

Como vimos na primeira seção, a ordem é importante ao encadear modificadores porque eles são aplicados ao elemento de composição que modificam do mais próximo para o mais distante. Isso significa que a medição e disposição dos modificadores à esquerda afetam o modificador à direita. O tamanho final de um elemento de composição depende de todos os modificadores que são transmitidos como parâmetros.

Primeiro, os modificadores atualizam as restrições da esquerda para a direita e, depois, eles retornam o tamanho da direita para a esquerda. Vejamos como isso funciona em um exemplo:

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Row(
        modifier = modifier
            .background(color = Color.LightGray)
            .size(200.dp)
            .padding(16.dp)
            .horizontalScroll(rememberScrollState())
    ) {
        StaggeredGrid {
            for (topic in topics) {
                Chip(modifier = Modifier.padding(8.dp), text = topic)
            }
        }
    }
}

Os modificadores aplicados dessa maneira geram a seguinte prévia:

cb209bb5edf634d6.png

Primeiro, mudamos o plano de fundo para ver como os modificadores afetam a IU. Depois disso, restringimos o tamanho para uma width e height de 200.dp. Por fim, aplicamos o padding para adicionar espaço entre o texto e os arredores.

Como as restrições são propagadas pela cadeia da esquerda para a direita, as restrições que precisam ser usadas para medir o conteúdo da Row são de (200-16-16)=168 dp para a width e height mínimas e máximas. Isso significa que o tamanho do StaggeredGrid será exatamente 168x168 dp. Portanto, o tamanho final da Row rolável, depois que a cadeia modifySize for executada da direita para a esquerda, será de 200x200 dp.

Se mudarmos a ordem dos modificadores para aplicar o padding primeiro e depois o tamanho, o resultado da IU será diferente:

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Row(
        modifier = modifier
            .background(color = Color.LightGray, shape = RectangleShape)
            .padding(16.dp)
            .size(200.dp)
            .horizontalScroll(rememberScrollState())
    ) {
        StaggeredGrid {
            for (topic in topics) {
                Chip(modifier = Modifier.padding(8.dp), text = topic)
            }
        }
    }
}

Esta é a prévia:

17da5805d6d8fc91.png

Neste caso, as restrições originais da Row rolável e do padding serão convertidas em restrições de size para medir os filhos. Assim, a restrição do StaggeredGrid será de 200 dp para a width e a height mínimas e máximas. O tamanho do StaggeredGrid é de 200x200 dp e, como ele é modificado da direita para a esquerda, ele será aumentado pelo modificador padding para (200+16+16)x(200+16+16)=232x232, que também será o tamanho final da Row.

Direção do layout

Você pode mudar a direção do layout de um elemento de composição usando o ambiente LayoutDirection.

Se você posicionar os elementos de composição de forma manual na tela, o layoutDirection será parte do LayoutScope do modificador layout ou do elemento Layout. Ao usar o layoutDirection, posicione os elementos de composição usando place porque, diferentemente do método placeRelative, ele não vai espelhar a posição de forma automática em contextos da direita para a esquerda.

Código completo desta seção

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.codelab.layouts.ui.LayoutsCodelabTheme
import kotlin.math.max

val topics = listOf(
    "Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
    "Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
    "Religion", "Social sciences", "Technology", "TV", "Writing"
)

@Composable
fun LayoutsCodelab() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(text = "LayoutsCodelab")
                },
                actions = {
                    IconButton(onClick = { /* doSomething() */ }) {
                        Icon(Icons.Filled.Favorite, contentDescription = null)
                    }
                }
            )
        }
    ) { innerPadding ->
        BodyContent(Modifier.padding(innerPadding))
    }
}

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Row(modifier = modifier
        .background(color = Color.LightGray)
        .padding(16.dp)
        .size(200.dp)
        .horizontalScroll(rememberScrollState()),
        content = {
            StaggeredGrid {
                for (topic in topics) {
                    Chip(modifier = Modifier.padding(8.dp), text = topic)
                }
            }
        })
}

@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    rows: Int = 3,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->

        // Keep track of the width of each row
        val rowWidths = IntArray(rows) { 0 }

        // Keep track of the max height of each row
        val rowHeights = IntArray(rows) { 0 }

        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.mapIndexed { index, measurable ->
            // Measure each child
            val placeable = measurable.measure(constraints)

            // Track the width and max height of each row
            val row = index % rows
            rowWidths[row] += placeable.width
            rowHeights[row] = Math.max(rowHeights[row], placeable.height)

            placeable
        }

        // Grid's width is the widest row
        val width = rowWidths.maxOrNull()
            ?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ?: constraints.minWidth

        // Grid's height is the sum of the tallest element of each row
        // coerced to the height constraints
        val height = rowHeights.sumOf { it }
            .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))

        // Y of each row, based on the height accumulation of previous rows
        val rowY = IntArray(rows) { 0 }
        for (i in 1 until rows) {
            rowY[i] = rowY[i - 1] + rowHeights[i - 1]
        }

        // Set the size of the parent layout
        layout(width, height) {
            // x co-ord we have placed up to, per row
            val rowX = IntArray(rows) { 0 }

            placeables.forEachIndexed { index, placeable ->
                val row = index % rows
                placeable.placeRelative(
                    x = rowX[row],
                    y = rowY[row]
                )
                rowX[row] += placeable.width
            }
        }
    }
}

@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
    Card(
        modifier = modifier,
        border = BorderStroke(color = Color.Black, width = Dp.Hairline),
        shape = RoundedCornerShape(8.dp)
    ) {
        Row(
            modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(
                modifier = Modifier
                    .size(16.dp, 16.dp)
                    .background(color = MaterialTheme.colors.secondary)
            )
            Spacer(Modifier.width(4.dp))
            Text(text = text)
        }
    }
}

@Preview
@Composable
fun ChipPreview() {
    LayoutsCodelabTheme {
        Chip(text = "Hi there")
    }
}

@Preview
@Composable
fun LayoutsCodelabPreview() {
    LayoutsCodelabTheme {
        LayoutsCodelab()
    }
}

10. Layout de restrição

A classe ConstraintLayout pode ajudar a posicionar os elementos de composição em relação a outros na tela. Essa é uma alternativa ao uso de várias Rows, Columns e Boxes. A ConstraintLayout é útil para implementar layouts maiores com requisitos de alinhamento mais complicados.

A dependência de layout de restrição do Compose pode ser encontrada no arquivo build.gradle do projeto:

// build.gradle
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01"

No Compose, a ConstraintLayout funciona com uma DSL:

  • As referências são criadas usando createRefs() ou createRef(), e cada elemento de composição na ConstraintLayout precisa ter uma referência associada.
  • As restrições são fornecidas usando o modificador constrainAs, que usa a referência como um parâmetro e permite especificar as restrições no lambda do corpo.
  • As restrições são especificadas usando linkTo ou outros métodos úteis.
  • parent é uma referência já existente que pode ser usada para especificar restrições ao próprio elemento ConstraintLayout.

Vamos começar com um exemplo simples.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {

        // Create references for the composables to constrain
        val (button, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            // Assign reference "button" to the Button composable
            // and constrain it to the top of the ConstraintLayout
            modifier = Modifier.constrainAs(button) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button")
        }

        // Assign reference "text" to the Text composable
        // and constrain it to the bottom of the Button composable
        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
        })
    }
}

@Preview
@Composable
fun ConstraintLayoutContentPreview() {
    LayoutsCodelabTheme {
        ConstraintLayoutContent()
    }
}

Esse código restringe a parte de cima do Button ao pai (com uma margem de 16.dp) e um Text na parte de baixo de Button(também com uma margem de 16.dp).

72fcb81ab2c0483c.png

Se quisermos centralizar o texto horizontalmente, podemos usar a função centerHorizontallyTo, que define o start e o end do Text como as bordas do parent:

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        ... // Same as before

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
            // Centers Text horizontally in the ConstraintLayout
            centerHorizontallyTo(parent)
        })
    }
}

Esta é a prévia:

729a1b4c03f1f187.png

O tamanho da classe ConstraintLayout será o menor possível para agrupar o conteúdo. É por isso que parece que o Text está centralizado em relação ao Button, e não do pai. Caso você queira definir outros comportamentos de dimensionamento, aplique modificadores de tamanho (como fillMaxSize e size) ao elemento ConstraintLayout, da mesma forma que seria feito em qualquer outro layout no Compose.

Auxiliares

A DSL também tem suporte para a criação de diretrizes, barreiras e cadeias. Exemplo:

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // Creates references for the three composables
        // in the ConstraintLayout's body
        val (button1, button2, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            modifier = Modifier.constrainAs(button1) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button 1")
        }

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button1.bottom, margin = 16.dp)
            centerAround(button1.end)
        })

        val barrier = createEndBarrier(button1, text)
        Button(
            onClick = { /* Do something */ },
            modifier = Modifier.constrainAs(button2) {
                top.linkTo(parent.top, margin = 16.dp)
                start.linkTo(barrier)
            }
        ) {
            Text("Button 2")
        }
    }
}

Esta é a prévia:

a4117576ef1768a2.png

Algumas considerações:

  • As barreiras, e todos os outros auxiliares, podem ser criadas no corpo da ConstraintLayout, mas não dentro de constrainAs.
  • O linkTo pode ser usado para restringir com diretrizes e barreiras do mesmo modo que é feito nas bordas de layouts.

Como personalizar dimensões

Por padrão, os filhos da classe ConstraintLayout podem definir o tamanho que precisam ter para agrupar o conteúdo. Isso significa, por exemplo, que um texto poderá ultrapassar os limites da tela quando for muito longo:

@Composable
fun LargeConstraintLayout() {
    ConstraintLayout {
        val text = createRef()

        val guideline = createGuidelineFromStart(fraction = 0.5f)
        Text(
            "This is a very very very very very very very long text",
            Modifier.constrainAs(text) {
                linkTo(start = guideline, end = parent.end)
            }
        )
    }
}

@Preview
@Composable
fun LargeConstraintLayoutPreview() {
    LayoutsCodelabTheme {
        LargeConstraintLayout()
    }
}

616c19b971811cfa.png

O ideal é que o texto inclua quebras de linha para ser mostrado no espaço disponível. Para fazer isso, é necessário mudar o comportamento da width do texto:

@Composable
fun LargeConstraintLayout() {
    ConstraintLayout {
        val text = createRef()

        val guideline = createGuidelineFromStart(0.5f)
        Text(
            "This is a very very very very very very very long text",
            Modifier.constrainAs(text) {
                linkTo(guideline, parent.end)
                width = Dimension.preferredWrapContent
            }
        )
    }
}

Esta é a prévia:

fc41cacd547bbea.png

Os comportamentos de Dimension disponíveis são estes:

  • preferredWrapContent: o layout mostra o conteúdo agrupado, sujeito às restrições de dimensão.
  • wrapContent: o layout mostra o conteúdo agrupado mesmo que ele exceda as restrições.
  • fillToConstraints: o layout será expandido para preencher o espaço definido pelas restrições nessa dimensão.
  • preferredValue: o layout é um valor de dp fixo, sujeito às restrições de dimensão.
  • value: o layout é um valor de dp fixo, independentemente das restrições de dimensão.

Além disso, é possível converter algumas Dimensions:

width = Dimension.preferredWrapContent.atLeast(100.dp)

API dissociada

Até aqui, as restrições nos exemplos foram especificadas in-line, com um modificador no elemento de composição ao qual elas são aplicadas. Contudo, há casos em que deixar as restrições dissociadas dos layouts a que elas são aplicadas é útil. Um exemplo comum é mudar as restrições com facilidade de acordo com a configuração da tela ou inserir animações entre dois grupos de restrições.

Para esses casos de uso, você pode usar a classe ConstraintLayout de uma maneira diferente:

  1. Transmita um ConstraintSet como um parâmetro para a ConstraintLayout.
  2. Atribua referências criadas no ConstraintSet aos elementos de composição usando o modificador layoutId.

O formato de API aplicado ao primeiro exemplo de ConstraintLayout apresentado acima e otimizado para a largura da tela ficaria assim:

@Composable
fun DecoupledConstraintLayout() {
    BoxWithConstraints {
        val constraints = if (maxWidth < maxHeight) {
            decoupledConstraints(margin = 16.dp) // Portrait constraints
        } else {
            decoupledConstraints(margin = 32.dp) // Landscape constraints
        }

        ConstraintLayout(constraints) {
            Button(
                onClick = { /* Do something */ },
                modifier = Modifier.layoutId("button")
            ) {
                Text("Button")
            }

            Text("Text", Modifier.layoutId("text"))
        }
    }
}

private fun decoupledConstraints(margin: Dp): ConstraintSet {
    return ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        constrain(button) {
            top.linkTo(parent.top, margin= margin)
        }
        constrain(text) {
            top.linkTo(button.bottom, margin)
        }
    }
}

11. Intrínsecos

Uma das regras do Compose é que os filhos precisam ser medidos somente uma vez. Medir duas vezes gera uma exceção no ambiente de execução. No entanto, há momentos em que você precisa de algumas informações sobre os filhos antes de medir.

Com os intrínsecos, é possível consultar os elementos filhos antes que eles sejam medidos.

Para um elemento de composição, você pode solicitar intrinsicWidth ou intrinsicHeight:

  • (min|max)IntrinsicWidth: considerando essa altura, quais são as larguras mínima/máxima para que o conteúdo seja pintado corretamente?
  • (min|max)IntrinsicHeight: considerando essa largura, quais são as alturas mínima/máxima para que o conteúdo seja pintado corretamente?

Por exemplo, se você solicitar a minIntrinsicHeight de um Text com width infinita, ela vai retornar a height do Text como se o texto tivesse sido desenhado em uma única linha.

Intrínsecos em ação

Imagine que queremos criar um elemento de composição que mostra dois textos na tela, separados por um divisor como este:

835f0b8c9f07cd9.png

Como podemos fazer isso? Podemos ter uma Row com dois Texts que se expandem o máximo possível e um Divider no meio. O divisor precisa ter a mesma altura que o Text mais alto e ser fino (width = 1.dp).

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )

        Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

@Preview
@Composable
fun TwoTextsPreview() {
    LayoutsCodelabTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

Na prévia, veremos que o divisor é expandido para a tela inteira, e esse não é o resultado esperado:

d61f179394ded825.png

Isso acontece porque a Row mede cada filho individualmente, e a altura do Text não pode ser usada para limitar o Divider. O objetivo é que o Divider preencha o espaço disponível com uma altura definida. Para isso, podemos usar o modificador height(IntrinsicSize.Min).

O height(IntrinsicSize.Min) dimensiona os filhos para que a altura deles seja igual à altura intrínseca mínima. Por ser recorrente, ele vai consultar a minIntrinsicHeight da Row e das filhas dela.

Aplicando isso ao código, ele terá o funcionamento esperado:

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )

        Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}

@Preview
@Composable
fun TwoTextsPreview() {
    LayoutsCodelabTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

Esta é a prévia:

835f0b8c9f07cd9.png

A minIntrinsicHeight da linha será a minIntrinsicHeight máxima das filhas dela. A minIntrinsicHeight do divisor é zero, porque ele não ocupa espaço se nenhuma restrição for definida. A minIntrinsicHeight do texto será igual à do texto que recebeu uma width específica. Portanto, a restrição da height da linha será a minIntrinsicHeight máxima dos Texts. O Divider vai expandir a height dele para a restrição da height especificada pela linha.

Faça você mesmo

Sempre que criar um layout personalizado, você pode modificar a forma como os intrínsecos são calculados com o (min|max)Intrinsic(Width|Height) da interface MeasurePolicy. Contudo, os valores padrão vão funcionar na maioria dos casos.

Além disso, você pode modificar os intrínsecos usando modificadores para substituir os métodos Density.(min|max)Intrinsic(Width|Height)Of da interface Modifier, que também tem um bom padrão.

12. Parabéns

Parabéns, você concluiu este codelab.

Solução do codelab

O código da solução deste codelab está disponível no GitHub:

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

Se preferir, faça o download do repositório como um arquivo ZIP:

Qual é a próxima etapa?

Confira os outros codelabs no programa de aprendizagem do Compose:

Leia mais

Apps de exemplo (link em inglês)

  • Owl: para criar layouts personalizados
  • Rally: para exibição de gráficos e tabelas
  • Jetsnack: para layouts personalizados