Layouts de fluxo no Compose

FlowRow e FlowColumn são elementos combináveis semelhantes a Row e Column, mas diferem porque os itens fluem para a próxima linha quando o contêiner fica sem espaço. Isso cria várias linhas ou colunas. O número de itens em uma linha também pode ser controlado definindo maxItemsInEachRow ou maxItemsInEachColumn. Muitas vezes, é possível usar FlowRow e FlowColumn para criar layouts responsivos. O conteúdo não será cortado se os itens forem grandes demais para uma dimensão, e o uso de uma combinação de maxItemsInEach* com Modifier.weight(weight) pode ajudar a criar layouts que preencham/expandam a largura de uma linha ou coluna quando necessário.

O exemplo típico é para uma interface de chip ou filtragem:

Cinco ícones em um FlowRow, mostrando o transbordamento para a próxima linha quando não há mais espaço disponível.
Figura 1. Exemplo de FlowRow.

Uso básico

Para usar FlowRow ou FlowColumn, crie esses elementos combináveis e coloque os itens dentro dele, que precisam seguir o fluxo padrão:

@Composable
private fun FlowRowSimpleUsageExample() {
    FlowRow(modifier = Modifier.padding(8.dp)) {
        ChipItem("Price: High to Low")
        ChipItem("Avg rating: 4+")
        ChipItem("Free breakfast")
        ChipItem("Free cancellation")
        ChipItem("£50 pn")
    }
}

Esse snippet resulta na interface mostrada acima, com os itens fluindo automaticamente para a próxima linha quando não há mais espaço na primeira.

Recursos do layout de fluxo

Os layouts de fluxo têm os recursos e propriedades abaixo que podem ser usados para criar diferentes layouts no seu app.

Organização do eixo principal: organização horizontal ou vertical

O eixo principal é aquele em que os itens são dispostos (por exemplo, em FlowRow, os itens são organizados horizontalmente). O parâmetro horizontalArrangement em FlowRow controla a forma como o espaço livre é distribuído entre os itens.

A tabela abaixo mostra exemplos de como definir horizontalArrangement em itens para FlowRow:

Organização horizontal definida em FlowRow

Resultado

Arrangement.Start (Default)

Itens organizados com início

Arrangement.SpaceBetween

Itens organizados com espaço no meio

Arrangement.Center

Itens organizados no centro

Arrangement.End

Itens organizados no final

Arrangement.SpaceAround

Itens organizados com espaço ao redor deles

Arrangement.spacedBy(8.dp)

Itens espaçados por um determinado dp

Para FlowColumn, opções semelhantes estão disponíveis com verticalArrangement, com o padrão de Arrangement.Top.

Disposição entre eixos

O eixo transversal é o eixo na direção oposta do eixo principal. Por exemplo, em FlowRow, esse é o eixo vertical. Se quiser mudar como o conteúdo geral dentro do contêiner é organizado no eixo cruzado, use verticalArrangement para FlowRow e horizontalArrangement para FlowColumn.

Para FlowRow, a tabela abaixo mostra exemplos de como definir verticalArrangement diferentes nos itens:

Organização vertical definida em FlowRow

Resultado

Arrangement.Top (Default)

Disposição da parte superior do contêiner

Arrangement.Bottom

Disposição inferior do contêiner

Arrangement.Center

Disposição do centro do contêiner

Para FlowColumn, opções semelhantes estão disponíveis com horizontalArrangement. A organização padrão de eixo cruzado é Arrangement.Start.

Alinhamento de itens individuais

É possível posicionar itens individuais na linha com diferentes alinhamentos. Isso é diferente de verticalArrangement e horizontalArrangement, já que alinha os itens na linha atual. É possível aplicar isso com Modifier.align().

Por exemplo, quando os itens em uma FlowRow têm alturas diferentes, a linha usa a altura do maior item e aplica Modifier.align(alignmentOption) a eles:

Alinhamento vertical definido em FlowRow

Resultado

Alignment.Top (Default)

Itens alinhados à parte superior

Alignment.Bottom

Itens alinhados à parte de baixo

Alignment.CenterVertically

Itens alinhados ao centro

Para FlowColumn, opções semelhantes estão disponíveis. O alinhamento padrão é Alignment.Start.

