카테고리 없음
📜 레이아웃 배우기 (핵심만 골라담는 젯팩 컴포즈 210-286p)
minnote29
2025. 4. 30. 14:47
사용자 인터페이스 디자인이란 주로 적절한 인터페이스 컴포넌트를 선택하고, 그 뷰들을 화면에 어떻게 배치할지 결정하고, 앱의 다양한 화면 간 이동을 구현하는 것인데 컴포즈는 앱을 개발할 때 이용할 수 있는 사용자 인터페이스를 구조화하고, 화면 방향이나 크기 변경 같은 요소들에 대한 레이아웃의 반응 방법을 정의할 수 있다.
🍎 RowColDemo 프로젝트
- 이 예시에서는 커스텀 컴포넌트인 TextCell의 인스턴스를 이용해 행/열 기반 레이아웃을 구현한 것이다. TextCell 컴포넌트는 검은 테두리 안에 텍스트를 표시하며, 약간의 패딩을 이용해 컴포넌트 사이의 간격을 조정한다.
- Row 컴포저블은 이름 그대로 자식 컴포넌트를 화면의 수평 방향으로 배열한다.
- Column 컴포저블은 Row 컴포저블과 동일한 목적을 수행하지만, 자식들을 수직 방향으로 배열한다는 점이 다르다.
- Row, Column 컴포저블을 조합해 표 스타일의 레이아웃을 만들 수도 있다.
@Composable
fun MainScreen2() {
Column {
Row {
Column {
TextCell("1")
TextCell("2")
TextCell("3")
}
Column {
TextCell("7")
TextCell("8")
}
Column {
TextCell("9")
TextCell("10")
TextCell("11")
}
}
Row {
TextCell("9")
TextCell("10")
TextCell("11")
}
}
}
- 이 기법을 활용해 Row, Column 레이아웃이 서로 포함되도록 하면 어떤 복잡한 레이아웃이라도 구성할 수 있다.
- Row, Column 컴포저블은 모두 사용자 인터페이스 레이아웃 안의 공간을 차지한다. 차지하는 공간은 자식 요소, 다른 컴포저블, 크기 관련 설정을 적용하는 모디파이어들에 따라 달라진다. 기본적으로 Row와 Column 내부의 자식 요소 그룹들은 콘텐츠 영역의 가장 왼쪽 위 모서리를 기준으로 정렬된다. 처음 만들었던 Row 컴포저블의 크기를 늘려보면 이를 확인할 수 있다.
- 이 변경을 적용하기 전 Row는 그 자식들을 감싸고 있다. (그 크기를 콘텐츠에 맞춘 상태.)
- 수직 방향 축의 기본 정렬 -> Row 컴포저블의 verticalAlignment 파라미터에 값을 전달해서 변경 가능. 수직 방향 중앙 정렬은 Alignment.CenterVertically 값을 Row에 전달하면 됨.
- Alignment.Top: 콘텐츠를 Row 영역의 수직 방향 위의 위치에 정렬한다. Bottom이면 아래 위치, Start는 수평 방향 시작 위치, Centerhorizontally는 Column 영역의 수평 방향 가운데 위치에 정렬, End는 Column 영역의 끝 위치로 정렬한다.
- 정력을 다룰 때는 컴포저블을 포함하는 흐름과 반대 축 기준으로 동작한다는 사실을 기억해야 한다.
- 정렬과 달리 배열은 자식의 위치를 컨테이너와 동일 축을 따라 제어한다. (Row에서는 수평(horizontalArrangement), Column에서는 수직 방향(verticalArrangement).)
- Arragement.Start는 콘텐츠를 Row 콘텐츠 영역의 수평 방향 시작 위치에 정렬한다. Center는 가운데, End는 끝 위치
- 배열 간격 조정을 이용해 Row 또는 Column 안의 자식 컴포넌트들의 콘텐츠 영역 안에서 간격을 조정한다. 이 설정은 horizontalArragement, verticalArragement 파라미터를 이용해 정의할 수 있다.
- Arragement.SpaceEvenly는 자식들을 균일한 간격을 유지시킨다. 첫 번째 자식의 앞, 마지막 자식의 뒷 공간을 포함한다. SpaceBetween는 첫 번째 자식의 앞, 마지막 자식의 뒷 공간을 포함하지 않는다. SpaceAround는 첫 번째 자식의 앞, 마지막 자식의 뒷 공간은 각 자식들 사이 공간의 절반이다.
📌 Row, Column 스코프 모디파이어
- 흔히 Row 또는 Column의 자식들은 부모의 스코프 안에 있다고 말한다. 이 두 스코프는 추가 모디파이어 함수들을 제공하며, 이를 이용해 Row 또는 Column 안에 포함된 각 자식들의 동작이나 형태를 변경할 수 있다. 편집기에서는 자식들이 스코프 안에 있을 때 시각적으로 표시한 기능을 제공한다. RowScope, ColumnScope 둘다.
- ColumnScope
- Modifier.align() - Alignment.CenterHorizontally, Alignment.Start, Alignment.End 값을 이용해 자식들을 수평으로 정렬한다.
- Modifier.alignBy() - 자식들과 alignBy() 모디파이어가 적용된 다른 형제를 수평으로 정렬한다.
- Modifier.weight() - 형제에 할당된 가중치에 따라 자식의 높이를 설정한다.
- RowScope
- Modifier.align() - CenterVertically, Top, Bottom 값 이용해 자식들을 수직 정렬.
- alignBy() - 정렬을 베이스 라인 또는 커스텀 정렬 라인 설정에 따라 수행할 수 있다.
- alignByBaseline() - 자식의 베이스라인을 alignBy() 또는 alignByBaseline() 모디파이어가 이미 적용된 형제들과 정렬한다.
- paddingFrom() - 자식의 정렬 라인에 패딩을 추가한다.
- weight - 형제에 할당된 가중치에 따라 자식의 폭을 설정한다.
- 사진은 Row가 2개의 Text 컴포저블의 위쪽 가장자리를 따라 정렬했고, 결과적으로 텍스트 베이스라인에서 벗어난 형태라고 할 수 있는데 이 문제를 해결할 때는 두 자식에게 alignByBaseline() 모디파이어를 적용하면 된다.
- 또는 alignByBaseline() 모디파이어를 alignBy() 함수 호출로 변경할 수도 있다. 이때는 정렬 파라미터에 FirstBaseline을 전달한다. -> Modifier.alignBy(FirstBaseline)
- 특정 자식의 정렬에 오프셋을 적용할 때는 paddingFrom() 모디파이어를 이용할 수 있다.
- RowScope 가중치 모디파이어를 이용하면 각 자식의 폭을 그 형제들을 기준으로 상대적으로 지정할 수 있다. ex) weight = 0.2f
- ColumnScope도 동일하게 가능한데, 베이스라인 개념은 존재하지 않는다.
package com.example.minexample2
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.tooling.preview.Preview
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MainScreen()
}
}
}
}
}
@Composable
fun MainScreen() {
Row {
TextCell("1", Modifier.weight(weight = 0.2f, fill = true))
TextCell("2", Modifier.weight(weight = 0.4f, fill = true))
TextCell("3", Modifier.weight(weight = 0.3f, fill = true))
}
}
@Composable
fun TextCell(text: String, modifier: Modifier = Modifier) {
val cellModifier = Modifier
.padding(4.dp)
.size(100.dp, 100.dp)
.border(width = 4.dp, color = Color.Black)
Text(text = text, cellModifier.then(modifier),
fontSize = 70.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center)
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MaterialTheme {
MainScreen()
}
}
MainActivity.kt
🍎 Box 레이아웃
- Row와 Column이 자식들을 수평의 행 또는 수직의 열로 구조화하는 것과 달리 Box 레이아웃은 자식들을 위로 쌓아 올린다(스택). 쌓이는 순서는 Box 선언 안에서 자식들을 호출한 순서에 따라 정의된다. 첫 번째로 호출된 자식을 스택의 가장 아래에 위치한다. Row, Column 레이아웃과 마찬가지로 Box는 여러 가지 파라미터와 모디파이어를 제공하며 이를 이용해 레이아웃을 커스터마이즈할 수 있다.
🍎 BoxLayout 프로젝트 만들기
구현 코드
@Composable
fun MainBoxScreen() {
Box(contentAlignment = Alignment.CenterEnd,
modifier = Modifier.size(height = 90.dp, width = 290.dp)) {
Text("TopStart", Modifier.align(Alignment.TopStart))
Text("TopCenter", Modifier.align(Alignment.TopCenter))
Text("TopEnd", Modifier.align(Alignment.TopEnd))
Text("CenterStart", Modifier.align(Alignment.CenterStart))
Text("Center", Modifier.align(Alignment.Center))
Text(text = "CenterEnd", Modifier.align(Alignment.CenterEnd))
Text("BottomStart", Modifier.align(Alignment.BottomStart))
Text("BottomCenter", Modifier.align(Alignment.BottomCenter))
Text("BottomEnd", Modifier.align(Alignment.BottomEnd))
}
// Box(Modifier.size(200.dp).clip(RoundedCornerShape(30.dp)).background(Color.Blue))
}
@Composable
fun TextCell2(text: String, modifier: Modifier = Modifier, fontSize: Int = 150 ) {
val cellModifier = Modifier
.padding(4.dp)
.border(width = 5.dp, color = Color.Black)
Surface {
Text(
text = text, cellModifier.then(modifier),
fontSize = fontSize.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
}
- 그냥 호출하면 Text 컴포저블은 기본적으로 투명하므로 3개의 자식이 위로 쌓이는 형태일 것이다. 자식들이 쌓여있음을 확인하는 목적으로는 투명한 배경이 유용하지만, 이를 확인하는 거싱 프로젝트의 목적은 아니다. TextCell의 배경을 불투명하게 하려면 Surface 컴포넌트 안에서 Text 컴포저블을 호출해야 한다. 미리 보기가 업데이트되면 Box에서 호출된 마지막 컴포저블, 즉 스택의 가장 위에 위치한 자식만 나타난다.
📌 Box 정렬
- Box 컴포저블은 하나의 정렬 파라미터를 제공하며, 이를 이용하면 박스의 콘텐츠 영역 안에 있는 자식 그룹의 위치를 커스터마이즈할 수 있다. 파라미터의 이름은 contentAlignment이며, 아래처럼 위치를 표시할 수 있다.
📌 BoxScope 모디파이어
- Modifier.align(): Box 콘텐츠 영역 안의 자식을 정렬한다. 지정한 Alignment 값을 이용한다.
- matchParentSize(): 모디파이어가 적용된 자식의 크기를 부모 Box의 크기에 맞춘다.
- align 모디파이어가 받은 Alignment 값의 집합은 Box 정렬에서 소개한 리스트와 동일하다.
- clip() 모디파이어를 이용하면 컴포저블을 특정한 형태로 렌더링되도록 할 수 있다. Box에만 지정할 수 있는 것은 아니지만, 아마도 Box 컴포넌트가 형태를 자르는 것을 보여주기에 가장 좋은 예시일 것이다. 컴포저블의 형태를 정의할 때는 clip() 모디파이어를 호출하고 Shape 값을 전달한다. Shape의 값에는 RectangleShape, CircleShape(원), RoundedCornerShape(모서리 둥글게), CutCornerShape(잘려나간 모서리)를 이용할 수 있다.
🍎 커스텀 레이아웃 모디파이어로 커스텀 레이아웃 개발
- 커스텀 레이아웃은 매우 직고나적으로 구현할 수 있으며 두 가지로 분류할 수 있다. 가장 기본적인 형태의 커스텀 레이아웃은 레이아웃 모디파이어로 구현할 수 있으며, 단일 사용자 인터페이스 요소에 적용할 수 있다. 새로운 레이아웃 컴포저블은 해당 컴포저블의 모든 자식에게 적용될 수도 있다.
- LayoutModifier 프로젝트
- 구현 코드
@Composable
fun MainCustomScreen() {
Box(contentAlignment = Alignment.Center,
modifier = Modifier.size(120.dp, 80.dp)) {
Column {
ColorBox(
Modifier.exampleLayout(0f).background(Color.Blue)
)
ColorBox(
Modifier.exampleLayout(0.25f).background(Color.Green)
)
ColorBox(
Modifier.exampleLayout(0.5f).background(Color.Yellow)
)
ColorBox(
Modifier.exampleLayout(0.25f).background(Color.Red)
)
ColorBox(
Modifier.exampleLayout(0.0f).background(Color.Magenta)
)
}
}
}
fun Modifier.exampleLayout(
fraction: Float
) = layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val x = -(placeable.width * fraction).roundToInt()
layout(placeable.width, placeable.height) {
placeable.placeRelative(x = x, y = 0)
}
}
@Composable
fun ColorBox(modifier: Modifier) {
Box(Modifier.padding(1.dp).size(width = 50.dp, height = 10.dp).then(modifier))
}
- 본래 Box 컴포넌트는 자식들을 스택으로 쌓아 올리기 위한 것이지만, 빈 Box는 화면에 사각형을 그리는 간단하고 효과적인 방법이기도 하다.
- 모디파이어를 ColorBox에 적용하면 부모 Box 안에서 새로운 위치로 이동시킬 수 있다.
- 표준 구문은 위와 같고, layout의 후행 람다는 measurable, constraints라는 2개의 파라미터를 각각 전달한다. measurable 파라미터는 해당 모디파이어가 호출된 자식 요소가 배치될 정보이며, constraints 파라미터는 자식이 이용할 수 있는 최소/최대 폭과 높이를 포함한다.
- 지금까지 만든 예시에서 기본 위치는 Box 콘텐츠 영역의 왼쪽 위 모서리이며, x와 y 좌표로 나타내면 0, 0에 해당한다. 한편, Row레이아웃의 두번째 자식은 부모의 컨텍스트 안에서 완전히 다른 기본 x, y 좌표에 위치할 것이다.
- 레이아웃 모디파이어는 부모 컨텍스트 안에서의 자식의 기본 위치에 신경 쓰지 않는다. 대신 '기본 위치를 기준으로' 자식의 위치를 계산하는 데 집중한다. 다시 말해, 모디파이어는 0, 0을 기준으로 새로운 위치를 계산한 뒤 새로운 오프셋으로 반환한다. 부모는 이후 오프셋을 실제 좌표에 적용해 자식을 임의의 위치로 옮긴다.
- 모디파이어를 이용해 자식을 배치할 때는 람다에 전달된 제약 조건의 준수 여부를 확인하기 위해 자식의 측정값을 알아야 한다. 이 측정값들은 measurable 인스턴스의 measure() 메서드를 호출해 얻을 수 있으며 제약 객체를 통해 전달된다. 호출한 결과로 하나의 placeable 인스턴스가 반환되며, 이 인스턴스는 높이와 폭 값을 갖는다. 그리고 이 placeable 인스턴스의 메서드를 호출해 그 부모 콘텐츠 영역 안에 있는 요소의 새로운 위치를 지정할 수 있다.
- 커스텀 레이아웃을 개발할 때는 모디파이어가 호출될 때마다 자식을 측정하는 규칙이 적용되고, 이를 싱글 패스 측정이라 부르며, 사용자 인터페이스 트리 계층을 신속하고 효율적으로 렌더링하기 위해 꼭 필요하다.
- 커스텀 모디파이어를 만들었으면 이를 자식 컴포저블, 다시 말해 ColorBox 컴포넌트에 적용할 수 있다.
- 가상 정렬 선에 따라 위치를 설정할 수 있는데, 이는 레이아웃 모디파이어에 파라미터로 전달해서 설정할 수 있다.
- val x = -(placeable.width * fraction).roundToInt() - placeable 객체로부터 자식의 폭을 받아서 fraction 파라미터값을 곱한다. 결과는 부동소수점 수가 되므로, 정숫값으로 반올림해서 placeRelative() 호출 시 좌푯값으로 이용한다. 마지막으로 정렬 선을 오른쪽으로 옮기는 것은 자식을 왼쪽으로 옮기는 것과 같으므로 x 값을 음수로 바꾼다. 이후 자식은 새로운 좌표에 위치하게 된다. 수직 위치는 변경되지 않았으므로 y 값은 0으로 설정된다. Column 레이아웃의 자식에 이 모디파이어를 적용해 보면 그 효과를 명확히 알 수 있다.
- FirstBaseline과 LastBaseline 정렬 선은 Text 컴포넌트 안에 포함된 텍스트 콘텐츠의 첫 번째 행과 마지막 행의 바닥선에 해당한다.
🍎 커스텀 레이아웃 구현하기
- 컴포즈가 제공하는 커스텀 레이아웃을 이용하면 직접 레이아웃 컴포넌트를 디자인하고 자식 요소의 크기와 위치를 자유롭게 제어할 수 있다.
- 커스텀 레이아웃 역시 커스텀 콘텐츠 모디파이어와 몇 가지 유사한 특징을 갖는데, 커스텀 레이아웃을 이용하면 여러 자식에 하나의 커스텀 레이아웃을 적용할 수 있다.
- 커스텀 레이아웃은 컴포즈의 Layout 컴포저블 함수를 이용해 선언하는데, 이 함수는 여러 자식을 측정하고 위치를 지정하는 목적으로만 이용한다.
- 대부분의 커스텀 레이아웃 선언은 값은 표준 구조로 시작한다.
- 커스텀 레이아웃을 만든 뒤에는 표준 컴포즈 레이아웃과 같은 방식으로 호출할 수 있다.
- 커스텀 레이아웃 프로젝트 구현 코드
// Cascade Layout
@Composable
fun MainCascadeScreen() {
Box {
CascadeLayout(spacing = 20) {
Box(modifier = Modifier.size(60.dp).background(Color.Blue))
Box(modifier = Modifier.size(80.dp, 40.dp).background(Color.Red))
Box(modifier = Modifier.size(90.dp, 100.dp).background(Color.Cyan))
Box(modifier = Modifier.size(50.dp).background(Color.Magenta))
Box(modifier = Modifier.size(70.dp).background(Color.Green))
}
}
}
// Cascade Layout
@Composable
fun CascadeLayout(
modifier: Modifier = Modifier,
spacing: Int = 0,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
var indent = 0
layout(constraints.maxWidth, constraints.maxHeight) {
var yCoord = 0
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
placeables.forEach { placeable ->
placeable.placeRelative(x = indent, y = yCoord)
indent += placeable.width + spacing
yCoord += placeable.height + spacing
}
}
}
}
- 이 코드는 자식들을 하나의 열 안에 배치한다. 각 자식은 이전 자식의 폭값을 이용해 식별한다. 선택 파라미터를 이용해 각 자식 요소 사이의 간격을 지정한다.
- spacing 파라미터를 추가하기 위해 기본값을 0으로 설정하고, 특정 자식을 식별하기 위한 indent 값은 자식이 열에 추가할 때마다 증가하므로, 가장 최근의 indent를 추적하기 위한 변수도 추가한다. 그리고 한 자식을 바로 이전 자식의 아래 표시하도록 하기 위해 y 좌푯값도 유지해야 한다.
- 마지막으로, forEach 루프 안에 코드를 넣어서 각 자식의 위치를 계산한다.
🍎 ConstraintLayout
- ConstraintLayout은 안드로이드 7 SDK에서 도입되었다. 반응형 사용자 인터페이스 레이아웃을 쉽게 만들기 위해 디자인되었으며, 간단하고 인상적이며 유연한 레이아웃 시스템을 제공한다. 또한, 다양한 크기의 화면 및 기기 회전으로 인해 발생하는 변경에 자동으로 반응해야 하는 사용자 인터페이스 레이아웃을 개발하는 데 특화되어 있다.
- 모든 레이아웃과 마찬가지로 ConstraintLayout 또한 자식 컴포넌트들의 위치 및 크기 동작을 관리한다. ContraintLayout은 각 자식에 설정된 제약 커넥션 기반으로 이를 수행한다.
- 제약, 체인, 마진, 체인 스타일, 반대 제약, 가이드라인, 제약 편향, 배리어 개념을 숙지하고 있어야 한다.
- 제약 - 본질적으로 일련의 규칙들이다. 이 규칙들은 한 컴포저블의 정렬과 위치를 조정함에 있어 다른 컴포저블들, ConstraintLayout 부모를 포함한 관게, 그리고 가이드라이과 배리어라 불리는 특별한 요소들을 기준으로 상대적으로 지정한다. 제약은 액티비티의 사용자 인터페이스 레이아웃이 기기 방향의 변경이나 다른 화면 크기의 기기에 표시될 때 반응하는 방법을 지정한다.
- 마진 - 고정된 거리를 지정하는 제약의 한 형태다.
- 반대 제약 - 동일한 축을 따라 한 컴포저블이 가진 2개의 제약을 반대 제약이라 한다. 즉, 한 컴포넌트가 왼쪽과 오른쪽 가장자리에 모두 제약을 갖고 있을 때 수평 반대 제약을 가진 것으로 간주한다.
- 제약 편향 - 기본적으로 반대 제약이 동일하면 해당 위젯은 축을 따라 중앙에 배치된다. 반대 제약 상태에서 컴포넌트의 위치 조정을 허용하기 위해서는 ConstraintLayout에서 제약 편향이라 불리는 피처를 구현해야 한다. 제약 편향을 이용하면 축을 따라 컴포저블의 위치를 지정함으로써 하나의 제약 조건에 대해 지정된 백분율만큼 치우치도록 할 수 있다.
- 체인 - 하나의 그룹으로 정의된 2개 이상의 컴포저블을 포함하는 레이아웃의 동작 방법을 제공한다. 체인은 수직축 및 수평축 기준으로 선언할 수 있으며, 체인 안에 있는 컴포넌트들의 간격과 크기를 정의한다. 컴포즈에서는 쉽게 체인을 만들 수 있도록 헬퍼를 제공한다. 그러나 내부적으로는 컴포저블이 양방향 제약으로 연결되어 있을 때 체인이 된다는 점에 주목해야 한다. 첫 번째 요소는 체인 헤드라고 불림.
- 체인 스타일 - 체인의 레이아웃 동작은 체인 헤드 컴포저블에 적용된 체인 스타일 설정에 따라 정의된다. Spread Chain(체인에 포함된 컴포저블들이 이용할 수 있는 공간에 공평하게 분배됨.), Spread Inside Chain(체인 헤드와 체인의 마지막 위젯 사이에서 공평하게 분배.), Weighted Chain(차지하는 공간이 weighting 프로퍼티를 이용해 정의됨.), Packed Chain(사이에 여유 공간 없이 위치한다. 편향을 이용해 부모 컨테이너 안에서 체인의 수직 또는 수평 위치를 제어할 수 있음.)
- 컴포저블 크기 제어는 사용자 인터페이스 디자인 프로세스의 핵심 요소다. 이 레이아웃은 5개의 옵션을 제공한다.
- Dimension.preferredWrapContent - 크기가 이전 제약에 따라 해당 컴포저블이 포함하는 콘텐츠에 따라 결정.
- wrapContent - 이전 제약 관계없이 해당 컴포저블이 포함하는 콘텐츠에 따라 결정.
- fillToConstraints - 이전 제약에 따라 할당된 공간을 가득 채움.
- preferredValue - 컴포저블의 크기는 이전 제약에 따라 정의된 크기로 고정됨.
- value - 이전 제약과 관계없이 지정된 크기로 고정됨.
- 가이드라인 헬퍼는 추가적으로 연결될 수 있는 제약을 제공한다.
- 배리어는 가상의 뷰로 컴포저블들을 레이아웃 안에 표시되도록 제한할 때 이용되며 가이드라인과 유사하다. 가이드라인과 마찬가지로 하나의 배리어는 수직 또는 수평으로 배치할 수 있고, 하나 이상의 컴포저블을 이에 맞춰 제약할 수 있다. 배리어의 위치는 레퍼런스 컴포넌트로 불리는 요소들에 의해 정의된다. 배리어는 컴포넌트들의 겹침을 포함해 빈번히 일어나는 이슈를 해결하기 위해 도입됐다.
🍎 ConstraintLayout 다루기
- ConstraintLayout은 컴포저블 및 컴포즈의 다른 레이아웃들과 동일한 형태로 제공되며, 다른 레이아웃 컴포저블과 마찬가지로, 하나의 Modifier 파라미터를 호출할 수 있다.
- 제약이 존재하지 않으면 ConstraintLayout의 컴포저블 자식은 콘텐츠 영역의 왼쪽 위 모서리에 배치된다. 제약을 받을 컴포저블은 제약을 적용하기 전에 참조를 할당해야 한다. 이를 위해서는 참조를 만들고, 만든 참조를 제약 적용 이전 컴포넌트에 할당한다. createRef() 함수를 호출해서 하나의 참조를 생성하고 그 결과를 상수에 할당할 수 있다.
- 또는 createRefs()를 호출해 한 번에 여러 참조를 생성할 수 있다.
- 참조를 만든 뒤에는 constrainAs() 모디파이어 함수를 이용해 참조를 개별 컴포저블에 적용할 수 있다.
- constrainAs() 모디파이어는 후행 람다를 가지며, 이 람다에 제약들이 추가된다.
- 가장 일반적인 형태의 제약은 컴포저블의 한쪽과 부모 ConstraintLayout 또는 다른 컴포저블의 한쪽 사이에 존재한다. 이런 유형의 제약은 linkTo() 함수에 대한 호출을 통해 constrainAs()의 후행 람다 안에서 선언된다.
🍎 ConstraintLayout 프로젝트
- ConstraintLayout 지원은 별도의 라이브러리를 통해 제공되며, 이는 새로운 프로젝트에 기본으로 포함되지 않는다. ConstraintLayout을 이용해 작업하기 전에, 이 라이브러리를 프로젝트 필드 설정에 추가해야 한다. build.gradle 파일을 열고, 편집기에 파일이 로드되면 dependencies 섹션을 찾아 implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")을 작성해준다.
- 구현 코드
@Composable
fun MainConstScreen() {
ConstraintLayout(Modifier.size(width = 350.dp, height = 220.dp)) {
val (button1, button2, button3) = createRefs()
val barrier = createEndBarrier(button1, button2)
MyButton(text = "Button1", Modifier.width(100.dp).constrainAs(button1) {
top.linkTo(parent.top, margin = 30.dp)
start.linkTo(parent.start, margin = 8.dp)
})
MyButton(text = "Button2", Modifier.width(150.dp).constrainAs(button2) {
top.linkTo(button1.bottom, margin = 20.dp)
start.linkTo(parent.start, margin = 8.dp)
})
MyButton(text = "Button3", Modifier.constrainAs(button3) {
linkTo(parent.top, parent.bottom,
topMargin = 8.dp, bottomMargin = 8.dp)
linkTo(button1.end, parent.end, startMargin = 30.dp,
endMargin = 8.dp)
start.linkTo(barrier, margin = 30.dp)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
})
}
}
private fun myConstraintSet(margin: Dp): ConstraintSet {
return ConstraintSet {
val button1 = createRefFor("button1")
constrain(button1) {
linkTo(parent.top, parent.bottom, topMargin = margin,
bottomMargin = margin)
linkTo(parent.start, parent.end, startMargin = margin,
endMargin = margin)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}
}
}
@Composable
fun MyButton(text: String, modifier: Modifier = Modifier) {
Button(
onClick = { },
modifier = modifier
) {
Text(text)
}
}
- 반대 제약은 컴포저블 양쪽 끝이 자리가 같은 축을 따라 제약되어 있을 때 만들어진다. 반대 제약은 ConstraintLayout 안의 컴포넌트를 수평으로 중앙 정렬하는 효과를 나타낸다. 반대 제약은 버튼과 부모 사이에 스프링 같은 연결선으로 표시된다.
- 반대 제약을 이용하는 목적이 부모 안에서 자식 컴포넌트를 중앙에 배치하기 위한 것뿐이라면 다음을 이용할 수 있다. centerVerticallyTo(parent), centerHorizontallyTo(parent).
- 컴포넌트들 사이에도 제약 적용 가능.
- 다른 설정이 없는 경우, 반대 제약은 제약을 받는 요소들의 한가운데에 컴포넌트를 위치시킨다. 편향을 적용함으로써 이용할 수 있는 공간에서 상대적인 위치에 제약을 받는 컴포저블을 이동시킬 수 있다.
- 제약 마진과 함께 사용해 컴포넌트와 다른 요소 사이에 고정 여백을 구현할 수 있다.
- ContraintLayout 마진은 제약 커넥션 끝에 나타나며, 편향을 조정하거나 사용자 인터페이스의 다른 곳에서 레이아웃을 변경하더라도 버튼을 이동할 수 없는 고정된 간격을 의미한다.
- 편향 설정을 하지 않더라도 마진은 컴포넌트의 위치에 영향을 미친다.
- 반대 제약, 마진, 편향은 ConstraintLayout을 이용해 안드로이드에서 반응형 레이아웃을 디자인하는 기반이다. 반대 제약 커넥션 없이 컴포저블에 제약을 적용하면, 이들은 근본적으로 마진 제약이 된다.
- 반대 제약을 구현하면 제약 커넥션이 스프링 같은 지그재그 선으로 표시된다.
- 반응형 및 적응형 사용자 인터페이스 레이아웃을 디자인할 때는 사용자 인터페이스 레이아웃을 수동으로 디자인하고, 자동으로생성된 제약을 수정해 편향과 반대 제약을 모두 고려하는 것이 중요하다.
- 체인 제약은 2개 이상의 컴포넌트에서 createHorizontalChain() 또는 createVerticalChain()을 호출하고, 컴포넌트 참조를 파라미터로 전달해 만든다.
🍎 IntrinsicSize 다루기
- 부모 컴포저블은 IntrinsicSize 열거형의 Min, Max 값에 접근함으로써 그 자식의 크기 정보를 얻을 수 있다. IntrinsicSize는 가장 넓은(큰) 자식이 가질 수 있는 최댓값, 최솟값에 관한 정보를 부모에게 제공한다. 부모는 이를 이용해 자식이 필요로 하는 크기에 기반해 크기에 관한 결정을 내릴 수 있다.
- IntrinsicSize를 이용하면, 이 컴포저블들은 그 자식들의 공간 요구에 맞춰 크기를 설정한다. 이후의 예시 프로젝트에서 확인하겠지만, 이는 하나 이상의 자식들의 크기가 동적으로 변경될 때 매우 유용하다.
- IntrinsiSize 열거형을 이용하면 최대/최소 측정값 모두에 접근할 수 있다. 이 두 값의 차이에 관해 좀 더 설명이 필요하다. 눈에 보이는 모든 컴포저블은 기기 화면에서 공간을 필요로 하며, 많은 컴포저블은 사용할 수 잇는 공간의 변화에 적응할 수 있다.
- IntrinsicSizeDeom 구현 코드
@Composable
fun MainIntrinsicScreen() {
var textState by remember { mutableStateOf("") }
val onTextChange = { text: String ->
textState = text
}
Column(Modifier.width(200.dp).padding(5.dp)) {
Column(Modifier.width(IntrinsicSize.Min)) {
Text(
modifier = Modifier
.padding(start = 4.dp),
text = textState
)
Box(Modifier.height(10.dp).fillMaxWidth().background(Color.Blue))
}
MyTextField(text = textState, onTextChange = onTextChange)
}
}
@Composable
fun MyTextField(text: String, onTextChange : (String) -> Unit) {
TextField(
value = text,
onValueChange = onTextChange
)
}