Layouts personalizados

No Compose, os elementos de IU são representados pelas funções que podem ser compostas, que emitem uma parte da IU quando invocadas. Essa parte é, então, adicionadas a uma árvore da IU e renderizada na tela. Cada elemento da IU tem um pai e, possivelmente, muitos filhos. Cada elemento também está localizado no pai, especificado como uma posição (x, y) e um tamanho (width e height).

Os pais definem as restrições dos elementos filhos. O tamanho do elemento é definido dentro dessas restrições. As restrições definem 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 tamanho dele. Depois que um elemento determina e informa o próprio tamanho, ele tem a oportunidade de definir o posicionamento dos elementos filhos em relação a ele mesmo, conforme descrito mais detalhadamente em Como criar layouts personalizados.

A disposição de cada nó na árvore da IU é um processo de três etapas. Cada nó precisa:

  1. medir os filhos;
  2. decidir o próprio tamanho;
  3. posicionar os filhos.

Três etapas do layout de nós: medir filhos, decidir o tamanho, posicionar filhos

O uso de escopos define quando você pode medir e posicionar elementos filhos. A medição de um layout só pode ser feita durante as transmissões de medição e de layout. Um filho só pode ser posicionado durante as transmissões de layout e somente depois de ser medido. Devido aos escopos do Compose, como MeasureScope e PlacementScope, isso é aplicado no momento da compilação.

Usar o modificador de layout

É possível usar o modificador layout para mudar a forma como um elemento é medido e disposto. Layout é um lambda. Os parâmetros dele incluem o elemento que você pode medir, transmitido como measurable, e as restrições desse elemento, transmitidas como constraints. Um modificador de layout personalizado pode ser assim:

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

Veja a exibição de um Text na tela e controle a distância da parte superior até a linha de base da primeira linha do texto. Isso é exatamente o que o modificador paddingFromBaseline faz, implementado aqui como um exemplo. Para fazer isso, use o modificador layout para colocar manualmente a função que pode ser composta na tela. Este é o comportamento esperado em que o padding superior do Text é definido como 24.dp:

Mostra a diferença entre o preenchimento normal da IU, que define o espaço entre os elementos, e o preenchimento de texto que define o espaço de uma linha de base até a próxima

Veja o código que produz esse espaçamento:

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = layout { measurable, constraints ->
    // Measure the composable
    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) {
        // Where the composable gets placed
        placeable.placeRelative(0, placeableY)
    }
}

Confira o que está acontecendo nesse código:

  1. No parâmetro lamdba measurable, o Text representado pelo parâmetro mensurável é medido ao chamar measurable.measure(constraints).
  2. Para especificar o tamanho do elemento combinável, chame o método layout(width, height), que também fornece um lambda usado para posicionar os elementos agrupados. Nesse caso, ele é a altura entre a última linha de base do texto e o padding superior adicionado.
  3. Posicione os elementos agrupados na tela chamando placeable.place(x, y). Se os elementos agrupados não forem posicionados, eles não ficarão visíveis. A posição y corresponde ao padding superior, que é a posição da primeira linha de base do texto.

Para verificar se isso funciona como esperado, use este modificador em um Text:

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

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

Várias visualizações de elementos do texto. Uma mostra o preenchimento comum entre os elementos, a outra mostra o preenchimento de uma linha de base até a próxima

Criar layouts personalizados

O modificador layout só muda o elemento que é autor da chamada. Para medir e definir o layout de vários elementos, use o elemento combinável Layout. Com ele, é possível medir e dispor os filhos manualmente. Todos os layouts de nível superior, como Column e Row, são criados com o elemento combinável Layout.

Vamos criar uma versão muito simples da Column. A maioria dos layouts personalizados segue este padrão:

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

Assim como o modificador layout, measurables é a lista de filhos que precisam ser medidos e constraints são as restrições do pai. Seguindo a mesma lógica de antes, MyBasicColumn pode ser implementada da seguinte forma:

@Composable
fun MyBasicColumn(
    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 children
            measurable.measure(constraints)
        }

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Track the y co-ord we have placed children up to
            var yPosition = 0

            // 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
            }
        }
    }
}

Os combináveis filhos são limitados pelas restrições de Layout (sem as de minHeight) e são posicionados de acordo com a yPosition do elemento combinável anterior.

Veja como esse elemento personalizado que pode ser composto seria usado:

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

Vários elementos de texto empilhados um em cima do outro em uma coluna.

Direção do layout

Para mudar a direção do layout de uma função que pode ser composta, mude o local da composição LocalLayoutDirection.

Quando você posiciona os combináveis manualmente na tela, a LayoutDirection faz parte do LayoutScope do modificador layout ou do combinável Layout.

Ao usar layoutDirection, posicione os elementos que podem ser compostos usando place. Ao contrário do método placeRelative place não muda de acordo com a direção do layout (esquerda para a direita ou direita para a esquerda).

Layouts personalizados em ação

Saiba mais sobre layouts e modificadores em Layouts básicos no Compose e veja layouts personalizados em ação nos exemplos do Compose que criam layouts personalizados (link em inglês).

Saiba mais

Para saber mais sobre layouts personalizados no Compose, consulte os recursos abaixo.

Vídeos