--- id: android-compose-custom-layout title: Compose Custom Layout — Layout / SubcomposeLayout category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [android, compose, layout, vibe-coding] tech_stack: { language: "Kotlin / Compose", applicable_to: ["Android"] } applied_in: [] aliases: [Layout composable, measure, place, SubcomposeLayout, intrinsic, modifier] --- # Compose Custom Layout > Row / Column 으로 부족할 때. **Layout composable 직접 작성** = 측정 + 배치 제어. SubcomposeLayout 으로 child 측정 결과 기반 동적 layout. Modifier.layout 도 가능. ## 📖 핵심 개념 - Single-pass measurement: 측정 한 번 → 배치 한 번. - Constraints: min/max width/height. - Placeable: measure 의 결과. - SubcomposeLayout: 자식의 measure 결과로 다른 자식 만들기. ## 💻 코드 패턴 ### Layout (단순) ```kotlin @Composable fun StaggeredGrid( columns: Int, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { Layout(content = content, modifier = modifier) { measurables, constraints -> val cellWidth = constraints.maxWidth / columns val cellConstraints = constraints.copy(minWidth = 0, maxWidth = cellWidth) val placeables = measurables.map { it.measure(cellConstraints) } // 컬럼별 height tracking val colHeights = IntArray(columns) { 0 } val positions = placeables.map { p -> val col = colHeights.indexOf(colHeights.min()!!) val x = col * cellWidth val y = colHeights[col] colHeights[col] += p.height x to y } val totalHeight = colHeights.max() ?: 0 layout(constraints.maxWidth, totalHeight) { placeables.forEachIndexed { i, p -> val (x, y) = positions[i] p.placeRelative(x, y) } } } } ``` ### Modifier.layout ```kotlin fun Modifier.firstBaselineToTop(top: Dp) = layout { measurable, constraints -> val placeable = measurable.measure(constraints) val baseline = placeable[FirstBaseline] val placeY = top.roundToPx() - baseline layout(placeable.width, placeable.height + placeY) { placeable.placeRelative(0, placeY) } } ``` ### SubcomposeLayout (자식 측정 후 다른 자식) ```kotlin @Composable fun TwoColumnsWithMatchedHeight( left: @Composable () -> Unit, right: @Composable () -> Unit, ) { SubcomposeLayout { constraints -> // 1. 먼저 left 측정 val leftPlaceables = subcompose("left", left).map { it.measure(constraints) } val leftHeight = leftPlaceables.maxOf { it.height } // 2. right 를 leftHeight 로 강제 val rightPlaceables = subcompose("right", right) .map { it.measure(constraints.copy(minHeight = leftHeight, maxHeight = leftHeight)) } val width = leftPlaceables.maxOf { it.width } + rightPlaceables.maxOf { it.width } layout(width, leftHeight) { var x = 0 leftPlaceables.forEach { it.place(x, 0); x += it.width } rightPlaceables.forEach { it.place(x, 0); x += it.width } } } } ``` ### ParentDataModifier (자식이 부모에 hint) ```kotlin class WeightModifier(val weight: Float) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?): Any = this@WeightModifier } fun Modifier.weight(weight: Float) = then(WeightModifier(weight)) @Composable fun WeightedRow(content: @Composable () -> Unit) { Layout(content) { measurables, constraints -> val totalWeight = measurables.sumOf { (it.parentData as? WeightModifier)?.weight?.toDouble() ?: 0.0 } // ... distribute width by weight } } ``` ### Intrinsic measurement ```kotlin @Composable fun MyBox(content: @Composable () -> Unit) { Layout(content) { measurables, constraints -> // Intrinsic: 자식의 minHeight 알고 싶음 (실제 측정 전) val maxIntrinsicHeight = measurables.maxOf { it.maxIntrinsicHeight(constraints.maxWidth) } // ... } } ``` 부모가 `Modifier.height(IntrinsicSize.Min)` 사용 시 invoked. ### Animation 친화 — looping content ```kotlin @Composable fun MarqueeText(text: String, modifier: Modifier = Modifier) { val measurer = rememberTextMeasurer() val textLayout = measurer.measure(text, TextStyle.Default) val width = textLayout.size.width val offset = remember { Animatable(0f) } LaunchedEffect(Unit) { offset.animateTo(-width.toFloat(), animationSpec = tween(5000, easing = LinearEasing)) } Canvas(modifier) { drawText(textLayout, topLeft = Offset(offset.value, 0f)) drawText(textLayout, topLeft = Offset(offset.value + width, 0f)) } } ``` ## 🤔 의사결정 기준 | 상황 | 도구 | |---|---| | 단순 grid / row / column | Row, Column, Grid | | Staggered / flow | LazyVerticalStaggeredGrid / FlowRow | | 부모가 자식 크기에 따라 다르게 | SubcomposeLayout | | 자식이 부모에 weight | ParentDataModifier | | 측정 변형 만 | Modifier.layout | | Canvas / 그림 | Canvas + drawIntoCanvas | ## ❌ 안티패턴 - **Layout 안에서 Composable 다중 측정**: 한 번만 measure. - **placeable.place + RTL 무시**: placeRelative 가 자동. - **Intrinsic 함수 매번 무거움**: cache or 단순화. - **SubcomposeLayout 남발**: 측정 N 번 = 느림. 정말 필요할 때만. - **Modifier 대신 Composable 만들기 — 유연성 잃음**: Modifier 가 유연. - **constraints 무시 + 자기 마음대로**: parent / child 가정 충돌. ## 🤖 LLM 활용 힌트 - 90% = Row / Column / FlowRow / LazyGrid 로 충분. - 10% = Layout 직접 (staggered). - SubcomposeLayout 은 정말 필요할 때만. ## 🔗 관련 문서 - [[Android_Compose_State_Hoisting]] - [[Android_Compose_Recomposition_Pitfalls]] - [[React_Component_Composition]]