커스텀 가능한 버튼 리스트를 만드는 게 목적이다. 여기서 말하는 버튼 리스트는:
그런 버튼들로 이루어진 리스트이다.
유사한 작동 방식을 가진 컴포넌트로 탭(Tab)이 있겠다. 여러 가지 선택지 중 딱 하나만 골라야 할 경우에 활용할 수 있는 그런 버튼 리스트이다.
버튼 리스트 구현에 필요한 구성 요소를 구현 순서에 따라 정렬했다. 아래를 참고하자:
data class ButtonItem
@Composable Button
@Composable ButtonPage
data class ButtonItem
버튼 구현에 필요한 데이터를 이 클래스에 정의해주는 것이 구현의 시작이다. 이 게시글에서는 최소한의 변수로 클래스를 정의했지만, 나중에 필요에 따라 더할 건 더하고 뺄 건 빼도 되겠다.
data class ButtonItem(
val index: Int,
val label: String,
val icon: ImageVector
)
클래스 구성에 대한 설명이다:
index
는 버튼의 인덱스이다. 특정 버튼의 인덱스가 4일 경우, 그 버튼을 클릭하면 state
에 걸린 인덱스가 4로 바뀌면서 그 버튼이 활성화되는 동작 방식을 갖는다.label
은 버튼의 항목명이다.icon
은 버튼의 아이콘이다. 이 게시글에서는 Google에서 제공하는 자체 아이콘을 쓰기 때문에 ImageVector
형태로 선언했다. 만약 사용자가 직접 벡터 이미지를 불러와 아이콘으로 쓸 예정이라면, ImageVector
를 Painter
로 바꾸어도 될 듯하다.@Composable Button
data class ButtonItem
를 정의했으니, 이제 버튼을 구현할 차례다. 버튼에는 아래와 같이 최소 3가지 변수가 필요하다:
fun Button(
item: ButtonItem,
isSelected: Boolean,
onTap: () -> Unit
)
각 변수에 대한 설명이다:
item
: 방금 전 선언한 ButtonItem
클래스이다. 버튼에 보여질 내용과 인덱스를 이 변수를 통해 전달한다.isSelected
: 버튼이 선택되었는지, 그렇지 않은지를 전달하는 변수이다.onTap
: 버튼을 클릭했을 때 동작할 함수를 전달하는 변수이다.이상의 변수들을 바탕으로 버튼이 동작하는 로직을 설명해보자면 아래와 같다:
isSelected
에 전달된 값을 확인하고, true
일 경우 버튼의 색상을 활성화한다. 그렇지 않을 경우 버튼의 색상을 비활성화한다.다음은 버튼의 세부 구현이다:
fun Button(
item: ButtonItem,
isSelected: Boolean,
onTap: () -> Unit
) {
val backgroundColor =
if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant
val contentColor =
if (isSelected) MaterialTheme.colorScheme.onPrimary
else MaterialTheme.colorScheme.onSurfaceVariant
// ...이후 설명
}
버튼 구현 전에 버튼의 색상을 함수 내 변수로 선언해주어야 한다. 특히 지금 구현중인 버튼은 클릭 여부에 따라 색상이 바뀌므로 이를 반영해주어야 한다. 거기에 필요한 게 바로 위의 2개 변수이다.
backgroundColor
는 버튼의 배경색이고, contentColor
는 버튼의 텍스트, 아이콘 등 주요 UI 요소의 색상이다. 여기에 조건문을 걸어, 버튼의 선택 여부에 따라 색상이 달라지도록 코드를 짜면 된다.
@Composable
fun Button(
item: ButtonItem,
isSelected: Boolean,
onTap: () -> Unit
) {
val backgroundColor =
if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant
val contentColor =
if (isSelected) MaterialTheme.colorScheme.onPrimary
else MaterialTheme.colorScheme.onSurfaceVariant
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.size(150.dp, 50.dp)
.clip(RoundedCornerShape(40.dp))
.background(color = backgroundColor)
.padding(10.dp)
.clickable { onTap() }
) {
Icon(
imageVector = item.icon,
contentDescription = null,
tint = contentColor
)
Text(
text = item.label,
color = contentColor
)
}
}
버튼의 전체 소스 코드이다. 나는 Row
를 활용했지만, 사실 Row
에 해당하는 코드는 버튼의 겉모습에 해당하기 때문에 온전히 사용자의 몫이다. 따라서 버튼의 사용 목적과 계획 등에 따라 디자인하면 된다. Row
말고 Column
이나 Box
나 뭐든 괜찮다. 커스텀이 가능하다고 한 이유가 바로 이 부분 때문이다.
어쨌든 여기서 제일 중요한 건, 버튼의 배경 색상과 버튼 내용물의 색상에 위에서 선언한 backgroundColor
와 contentColor
를 적용해야 한다는 점이다.
나의 경우는, 아이콘과 텍스트를 감싸는 Row
에 Modifier
를 걸어 배경 색상을 backgroundColor
로 지정했고, 아이콘과 텍스트에는 contentColor
를 적용했다.
@Composable ButtonPage
@Composable Button
을 구현했으니, 이 버튼들이 탑재될 페이지를 간단하게 작업해보자.
@Composable
fun ButtonPage() {
val buttonItemList = listOf(
ButtonItem(0, "홈", Icons.Rounded.Home),
ButtonItem(1, "연락처", Icons.Rounded.Person),
ButtonItem(2, "설정", Icons.Rounded.Settings)
)
var selectedIndex by rememberSaveable { mutableStateOf(0) }
// ...이후 설명
}
ButtonPage
의 본격적인 구현 전에, 아래의 변수 2개를 선언해주어야 한다:
buttonItemList
selectedIndex
rememberSaveable
과 mutableStateOf
를 활용해 선언했다. 위에서 말했던 'state 값'이 바로 이거다. 사용자가 인덱스 5인 버튼을 누르면 selectedIndex
가 5로 바뀌는 식.여담으로 런타임 동안 값이 바뀔 일이 없는 buttonItemList
는 val
로 선언했고, 버튼을 누름에 따라 값이 변경될 selectedIndex
는 var
로 선언했다. 참고하도록 하자.
이제 본격적인 구현이다. LazyColumn
을 활용한다. 물론 LazyRow
도 사용 가능.
@Composable
fun ButtonPage() {
val buttonItemList = listOf(// ...생략)
var selectedIndex by rememberSaveable { mutableStateOf(0) }
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
items(buttonItemList) { item ->
Button(
item = item,
isSelected = selectedIndex == item.index,
onTap = { selectedIndex = item.index }
)
}
}
}
코드를 한 줄씩 확인해보자.
items(buttonItemList) { item ->
// ...생략
}
LazyColumn
의 가장 보편적인 활용 방법이다. 리스트의 내용물을 item
이라는 이름으로 컨트롤하게 된다. 이 코드에서는 buttonItemList
가 List<ButtonItem>
이므로, item
의 자료형은 ButtonItem
이 된다.verticalArrangement = Arrangement.spacedBy(10.dp)
Button(
item = item,
isSelected = selectedIndex == item.index,
onTap = { selectedIndex = item.index }
)
item
항목에는 뭐 당연히... ButtonItem
항목이 들어간다.isSelected
에는 selectedIndex == item.index
라는 평가식을 활용해, 현재의 인덱스와 각 버튼의 인덱스를 비교할 수 있도록 한다. 저 식 자체가 Boolean
결과값을 내므로 그냥 통으로 입력한다.onTap
에는 버튼을 눌렀을 때 현재 인덱스를 버튼의 인덱스로 변경해주는 동작이 반드시 들어가야 한다. 그 외에 별도로 필요한 동작이 있다면 추가로 달아주면 되겠다.대충 잘 바뀌는 것 같다. 굳이다.