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, especificado como width e um 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 medição de passagem única é boa para o desempenho, permitindo que o Compose processe árvores de IU profundas com eficiência. Se um elemento de layout medisse o filho duas vezes e esse filho medisse um filho dele duas vezes, e assim por diante, uma única tentativa de dispor uma IU inteira tomaria muito trabalho, dificultando a manutenção do bom desempenho do app. No entanto, há momentos em que você realmente precisa de mais informações sobre uma única medição do filho. Existem abordagens para lidar eficientemente com uma situação como essa, que são discutidas na seção Medições intrínsecas.

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 durante a compilação.

Como usar modificadores 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(...) =
    this.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. Esse é 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)
    }
}

Veja 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 da função que pode ser composta, 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

Como 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 comando 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 Layout que pode ser composto.

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

As funções filhas são limitadas pelas restrições de Layout (sem as restrições de minHeight) e são posicionadas de acordo com a yPosition da função que pode ser composta 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.

Se você posicionar os elementos que podem ser compostos manualmente na tela, a LayoutDirection será parte do LayoutScope do modificador layout ou do elemento Layout que pode ser composto.

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 personalizados no codelab de Layouts no Jetpack Compose e veja as APIs em ação nos exemplos do Compose para criar layouts personalizados (link em inglês).