Diseños en Compose

Jetpack Compose facilita mucho el diseño y la compilación de la IU de tu app. En este documento, se explican algunos de los componentes fundamentales que proporciona Compose para ayudarte a diseñar los elementos de tu IU, y se muestra cómo compilar diseños más especializados cuando los necesites.

Objetivos de los diseños en Compose

La implementación de Jetpack Compose del sistema de diseño tiene dos objetivos principales: ser de alto rendimiento y facilitar la escritura de diseños personalizados. Para lograr un alto rendimiento en Compose, se prohíbe que los elementos secundarios de diseño se midan más de una vez. En caso de que se necesiten varias mediciones, Compose tiene un sistema especial, las medidas intrínsecas. Puedes obtener más información sobre esta función en la sección sobre mediciones intrínsecas.

Conceptos básicos de las funciones que admiten composición

Las funciones que admiten composición son los componentes fundamentales de Compose. Una función de este tipo emite una Unit que describe alguna parte de tu IU. La función toma alguna entrada y genera lo que se muestra en la pantalla. Para obtener más información sobre elementos componibles, consulta la documentación sobre el modelo mental de Compose.

Una función que admite composición podría emitir varios elementos de la IU. Sin embargo, si no indicas cómo deben organizarse, es posible que Compose lo haga de una forma que no te agrade. Por ejemplo, este código genera dos elementos de texto:

@Composable
fun ArtistCard() {
    Text("Alfred Sisley")
    Text("3 minutes ago")
}

Si no tiene indicaciones sobre cómo quieres organizarlos, Compose los apila uno encima del otro y resultan ilegibles:

Dos elementos de texto dibujados uno encima del otro, lo que hace que el texto sea ilegible

Compose proporciona una colección de diseños listos para usar que te ayudan a organizar los elementos de la IU y facilitan la definición de tus propios diseños más especializados.

Componentes de diseño estándar

En muchos casos, puedes usar los elementos de diseño estándar de Compose.

Usa Column para colocar elementos en sentido vertical en la pantalla.

@Composable
fun ArtistCard() {
    Column {
        Text("Alfred Sisley")
        Text("3 minutes ago")
    }
}

Dos elementos de texto organizados en un diseño de columna; por lo tanto, el texto es legible

Del mismo modo, usa Row para colocar los elementos en sentido horizontal en la pantalla. Tanto Column como Row admiten la configuración de alineación de los elementos que contienen.

@Composable
fun ArtistCard(artist: Artist) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(/*...*/)
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

Muestra un diseño más complejo, con un pequeño gráfico junto a una columna de elementos de texto

Usa Box para colocar un elemento sobre otro.

Compara tres elementos de diseño simples que admiten composición: columnas, filas y cuadros.

Estos componentes fundamentales suelen ser todo lo que necesitas. Puedes escribir tu propia función que admita composición para combinar esos diseños en uno más elaborado que se adapte a tu app.

Para establecer la posición de los elementos secundarios dentro de un Row, configura los argumentos horizontalArrangement y verticalAlignment. Para un objeto Column, configura los argumentos verticalArrangement y horizontalAlignment:

@Composable
fun ArtistCard(artist: Artist) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.End
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

Los elementos están alineados a la derecha

Modificadores

Los modificadores te permiten decorar o aumentar un elemento componible. Por ejemplo, puedes hacer todo esto:

  • Cambiar el tamaño, el diseño, el comportamiento y el aspecto del elemento componible
  • Agregar información (p. ej., etiquetas de accesibilidad)
  • Procesar entradas del usuario
  • Agregar interacciones de nivel superior, (p. ej., hacer que un elemento sea apto para hacer clic, desplazable, arrastrable o ampliable)

Los modificadores son objetos estándar de Kotlin. Para crear uno, llama a una de las funciones de clase Modifier. Puedes encadenar esas funciones para crear una composición:

@Composable
fun ArtistCard(
    artist: Artist,
    onClick: () -> Unit
) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
        Spacer(Modifier.size(padding))
        Card(elevation = 4.dp) { /*...*/ }
    }
}

