[Android Compose] margin 설정 방법 정리
이 포스팅에선 컴포즈에서 마진을 설정하는 방법들을 정리한다. 앱을 만들면 필수적으로 마진, 패딩을 입혀야 하는 경우가 있는데 패딩은 Modifier의 padding()을 사용하면 되지만 마진은 다양한 방법들이 있어서 정리하려고 한다.
Modifier.padding()
정확히는 요소 주변에 공간을 추가해서 마진처럼 보이게 하는 방법이다. 내부 컴포넌트와 부모 레이아웃 사이에 공간을 추가한다.
@Composable
fun Test() {
Scaffold(
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.background(Color.Yellow),
) {
Text(
text = "안녕하세요",
modifier = Modifier
.padding(16.dp)
.background(Color.Blue),
)
}
}
}
개별적으로 설정할 수도 있다. 아래는 위 코드를 수정해서 start, end에는 8dp씩, top, bottom에 16dp씩 패딩을 먹이는 예시다.
@Composable
fun Test() {
Scaffold(
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.background(Color.Yellow),
) {
Text(
text = "안녕하세요",
modifier = Modifier
.padding(
start = 8.dp,
top = 16.dp,
end = 8.dp,
bottom = 16.dp
)
.background(Color.Blue),
)
}
}
}
Modifier.offset()
이 Modifier는 요소를 지정된 거리만큼 이동시킨다. 수동으로 이동시키는 방식이기 때문에 잘못 사용하면 생각했던 결과물과 많이 다를 수 있다.
@Composable
fun Test() {
Scaffold(
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.background(Color.Yellow),
) {
Text(
text = "안녕하세요",
modifier = Modifier
.offset(
x = 8.dp,
y = 16.dp
)
.background(Color.Blue),
)
}
}
}
Spacer()
요소 사이에 지정된 크기의 빈 공간을 추가한다.
@Composable
fun Test() {
Scaffold(
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.background(Color.Yellow),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "안녕하세요",
modifier = Modifier
.background(Color.Blue),
color = Color.White,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "식사는 하셨나요",
modifier = Modifier
.background(Color.Blue),
color = Color.White,
)
}
}
}
Column, Row에서 간단하게 마진 효과를 넣고 싶을 때 쓸 수 있는 방법이다. 위 코드에선 height를 넣었지만 width로 바꾸거나 둘 다 쓸 수도 있다.
Modifier.wrapContentSize()
부모 레이아웃 안에서 요소를 특정 위치 or 중앙에 정렬하면서 여백을 남겨야 할 때 사용할 수 있다.
아래는 Box를 중앙 정렬하면서 여백이 생성되는 걸 확인하기 위한 예시 코드다.
@Composable
fun Test() {
Scaffold(
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.background(Color.Yellow),
) {
Box(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
.background(Color.Red) // Box 배경색
.padding(32.dp)
) {
Text(
text = "안녕하세요",
color = Color.White,
modifier = Modifier
.padding(16.dp)
.background(Color.Cyan) // Text 배경색
)
}
}
}
}
Arrangement, Alignment의 spacedBy()
아래는 Column에서 verticalArrangement를 쓸 때 spacedBy()를 사용하는 예시다.
@Composable
fun Test() {
Scaffold(
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.background(Color.Yellow),
verticalArrangement = Arrangement.spacedBy(8.dp), // 각 항목 사이에 8dp씩 간격 추가
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "안녕하세요",
modifier = Modifier.background(Color.Red),
color = Color.White,
)
Text(
text = "안녕하세요2",
modifier = Modifier.background(Color.Red),
color = Color.White,
)
}
}
}
spacedBy() 사이의 숫자를 조절하면 두 Text 사이의 간격이 늘어나고 줄어드는 걸 볼 수 있다.
커스텀 레이아웃
까짓 거 한번 해보죠 마인드로 레이아웃을 한땀한땀 만드는 방식이다. 복잡한 마진 요구사항이 있을 때 쓸 수 있는 최후의 방식이다.
처음부터 직접 만드는 만큼 마진은 완벽하게 제어할 수 있다. 그러나 직접 만드는 만큼 구현이 복잡해질 수 있다.
@Composable
fun HorizontalSpacedLayout(
modifier: Modifier = Modifier,
spacing: Dp = 8.dp,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 모든 자식 요소 측정
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 레이아웃 전체 너비 계산 (모든 자식의 너비 + 간격)
val totalWidth = placeables.sumOf { it.width } + (placeables.size - 1) * spacing.roundToPx()
// 가장 높은 자식의 높이를 레이아웃 높이로 사용
val height = placeables.maxOfOrNull { it.height } ?: 0
// 레이아웃 배치
layout(totalWidth, height) {
var xPosition = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = xPosition, y = 0)
xPosition += placeable.width + spacing.roundToPx()
}
}
}
}
@Preview
@Composable
private fun TestPreview() {
TestTheme {
HorizontalSpacedLayout(spacing = 16.dp) {
Box(modifier = Modifier.size(50.dp).background(Color.Red))
Box(modifier = Modifier.size(75.dp, 100.dp).background(Color.Blue))
Box(modifier = Modifier.size(100.dp).background(Color.Green))
}
}
}
spacing 파라미터를 통해 요소 사이의 간격을 지정할 수 있고, 레이아웃 너비는 모든 자식 요소의 너비 + 간격 합으로 계산된다. 높이는 가장 높은 자식 요소의 높이로 설정된다.
위 함수는 measurables.map{}을 써서 모든 자식 요소들을 측정하고 전체 너비, 높이를 계산해서 layout {}에서 실제로 요소를 배치한다. 이 때 forEach를 써서 모든 요소를 순회하며 placeRelative()를 써서 레이아웃에 위치시키는 함수다. 이런 식으로 만들 수 있다는 예시 코드일 뿐이니 그런가보다 하고 넘기면 된다.
ConstraintLayout
레거시 뷰 시스템에서처럼 직접 마진을 제어하기 위한 방법이다. 아래는 컴포즈에서 ConstraintLayout을 사용하는 예시다. 의존성이 필요하기 때문에 앱 gradle에 아래 의존성을 추가한다. 복붙 후 Alt + Enter를 누르면 안드로이드 스튜디오에서 자동으로 버전 카탈로그로 바꿔준다.
implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
그리고 아래 코드를 확인한다.
@Composable
fun Test() {
ConstraintLayout(
modifier = Modifier
.fillMaxSize()
.background(Color.Yellow)
) {
val (content) = createRefs()
Box(
modifier = Modifier.constrainAs(content) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(parent.start, margin = 16.dp)
}
) {
Text(
modifier = Modifier.background(Color.Blue),
text = "안녕하세요",
color = Color.White
)
}
}
}
레거시 뷰를 사용할 때 ConstraintLayout을 자주 사용했다면 그렇게 어렵지 않다. 사용법만 컴포즈에서 조금 특이해졌을 뿐이고 로직은 XML에서 startToEnd 등으로 요소를 부모 레이아웃에 연결하는 것과 같다.