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