Un diseño aún más complejo, que usa modificadores para cambiar la disposición de los gráficos y las áreas que responden a la entrada del usuario

En el código anterior, observa distintas funciones de modificadores que se usan juntas.

  • clickable hace que un elemento componible reaccione a la entrada del usuario y muestre una onda.
  • padding coloca espacio alrededor de un elemento.
  • fillMaxWidth hace que el elemento componible ocupe el ancho máximo que le otorga su elemento superior.
  • size() especifica el ancho y la altura preferidos de un elemento.

El orden de los modificadores es importante

El orden de las funciones de los modificadores es importante. Como cada función realiza cambios en el Modifier que muestra la función anterior, la secuencia afecta al resultado final. Veamos un ejemplo:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

Toda el área, incluso el padding alrededor de los bordes, responde a los clics

En el código anterior, se puede hacer clic en toda el área, incluso en el padding que la rodea, porque se aplicó el modificador padding después del modificador clickable. Si se invierte el orden de los modificadores, el espacio que agrega padding no reacciona a la entrada del usuario:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .padding(padding)
            .clickable(onClick = onClick)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

El padding alrededor del borde del diseño ya no responde a los clics

Modificadores integrados

Jetpack Compose proporciona una lista de modificadores integrados para ayudarte a decorar o compilar un elemento componible. Ya se introdujeron algunos modificadores, como padding, clickable y fillMaxWidth. A continuación, se incluye una lista de otros modificadores comunes:

size

De forma predeterminada, los diseños proporcionados en Compose unen sus objetos secundarios. Sin embargo, puedes establecer un tamaño con el modificador size:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

Ten en cuenta que el tamaño especificado podría no respetarse si no cumple con las restricciones provenientes del elemento superior del diseño. Si necesitas que el tamaño del elemento componible se corrija independientemente de las restricciones entrantes, usa el modificador requiredSize:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.requiredSize(150.dp)
        )
        Column { /*...*/ }
    }
}

La imagen secundaria es más grande que las restricciones que provienen de su elemento superior

En este ejemplo, incluso con el height superior establecido en 100.dp, la altura de la Image será 150.dp, ya que el modificador requiredSize tiene prioridad.

Si quieres que un diseño secundario rellene todo el alto disponible que permite el elemento superior, agrega el modificador fillMaxHeight (Compose también proporciona fillMaxSize y fillMaxWidth):

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.fillMaxHeight()
        )
        Column { /*...*/ }
    }
}

La altura de la imagen es tan grande como la del elemento superior

Si deseas agregar padding sobre un modelo de referencia de texto para alcanzar una distancia específica desde la parte superior del diseño al modelo de referencia, usa el modificador paddingFromBaseline:

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(
                text = artist.name,
                modifier = Modifier.paddingFromBaseline(top = 50.dp)
            )
            Text(artist.lastSeenOnline)
        }
    }
}

Texto con padding encima

Desplazamiento

Para posicionar un diseño relacionado con su posición original, agrega el modificador offset y configura el desplazamiento en los ejes y y x. Las compensaciones pueden ser positivas y no positivas. La diferencia entre padding y offset es que agregar un offset a un elemento componible no cambia sus mediciones:

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(artist.name)
            Text(
                text = artist.lastSeenOnline,
                modifier = Modifier.offset(x = 4.dp)
            )
        }
    }
}

El texto se movió al lado derecho del contenedor superior

El modificador offset se aplica horizontalmente según la dirección del diseño. En un contexto de izquierda a derecha, un offset positivo cambia el elemento hacia la derecha, mientras que en un contexto de derecha a izquierda desplaza el elemento hacia la izquierda. Si necesitas establecer un desplazamiento sin considerar la dirección del diseño, consulta el modificador absoluteOffset, en el que un valor de desplazamiento positivo siempre cambia el elemento hacia la derecha.

Seguridad de tipo en Compose

En Compose, existen modificadores que solo funcionan cuando se aplican a elementos secundarios de determinados elementos que admiten composición. Por ejemplo, si deseas que un elemento secundario sea tan grande como el Box superior sin afectar el tamaño de Box, usa el modificador matchParentSize.