Máximo de itens na linha ou coluna

Os parâmetros maxItemsInEachRow ou maxItemsInEachColumn definem o número máximo de itens no eixo principal a serem permitidos em uma linha antes de mudar para a próxima. O padrão é Int.MAX_INT, que permite o máximo de itens possível, desde que o tamanho permita que eles se encaixem na linha.

Por exemplo, definir uma maxItemsInEachRow força o layout inicial a ter apenas três itens:

Nenhum máximo definido

maxItemsInEachRow = 3

Nenhum máximo definido na linha do fluxo Máximo de itens definidos na linha do fluxo

Itens de fluxo com carregamento lento

ContextualFlowRow e ContextualFlowColumn são uma versão especializada de FlowRow e FlowColumn que permitem o carregamento lento do conteúdo da linha ou coluna do fluxo. Eles também fornecem informações sobre a posição dos itens (índice, número da linha e tamanho disponível), por exemplo, se o item está na primeira linha. Isso é útil para grandes conjuntos de dados e se você precisar de informações contextuais sobre um item.

O parâmetro maxLines limita o número de linhas exibidas, e o parâmetro overflow especifica o que precisa ser exibido quando um estouro de itens é atingido, permitindo que você especifique um expandIndicator ou collapseIndicator personalizado.

Por exemplo, para mostrar um botão "+ (número de itens restantes)" ou "Mostrar menos":

val totalCount = 40
var maxLines by remember {
    mutableStateOf(2)
}

val moreOrCollapseIndicator = @Composable { scope: ContextualFlowRowOverflowScope ->
    val remainingItems = totalCount - scope.shownItemCount
    ChipItem(if (remainingItems == 0) "Less" else "+$remainingItems", onClick = {
        if (remainingItems == 0) {
            maxLines = 2
        } else {
            maxLines += 5
        }
    })
}
ContextualFlowRow(
    modifier = Modifier
        .safeDrawingPadding()
        .fillMaxWidth(1f)
        .padding(16.dp)
        .wrapContentHeight(align = Alignment.Top)
        .verticalScroll(rememberScrollState()),
    verticalArrangement = Arrangement.spacedBy(4.dp),
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    maxLines = maxLines,
    overflow = ContextualFlowRowOverflow.expandOrCollapseIndicator(
        minRowsToShowCollapse = 4,
        expandIndicator = moreOrCollapseIndicator,
        collapseIndicator = moreOrCollapseIndicator
    ),
    itemCount = totalCount
) { index ->
    ChipItem("Item $index")
}

Exemplo de linhas de fluxo contextual.
Figura 2. Exemplo de ContextualFlowRow.

Pesos do item

O peso aumenta um item com base no fator dele e no espaço disponível na linha em que ele foi colocado. É importante ressaltar que há uma diferença entre FlowRow e Row com a forma como os pesos são usados para calcular a largura de um item. Para Rows, o peso é baseado em todos os itens em Row. Com FlowRow, o peso é baseado nos itens na linha em que um item é colocado, não em todos os itens no contêiner FlowRow.

Por exemplo, se você tiver quatro itens que se enquadram em uma linha, cada um com pesos diferentes de 1f, 2f, 1f e 3f, o peso total será 7f. O espaço restante em uma linha ou coluna será dividido por 7f. Em seguida, a largura de cada item será calculada usando: weight * (remainingSpace / totalWeight).

Você pode usar uma combinação de Modifier.weight e itens máximos com FlowRow ou FlowColumn para criar um layout semelhante a uma grade. Essa abordagem é útil para criar layouts responsivos que se ajustam ao tamanho do dispositivo.

Há alguns exemplos diferentes do que é possível conseguir usando pesos. Um exemplo é uma grade em que os itens têm o mesmo tamanho, conforme mostrado abaixo:

Grade criada com a linha do fluxo
Figura 3. Como usar FlowRow para criar uma grade.

Para criar uma grade de tamanhos iguais de itens, faça o seguinte:

val rows = 3
val columns = 3
FlowRow(
    modifier = Modifier.padding(4.dp),
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    maxItemsInEachRow = rows
) {
    val itemModifier = Modifier
        .padding(4.dp)
        .height(80.dp)
        .weight(1f)
        .clip(RoundedCornerShape(8.dp))
        .background(MaterialColors.Blue200)
    repeat(rows * columns) {
        Spacer(modifier = itemModifier)
    }
}

