Android

Android) CodeLab - Layouts in Jetpack Compose

가짜 개발자 2021. 11. 14. 22:02


Modifiers

  • Modifiers는 기존 View에서 xml 속성과 유사한 역할을 하는데, 범위별 Modifiers의 유형 안전성은 특정 레이아웃에 사용 가능하고 적용할 수 있는 항목을 검색하고 이해하는데 도움을 준다.
  • 함수의 파라미터로 Modifier를 넣어주는 컨벤션을 사용하면 좋다.
  • 전체 영역에 clickable 효과를 주고 싶을 때, padding은 clickable 뒤에 적용해주자.
  • CompositionLocalProvider : 컴포지션 트리를 통해 암시적으로 데이터를 전달할 수 있다.
@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
    Row(
        modifier
            .padding(8.dp)
            .clip(RoundedCornerShape(4.dp))
            .background(MaterialTheme.colors.surface)
            .clickable(onClick = {})
            .padding(16.dp)
    ) {
        Surface(
            modifier.size(50.dp),
            shape = CircleShape,
            color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
        ) {

        }

        Column(
            modifier
                .padding(start = 8.dp)
                .align(Alignment.CenterVertically)
        ) {
            Text("Alfred Sisley", fontWeight = FontWeight.Bold)
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("3 minutes ago", style = MaterialTheme.typography.body2)
            }
        }
    }
}

 

Scaffold

  • 가장 높은 수준의 컴포저블.
  • Scaffold를 사용하면 기본 Material Design 레이아웃 구조로 UI를 구현할 수 있다.
  • TopAppBar, BottomAppBar, FloatingActionButtion 및 Drawer 등을 제공한다.
@Composable
fun LayoutsCodelab() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(text = "LayoutsCodelab")
                },
                actions = {
                    IconButton(onClick = { /*TODO*/ }) {
                        Icon(Icons.Filled.Favorite, contentDescription = null)
                    }
                }
            )
        }
    ) { innerPadding ->
        BodyContent(Modifier.padding(innerPadding))
    }
}

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Column(modifier.padding(8.dp)) {
        Text(text = "Hi there!")
        Text(text = "Thanks for going through the Layouts codelab")
    }
}

 

Working with lists

  • 컴포즈는 Column과 Row로 리스트를 쉽게 표현할 수 있다.
  • Column은 기본적으로 스크롤을 다루지 못한다. verticalScroll 옵션을 통해 스크롤할 수 있다.
  • LazyColumn을 통해 보이는 아이템만 리스트에 렌더링 해서 성능을 개선시키자. 기존의 리사이클러뷰와 동일한 개념이다.
  • 이미지를 인터넷에서 렌더링 할 때, 스크롤 시에 렌더링이 블라킹 되지 않도록 remeberCoroutineScope를 사용하여 코루틴 스코프 내에서 suspend functions로 사용하자.
@ExperimentalCoilApi
@Preview
@Composable
fun ImageList() {
    val listSize = 100
    val scrollState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()

    Column {
        Row {
            Button(onClick = {
                coroutineScope.launch {
                    scrollState.animateScrollToItem(0)
                }
            }) {
                Text("Scroll to the top")
            }
            Button(onClick = {
                coroutineScope.launch {
                    scrollState.animateScrollToItem(listSize - 1)
                }
            }) {
                Text("Scroll to the end")
            }
        }
    }
    LazyColumn(state = scrollState) {
        items(100) {
            ImageListItem(it)
        }
    }
}

@ExperimentalCoilApi
@Composable
fun ImageListItem(index: Int) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(
            painter = rememberImagePainter(
                data = "https://developer.android.com/images/brand/Android_Robot.png"
            ),
            contentDescription = "Android Logo",
            modifier = Modifier.size(50.dp)
        )
        Spacer(Modifier.width(10.dp))
        Text("Item #$index", style = MaterialTheme.typography.subtitle1)
    }
}

 

Create your custom layout

  • 기본적으로 Column, Row, Box를 사용해서 레이아웃을 만들 수 있지만, 특별한 경우는 Layout 컴포저블을 사용할 수 있다.
  • Principles of Layout in Compose - 컴포즈 UI는 다중 측정을 허용하지 않는다. 자식을 두 번 이상 측정할 수 없음을 의미한다. 즉 오직 한 번만 자식을 측정한다.
