El modelo de layout en Compose
Compose mide y coloca elementos en dos fases distintas. En la fase de medición, cada elemento recibe constraints (ancho mínimo, máximo, alto mínimo, máximo) y retorna su tamaño. En la fase de placement, los elementos se posicionan en coordenadas relativas al padre.
Una regla importante de Compose: cada elemento se mide exactamente una vez. No hay dos pasadas de medición como en el View System — esto hace que los layouts de Compose sean predeciblemente eficientes.
Layout — el composable para layouts custom
// La firma de Layout:
@Composable
fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
)
// MeasurePolicy es donde ocurre la magia:
// measurePolicy = MeasurePolicy { measurables, constraints ->
// // measurables: los hijos a medir
// // constraints: los límites que el padre nos impone
//
// // 1. Medir los hijos
// val placeables = measurables.map { it.measure(constraints) }
//
// // 2. Calcular nuestro propio tamaño
// layout(ancho, alto) {
// // 3. Colocar los hijos
// placeables.forEach { it.placeRelative(x, y) }
// }
// }
Reimplementar Column desde cero
// Una Column vertical simple — para entender el mecanismo
@Composable
fun MiColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// Los hijos pueden usar todo el ancho disponible
val childConstraints = constraints.copy(minHeight = 0)
// Medir todos los hijos
val placeables = measurables.map { measurable ->
measurable.measure(childConstraints)
}
// Nuestro ancho: el mayor de los hijos (o el mínimo de constraints)
val ancho = placeables.maxOfOrNull { it.width }
?.coerceIn(constraints.minWidth, constraints.maxWidth)
?: constraints.minWidth
// Nuestro alto: la suma de todos los hijos
val alto = placeables.sumOf { it.height }
.coerceIn(constraints.minHeight, constraints.maxHeight)
// Colocar los hijos uno debajo del otro
layout(ancho, alto) {
var yActual = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = yActual)
yActual += placeable.height
}
}
}
}
Un layout real: Badge superpuesto
// Layout que superpone un badge en la esquina superior derecha de su contenido
@Composable
fun ConBadge(
badge: @Composable () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = {
content() // primer medurable: el contenido principal
badge() // segundo medurable: el badge
},
modifier = modifier
) { measurables, constraints ->
check(measurables.size == 2)
// Medir el contenido con los constraints originales
val contenidoPlaceable = measurables[0].measure(constraints)
// El badge no tiene restricciones — se mide a su tamaño natural
val badgePlaceable = measurables[1].measure(Constraints())
// Nuestro tamaño es el del contenido
layout(contenidoPlaceable.width, contenidoPlaceable.height) {
// Contenido en (0, 0)
contenidoPlaceable.placeRelative(0, 0)
// Badge en la esquina superior derecha, medio desplazado
val badgeX = contenidoPlaceable.width - badgePlaceable.width / 2
val badgeY = -badgePlaceable.height / 2
badgePlaceable.placeRelative(badgeX, badgeY)
}
}
}
// Uso:
ConBadge(
badge = {
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.error
) {
Text("3", modifier = Modifier.padding(4.dp))
}
}
) {
Icon(Icons.Default.Notifications, contentDescription = "Notificaciones")
}
Constraints — los límites de medición
// Constraints tiene cuatro valores:
// minWidth, maxWidth, minHeight, maxHeight
// Constraints comunes:
val llenarAncho = constraints.copy(minWidth = constraints.maxWidth) // fuerza ancho máximo
val tamanioNatural = Constraints() // sin restricciones — tamaño propio
val acotarAlto = constraints.copy(maxHeight = 200.dp.roundToPx()) // limitar alto
// Cuando medís un hijo con constraints modificados:
val placeable = measurable.measure(
constraints.copy(minWidth = 0, minHeight = 0) // permite tamaño cero
)
// Los constraints son monótonos: no podés dar más espacio del que tenés
// (maxWidth no puede superar el maxWidth de los constraints recibidos)
SubcomposeLayout — medir antes de componer
SubcomposeLayout permite medir un elemento antes de componer los demás. Esto es lo que usa BoxWithConstraints internamente — mide el contenedor primero y luego compone el contenido con esa información:
// Implementar BoxWithConstraints desde cero
@Composable
fun MiBoxConConstraints(
modifier: Modifier = Modifier,
content: @Composable BoxWithConstraintsScope.() -> Unit
) {
SubcomposeLayout(modifier) { constraints ->
// Primero obtenemos los constraints disponibles
val scope = BoxWithConstraintsScopeImpl(
constraints = constraints,
density = this
)
// Luego componemos el contenido con esa información
val measurables = subcompose("contenido") {
scope.content()
}
val placeables = measurables.map { it.measure(constraints) }
val ancho = placeables.maxOfOrNull { it.width } ?: constraints.minWidth
val alto = placeables.maxOfOrNull { it.height } ?: constraints.minHeight
layout(ancho, alto) {
placeables.forEach { it.placeRelative(0, 0) }
}
}
}
// Caso de uso típico: layout que muestra diferente UI según el espacio disponible
@Composable
fun LayoutAdaptativo(content: @Composable (esAncho: Boolean) -> Unit) {
BoxWithConstraints {
val esAncho = maxWidth > 600.dp
content(esAncho)
}
}
Custom Modifiers con layout
// Un Modifier custom que agrega padding solo en el top
fun Modifier.paddingTop(valor: Dp): Modifier = this.then(
layout { measurable, constraints ->
val valorPx = valor.roundToPx()
// Reducir el alto disponible para el hijo
val childConstraints = constraints.copy(
maxHeight = if (constraints.maxHeight == Constraints.Infinity)
Constraints.Infinity
else
(constraints.maxHeight - valorPx).coerceAtLeast(0)
)
val placeable = measurable.measure(childConstraints)
layout(placeable.width, placeable.height + valorPx) {
placeable.placeRelative(0, valorPx)
}
}
)
// Modifier que intercepta el tamaño sin modificarlo (para logging o analytics)
fun Modifier.logSize(tag: String): Modifier = this.then(
layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
Log.d(tag, "Tamaño: ${placeable.width}x${placeable.height}")
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
)