É importante ressaltar que, se você adicionar outro item e repeti-lo 10 vezes em vez de nove, o último item ocupa toda a última coluna, já que o peso total da linha inteira é 1f:

Último item em tamanho original na grade
Figura 4. Usar FlowRow para criar uma grade com o último item ocupando toda a largura.

É possível combinar ponderações com outros Modifiers, como Modifier.width(exactDpAmount), Modifier.aspectRatio(aspectRatio) ou Modifier.fillMaxWidth(fraction). Todos esses modificadores funcionam em conjunto para permitir o dimensionamento responsivo de itens em uma FlowRow (ou FlowColumn).

Você também pode criar uma grade alternada de tamanhos de item diferentes, em que dois itens ocupam metade da largura cada, e um item ocupa toda a largura da próxima coluna:

Grade alternada com linha de fluxo
Figura 5. FlowRow com tamanhos de linhas alternados

Você pode fazer isso com o seguinte código:

FlowRow(
    modifier = Modifier.padding(4.dp),
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    maxItemsInEachRow = 2
) {
    val itemModifier = Modifier
        .padding(4.dp)
        .height(80.dp)
        .clip(RoundedCornerShape(8.dp))
        .background(Color.Blue)
    repeat(6) { item ->
        // if the item is the third item, don't use weight modifier, but rather fillMaxWidth
        if ((item + 1) % 3 == 0) {
            Spacer(modifier = itemModifier.fillMaxWidth())
        } else {
            Spacer(modifier = itemModifier.weight(0.5f))
        }
    }
}

Tamanho fracionário

Usando Modifier.fillMaxWidth(fraction), é possível especificar o tamanho do contêiner que um item precisa ocupar. Isso é diferente de como Modifier.fillMaxWidth(fraction) funciona quando aplicado a Row ou Column, porque os itens Row/Column ocupam uma porcentagem da largura restante, em vez de todo o contêiner.

Por exemplo, o código a seguir produz resultados diferentes ao usar FlowRow e Row:

FlowRow(
    modifier = Modifier.padding(4.dp),
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    maxItemsInEachRow = 3
) {
    val itemModifier = Modifier
        .clip(RoundedCornerShape(8.dp))
    Box(modifier = itemModifier.height(200.dp).width(60.dp).background(Color.Red))
    Box(modifier = itemModifier.height(200.dp).fillMaxWidth(0.7f).background(Color.Blue))
    Box(modifier = itemModifier.height(200.dp).weight(1f).background(Color.Magenta))
}

FlowRow: item do meio com 0,7 fração da largura total do contêiner.

Largura fracionária com linha de fluxo

Row: item do meio ocupando 0,7% da largura restante do Row.

Largura fracionária com linha

fillMaxColumnWidth() e fillMaxRowHeight()

Aplicar Modifier.fillMaxColumnWidth() ou Modifier.fillMaxRowHeight() a um item dentro de FlowColumn ou FlowRow garante que os itens na mesma coluna ou linha ocupem a mesma largura ou altura que o maior item na coluna/linha.

Por exemplo, este exemplo usa FlowColumn para mostrar a lista de sobremesas do Android. Você pode conferir a diferença nas larguras de cada item quando Modifier.fillMaxColumnWidth() é aplicado aos itens em relação a quando não são e os itens são unidos.

FlowColumn(
    Modifier
        .padding(20.dp)
        .fillMaxHeight()
        .fillMaxWidth(),
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp),
    maxItemsInEachColumn = 5,
) {
    repeat(listDesserts.size) {
        Box(
            Modifier
                .fillMaxColumnWidth()
                .border(1.dp, Color.DarkGray, RoundedCornerShape(8.dp))
                .padding(8.dp)
        ) {

            Text(
                text = listDesserts[it],
                fontSize = 18.sp,
                modifier = Modifier.padding(3.dp)
            )
        }
    }
}

Modifier.fillMaxColumnWidth() aplicado a cada item

fillMaxColumnWidth

Nenhuma alteração de largura definida (agrupamento de itens)

A largura máxima da coluna de preenchimento não foi definida