@Composable
fun MyOwnColumn(
    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 child
            measurable.measure(constraints)
        }

        // Track the y co-ord we have placed children up to
        var yPosition = 0

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // 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
            }
        }
    }
}

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    MyOwnColumn(modifier.padding(8.dp)) {
        Text("MyOwnColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

 

Complex custom layout

 

  • 다음과 같은 복잡한 레이아웃(staggerd grid layout)을 구성하기 위해 Column과 Row만으로는 불가능하다. 레이아웃의 staggering(지그재그)을 표현할 수 없기 때문이다.
  • 그래서 이런 복잡한 레이아웃도 위에서 배웠던 custom layout을 사용하여 표현할 수 있다.
  • 자식을 오직 한번만 측정할 수 있음을 기억하자.
더보기

@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
    Card(
        modifier = modifier,
        border = BorderStroke(color = Color.Black, width = Dp.Hairline),
        shape = RoundedCornerShape(8.dp)
    ) {
        Row(
            modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(
                modifier = Modifier
                    .size(16.dp, 16.dp)
                    .background(color = MaterialTheme.colors.secondary)
            )
            Spacer(Modifier.width(4.dp))
            Text(text = text)
        }
    }
}

val topics = listOf(
    "Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
    "Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
    "Religion", "Social sciences", "Technology", "TV", "Writing"
)


@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Row(modifier = modifier.horizontalScroll(rememberScrollState())) {
        StaggeredGrid {
            for (topic in topics) {
                Chip(modifier = Modifier.padding(8.dp), text = topic)
            }
        }
    }
}

@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    rows: Int = 3,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        val rowWidths = IntArray(rows) { 0 }

        val rowHeights = IntArray(rows) { 0 }

        val placeables = measurables.mapIndexed { index, measurable ->
            val placeable = measurable.measure(constraints)

            val row = index % rows
            rowWidths[row] += placeable.width
            rowHeights[row] = Math.max(rowHeights[row], placeable.height)

            placeable
        }

        val width = rowWidths.maxOrNull()
            ?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ?: constraints.minWidth

        val height = rowHeights.sumOf { it }
            .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))

        val rowY = IntArray(rows) { 0 }
        for (i in 1 until rows) {
            rowY[i] = rowY[i - 1] + rowHeights[i - 1]
        }

        layout(width, height) {
            val rowX = IntArray(rows) { 0 }

            placeables.forEachIndexed { index, placeable ->
                val row = index % rows
                placeable.placeRelative(
                    x = rowX[row],
                    y = rowY[row]
                )
                rowX[row] += placeable.width
            }
        }
    }
}


@Preview
@Composable
fun ChipPreview() {
    BasicCodelabTheme {
        BodyContent()
    }
}

 

Constraint Layout

  • 컴포즈에도 Constraint Layout이 있다. 기본 레이아웃보다 더 복잡한 레이아웃을 구현할 때 유용하다.
  • createRefs() 또는 createRef() 메서드를 사용하여 Constraint Layout의 연결할 수 있다.
  • constraintAs라는 modifier를 사용하여 제약 조건을 지정할 수 있도록 한다.
  • linktTo를 사용하여 제약 조건을 지정한다.
  • parent는 Contstraint Layout에 대한 제약 조건을 지정하는 데 사용할 수 있는 기존 레퍼런스.
@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)
            // Centers Text horizontally in the ConstraintLayout
            centerHorizontallyTo(parent)
        })
    }
}

 

Intrinsics

  • 계속 강조하는 부분이 컴포즈는 자식을 한 번만 측정해야 한다는 것이다. 두 번 측정할 경우 런타임 에러가 발생한다.
  • 그러나, 자식을 측정하기 전에 정보가 필요할 경우가 있다.
  • 이때, Intrinsics가 실제로 측정되기 전에 쿼리에 대해 알려주는 역할을 한다.
  • intrinsicWidth와 intrinsicHeight를 통해 너비와 높이를 알아낸다.
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    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
        )
    }
}

반응형