Compose aplica de manera forzosa esta seguridad de tipo mediante alcances personalizados. Por ejemplo, matchParentSize solo está disponible en BoxScope. Por lo tanto, solo se puede usar cuando se usa el elemento secundario dentro de un Box.

Los modificadores con alcance notifican al elemento superior sobre cierta información del elemento secundario que el superior debe conocer. Por lo general, también se los conoce como modificadores de datos del elemento superior. Sus aspectos internos difieren de los modificadores de uso general, pero, desde una perspectiva de uso, estas diferencias no resultan importantes.

matchParentSize en el cuadro

Como se mencionó con anterioridad, si deseas que un diseño secundario tenga el mismo tamaño que un Box superior sin afectar el tamaño del Box, usa el modificador matchParentSize.

Ten en cuenta que matchParentSize solo está disponible dentro del alcance de un Box, lo que significa que solo se aplica a elementos secundarios directos de elementos componibles de Box.

En el siguiente ejemplo, el Spacer secundario toma su tamaño desde el elemento Box superior, que, a su vez, toma su tamaño del elemento secundario más grande, ArtistCard en este caso.

@Composable
fun MatchParentSizeComposable() {
    Box {
        Spacer(Modifier.matchParentSize().background(Color.LightGray))
        ArtistCard()
    }
}

Fondo gris que rellena su contenedor

Si se usara fillMaxSize en lugar de matchParentSize, Spacer tomaría todo el espacio disponible para el elemento superior, lo que podría hacer que este se expanda y rellene todo el espacio disponible.

Fondo gris que ocupa toda la pantalla

El grosor en Fila y Columna

Como viste en la sección anterior Padding y tamaño, el tamaño de un objeto componible se define de forma predeterminada según el contenido al que está unido. Puedes configurar el tamaño de un elemento componible para que sea flexible dentro de su elemento superior usando el modificador weight que solo está disponible en RowScope y ColumnScope.

Tomemos un objeto Row que contiene dos elementos componibles Box. El primer cuadro recibe el doble de weight del segundo, por lo que recibe el doble del ancho. Dado que Row tiene 210.dp de ancho, el primer Box tiene 140.dp de ancho y, el segundo, 70.dp:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Image(
            /*...*/
            modifier = Modifier.weight(2f)
        )
        Column(
            modifier = Modifier.weight(1f)
        ) {
            /*...*/
        }
    }
}

El ancho de la imagen es el doble del ancho del texto

Diseños desplazables

Obtén más información sobre los diseños desplazables en la documentación sobre gestos de Compose.

Para obtener información sobre listas y listas Lazy, consulta la documentación sobre listas de Compose.

Diseños receptivos

Un diseño debe tener en cuenta diferentes orientaciones de pantalla y tamaños de factores de forma. Compose ofrece configuraciones integradas de forma inmediata para facilitar la adaptación de tus diseños componibles a diferentes configuraciones de pantalla.

Restricciones

Para conocer las restricciones que provienen del elemento superior y diseñar el diseño según corresponda, puedes usar BoxWithConstraints. Las restricciones de medición se pueden encontrar en el alcance de la lambda de contenido. Puedes usar estas restricciones de medición a fin de componer diferentes diseños para distintas configuraciones de pantalla:

@Composable
fun WithConstraintsComposable() {
    BoxWithConstraints {
        Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
    }
}

Diseños basados en ranuras

Compose proporciona una gran variedad de elementos componibles basados en Material Design con la dependencia androidx.compose.material:material (que se incluye cuando se crea un proyecto de Compose en Android Studio) para facilitar la compilación de IU. Estos elementos incluyen Drawer, FloatingActionButton y TopAppBar.

Los componentes de Material usan mucho las API de ranuras, un patrón que introduce Compose para agregar una capa de personalización sobre elementos componibles. Este enfoque hace que los componentes sean más flexibles, ya que aceptan un elemento secundario que puede configurarse automáticamente, en lugar de tener que exponer cada parámetro de configuración del elemento secundario. Las ranuras dejan un espacio vacío en la IU para que el desarrollador lo complete como quiera. Por ejemplo, estas son las ranuras que puedes personalizar en una TopAppBar:

