Diseños personalizados

En Compose, los elementos de la IU se representan con funciones que admiten composición y que emiten una porción de la IU cuando se invoca, que luego se agregan a un árbol de IU que se procesa en la pantalla. Cada elemento de la IU tiene un elemento superior y, posiblemente, varios secundarios. Cada elemento también está ubicado dentro de su elemento superior, especificado como una posición (x, y) y un tamaño, especificado como width y height.

Los elementos superiores definen las restricciones de sus elementos secundarios. Se solicita a un elemento que defina su tamaño dentro de esas restricciones. Las restricciones limitan los valores mínimos y máximos de width y height de un elemento. Si un elemento tiene elementos secundarios, puede medir cada uno de ellos para ayudar a determinar su tamaño. Una vez que un elemento determina e informa su propio tamaño, tiene la oportunidad de definir cómo colocar sus elementos secundarios en relación con ellos, como se describe en detalle en Cómo crear diseños personalizados.

La medición de un solo paso es ideal en términos de rendimiento y permite que Compose procese de manera eficiente los árboles detallados de la IU. Supongamos que un elemento midió dos veces a su elemento secundario, y el elemento secundario, a su vez, midió dos veces a su elemento secundario, y así sucesivamente. Un solo intento para implementar toda la IU requeriría muchísimo trabajo, lo que dificultaría lograr que tu app funcione bien. Sin embargo, hay momentos en los que realmente necesitas información adicional, más allá de lo que te pueda indicar una sola medición del elemento secundario. Existen enfoques que pueden resolver de manera efectiva una situación como esta, que se analizan en Medidas intrínsecas.

El uso de alcances define cuándo puedes medir y ubicar tus elementos secundarios. Solo podrás medir un diseño mientras se realicen los pases de medición y diseño. Podrás ubicar un elemento secundario únicamente durante los pases de diseño y solo después de que se haya medido de antemano. Debido a los alcances de Compose, como MeasureScope y PlacementScope, esto se aplica de manera forzosa en el tiempo de compilación.

Cómo usar el modificador de diseño

Puedes usar el modificador layout para modificar la forma en que se mide y se organiza un elemento. Layout es una expresión lambda; sus parámetros incluyen el elemento componible que puedes medir, que se pasó como measurable, y las restricciones correspondientes a ese elemento, que se pasaron como constraints. Un modificador de diseño personalizado puede verse de la siguiente manera:

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

Mostremos un Text en la pantalla y controlemos la distancia desde la parte superior hasta la línea de base de la primera línea de texto. Esto es exactamente lo que hace el modificador paddingFromBaseline. Lo estamos implementando aquí como un ejemplo. Para ello, usa el modificador layout, que permite colocar el elemento componible de forma manual en la pantalla. Este es el comportamiento deseado en el que el padding superior de Text está configurado en 24.dp:

Muestra la diferencia entre el padding normal de la IU, que establece el espacio entre los elementos, y el padding del texto, que establece el espacio desde una línea de base a la siguiente.

Este es el código que genera ese espaciado:

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

Esto es lo que sucede en este código:

  1. En el parámetro lambda measurable, puedes medir el Text representado por el parámetro medible llamando a measurable.measure(constraints).
  2. A fin de especificar el tamaño del elemento componible, llama al método layout(width, height), que también proporciona una expresión lambda que se usa para posicionar los elementos secundarios. En este caso, es la altura entre la última línea de base y el padding superior agregado.
  3. Para posicionar los elementos unidos en la pantalla, llama a placeable.place(x, y). Si no se colocan los elementos unidos, no serán visibles. La posición y corresponde al padding superior, es decir, la posición de la primera línea de base del texto.

Para verificar que funcione como se espera, usa este modificador sobre un 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))
    }
}

Varias vistas previas de elementos de texto; una muestra un padding común entre los elementos, y la otra el padding desde una línea de base a la siguiente

Cómo crear diseños personalizados

El modificador layout solo cambia el elemento componible al que se llama. Para medir y diseñar varios elementos componibles, usa el elemento Layout. Ese elemento te permite medir e implementar elementos secundarios de forma manual. Todos los diseños de nivel superior, como Column y Row, se compilan con el elemento componible Layout.

Compilemos una versión muy básica de Column. La mayoría de los diseños personalizados siguen este patrón:

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

Al igual que el modificador layout, measurables es la lista de elementos secundarios que deben medirse, y constraints son las restricciones del elemento superior. Siguiendo la misma lógica de antes, se puede implementar MyBasicColumn de la siguiente manera:

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

Los elementos componibles secundarios están limitados por las restricciones Layout (sin las restricciones minHeight) y se colocan según la yPosition del elemento componible anterior.

Así es cómo se usaría ese elemento personalizado:

@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!")
    }
}

Varios elementos de texto apilados, uno encima del siguiente en una columna.

Dirección del diseño

Para cambiar la dirección del diseño de un elemento componible, cambia el objeto de composition local LocalLayoutDirection.

Si quieres posicionar elementos componibles de manera manual en la pantalla, la LayoutDirection forma parte del LayoutScope del modificador layout o del elemento Layout.

Cuando uses layoutDirection, posiciona los elementos que admiten composición con place. A diferencia del método placeRelative, place no cambia en función de la dirección del diseño (de izquierda a derecha o de derecha a izquierda).

Diseños personalizados en acción

Obtén más información sobre los diseños y modificadores personalizados en el codelab de Diseños en Jetpack Compose y consulta los ejemplos de Compose que crean diseños personalizados para ver las API en acción.