[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user