Diagrama que muestra los espacios disponibles en la barra de la app de componentes de Material

Los elementos componibles suelen adoptar una expresión lambda que admite composición content (content: @Composable () -> Unit). Las API con ranuras exponen varios parámetros de content para usos específicos. Por ejemplo, TopAppBar te permite proporcionar el contenido para title, navigationIcon y actions.

Por ejemplo, Scaffold te permite implementar una IU con la estructura básica de diseño de Material Design. Scaffold proporciona ranuras para los componentes de Material de nivel superior más comunes, como TopAppBar, BottomAppBar, FloatingActionButton y Drawer. Si usas Scaffold, es fácil asegurarte de que esos componentes estén bien posicionados y funcionen de forma correcta.

La app de muestra de JetNews, que usa Scaffold para posicionar varios elementos

@Composable
fun HomeScreen(/*...*/) {
    Scaffold(
        drawerContent = { /*...*/ },
        topBar = { /*...*/ },
        content = { /*...*/ }
    )
}

ConstraintLayout

ConstraintLayout puede ayudar a posicionar elementos componibles en relación con otros en la pantalla y es una alternativa al uso de varios Row, Column, Box anidados y diseños personalizados. ConstraintLayout resulta útil cuando se implementan diseños más grandes con requisitos de alineación más complejos.

Para usar ConstraintLayout en Compose, debes agregar esta dependencia en tu build.gradle:

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha08"

En Compose, ConstraintLayout funciona con una DSL:

  • Las referencias se crean con createRefs() o createRefFor(), y cada elemento componible en ConstraintLayout debe tener una referencia asociada.
  • Las restricciones se proporcionan mediante el modificador constrainAs(), que toma la referencia como parámetro y te permite especificar sus restricciones en la expresión lambda del cuerpo.
  • Las restricciones se especifican mediante linkTo() o algún otro método útil.
  • parent es una referencia existente que se puede usar para especificar restricciones hacia el mismo elemento ConstraintLayout.

En este ejemplo vemos uno de esos elementos que usa un ConstraintLayout:

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

Este código restringe la parte superior del Button al elemento superior, con un margen de 16.dp, y un Text a la parte inferior del Button, también con un margen de 16.dp.

Muestra un botón y un elemento de texto organizados en un ConstraintLayout

A fin de obtener más ejemplos para trabajar con ConstraintLayout, prueba el codelab de diseños.

API desacoplada

En el ejemplo de ConstraintLayout, las restricciones se especifican de forma intercalada, con un modificador en el elemento que admite composición al que se aplican. Sin embargo, hay situaciones en las que es preferible desacoplar las restricciones de los diseños a los que se aplican. Por ejemplo, quizás querrías cambiar las restricciones en función de la configuración de la pantalla o agregar una animación entre dos conjuntos de restricciones.

En casos como esos, puedes usar ConstraintLayout de otro modo:

  1. Pasa un ConstraintSet como parámetro a ConstraintLayout.
  2. Asigna referencias creadas en el ConstraintSet a los elementos que admiten composición con el modificador layoutId.
@Composable
fun DecoupledConstraintLayout() {
    BoxWithConstraints {
        val constraints = if (minWidth < 600.dp) {
            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)
        }
    }
}

Luego, cuando necesites cambiar las restricciones, simplemente puedes pasar un ConstraintSet diferente.

Más información

Obtén más información sobre ConstraintLayout en Compose, en la sección de Diseño de restricciones correspondiente a los Diseños que se encuentran en el codelab de Jetpack Compose y consulta los ejemplos de Compose que usan ConstraintLayout para ver las API en acción.

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 una situación como esta, que se analizan en la sección de mediciones 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,
        children = 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.

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.

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

Medidas intrínsecas

Una de las reglas de Compose es que solo debes medir tus elementos secundarios una vez. Si lo haces dos veces, se genera una excepción de tiempo de ejecución. Sin embargo, hay momentos en los que necesitas información sobre tus elementos secundarios antes de medirlos.

Los elementos intrínsecos te permiten realizar consultas a los elementos secundarios antes de que se midan realmente.

Para un elemento componible, puedes solicitar su intrinsicWidth o intrinsicHeight:

  • (min|max)IntrinsicWidth: Con esta altura, ¿cuál es el ancho mínimo y máximo con el que puedes pintar el contenido de manera correcta?
  • (min|max)IntrinsicHeight: Con este ancho, ¿cuál es la altura mínima o máxima con la que puedes pintar correctamente el contenido?

Por ejemplo, si solicitas la minIntrinsicHeight de un Text con width infinito, se mostrará la height del Text como si se hubiera dibujado el texto en una sola línea.

Funciones intrínsecas en acción

Imagina que queremos crear un elemento componible que muestre dos textos en la pantalla separados por un divisor como este:

Dos elementos de texto, uno al lado del otro, con un divisor vertical entre ellos

¿Cómo podemos hacer esto? Podemos tener un objeto Row con dos Text que se expandan tanto como sea posible y un Divider en el medio. Queremos que el divisor sea delgado (width = 1.dp) y tan alto como el Text más alto.

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    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() {
    MaterialTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

En la vista previa, vemos que el divisor se expande a toda la pantalla, pero eso no es lo que deseamos:

Dos elementos de texto, uno al lado del otro, con un divisor entre ellos, pero el divisor se expande debajo de la parte inferior del texto

Esto ocurre porque Row mide cada elemento secundario de forma individual, y la altura de Text no se puede usar para restringir Divider. Queremos que el Divider ocupe el espacio disponible con una altura determinada. Para eso, podemos usar el modificador height(IntrinsicSize.Min).

height(IntrinsicSize.Min) ajusta su tamaño a los elementos secundarios para que sean tan altos como su altura mínima intrínseca. Como es recurrente, realizará consultas a Row y sus elementos secundarios minIntrinsicHeight.

Cuando lo apliquemos a nuestro código, funcionará según lo esperado:

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    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
        )
    }
}

Con vista previa:

Dos elementos de texto, uno al lado del otro, con un divisor vertical entre ellos

La minIntrinsicHeight del elemento componible Row será la minIntrinsicHeight máxima de sus elementos secundarios. El minIntrinsicHeight de Divider element's es 0, ya que no ocupa espacio si no se le aplican restricciones. El minIntrinsicHeight de Text será el del texto según un width específico. Por lo tanto, la restricción height del elemento Row será la minIntrinsicHeight máxima de los Text. Luego, Divider expandirá su height a la restricción height proporcionada por la Row.

Funciones intrínsecas en tus diseños personalizados

Cuando se crea un modificador Layout o layout personalizado, las mediciones intrínsecas se calculan automáticamente en función de aproximaciones. Por lo tanto, es posible que los cálculos no sean correctos para todos los diseños. Estas API ofrecen opciones para anular estos valores predeterminados.

A fin de especificar las mediciones intrínsecas de tu Layout personalizado, anula los minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth y maxIntrinsicHeight de la interfaz MeasurePolicy al momento de su creación.

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    return object : MeasurePolicy {
        override fun MeasureScope.measure(
            measurables: List<Measurable>,
            constraints: Constraints
        ): MeasureResult {
            // Measure and layout here
        }

        override fun IntrinsicMeasureScope.minIntrinsicWidth(
            measurables: List<IntrinsicMeasurable>,
            height: Int
        ) = {
            // Logic here
        }

        // Other intrinsics related methods have a default value,
        // you can override only the methods that you need.
    }
}

Cuando crees el modificador layout personalizado, anula los métodos relacionados en la interfaz LayoutModifier.

fun Modifier.myCustomModifier(/* ... */) = this.then(object : LayoutModifier {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // Measure and layout here
    }

    override fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int = {
        // Logic here
    }

    // Other intrinsics related methods have a default value,
    // you can override only the methods that you need.
})

Más información

Obtén más información sobre las mediciones intrínsecas en la sección Funciones intrínsecas del codelab de Diseños en Jetpack Compose.

Más información

Para obtener más información, prueba el codelab de diseños de Jetpack Compose.