Compose로 선택 가능한 칩 그룹 만들기

임찬형·2022년 3월 30일
0

개인 프로젝트에서 사용하였던 선택 가능한 칩 그룹을 복습 차 정리합니다.

기본적으로 LazyRow를 이용하여 가로 스크롤 기능을 지원하며 chip을 클릭할 경우 아래처럼 해당 chip 색깔만 바뀌는 UI 요소입니다.

chip은 예시에는 4개로 구현하였으며, chip 목록을 배열에 넣어 관리하기 때문에 배열에 chip을 추가하거나 chip의 버튼을 눌러 해당 chip을 삭제하는 등 응용할 수 있습니다.
(추가로 chip을 추가하거나 삭제할 수 있도록 하고 싶은 경우 mutableStateListOf를 사용하여 chip을 관리하면 유용합니다)

먼저, 각각의 chip을 관리하기 위한 ChipState를 생성하였습니다

data class ChipState(
    var text: String,
    val isSelected: MutableState<Boolean>
)

이는 하나의 chip을 나타낼 것이며, isSelected를 MutableState로 지정한 것은 해당 chip의 선택 상태 여부를 가지고 있어야 하기 때문입니다.

다음으로 Chip 하나에 해당하는 UI인 Chip 함수입니다.

@Composable
private fun Chip(
    text: String,
    selected: Boolean,
    modifier: Modifier = Modifier,
    onChipClicked: (String, Boolean) -> Unit,
    onDeleteButtonClicked: (String, Boolean) -> Unit
) {
    Surface(
        color = when {
            selected -> colorResource(id = R.color.main_ingredient)
            else -> colorResource(id = R.color.sub_ingredient)
        },
        contentColor = Color.White,
        shape = RoundedCornerShape(16.dp),
        border = BorderStroke(
            width = 1.dp,
            color = when {
                selected -> colorResource(id = R.color.main_ingredient)
                else -> colorResource(id = R.color.sub_ingredient)
            }
        ),
        modifier = modifier
    ) {
        Row(modifier = Modifier) {
            Text(
                text = text,
                style = typography.body2,
                modifier = Modifier
                    .padding(8.dp)
                    .clickable { onChipClicked(text, selected) }
            )
            
            Image(
                imageVector = Icons.Default.Clear,
                contentDescription = null,
                modifier = Modifier
                    .padding(8.dp)
                    .size(20.dp)
                    .clickable { onDeleteButtonClicked(text, selected) },
                colorFilter = ColorFilter.tint(Color.White)
            )
        }
    }
}

Chip은 Chip 그룹에서만 사용할 함수이므로 private로 선언하였습니다.
매개변수에 대한 의미는 다음과 같습니다.
1. text는 chip에 쓰여질 텍스트입니다.
2. selected는 chip의 선택 여부로 색상을 결정합니다.
3. onChipClicked의 경우 chip을 클릭할 경우 호출되는 람다 함수입니다.
4. onDeleteButtonclicked는 삭제 버튼을 눌렀을 경우 호출되는 람다 함수입니다.

@Composable
fun Chips(
    modifier: Modifier = Modifier,
    elements: List<ChipState>,
    onChipClicked: (String, Boolean, Int) -> Unit,
    onDeleteButtonClicked: (String, Boolean, Int) -> Unit
) {
    LazyRow(modifier = modifier) {
        items(elements.size){ idx ->
            Chip(
                text = elements[idx].text,
                selected = elements[idx].isSelected.value,
                onChipClicked = { content, isSelected ->
                    onChipClicked(content,isSelected,idx)
                },
                onDeleteButtonClicked = { content, isSelected ->
                    onDeleteButtonClicked(content, isSelected, idx)
                }
            )
            Spacer(modifier = Modifier.padding(8.dp))
        }
    }
}

다음은 여러 개의 chip을 나타낼 Chips 함수입니다.
elements 매개변수는 chip 목록에 대한 리스트로, 해당 개수만큼의 chip이 있음을 뜻합니다.
따라서 LazyRow 컴포저블로 원하는 개수(elements.size)만큼의 chip을 items{}로 생성합니다.

여기서 onChipClicked 함수와 onDeleteButtonClicked의 매개변수 타입이 Chip과 다르다는 것을 보실 수 있습니다.

Chip에서의 onChipClicked는 chip 하나에 대한 클릭 처리이므로 어떤 chip인지 text와 선택 여부인 isSelected만 전달합니다.
하지만 Chips에서의 onChipClicked는 몇 번째 chip이 클릭된 것인지에 대한 인덱스 정보도 필요로 하므로 매개변수를 다르게 해 호출해 주었습니다.
같은 원리로 onDeleteButtonClicked 함수도 매개변수를 다르게 하였습니다.

이렇게 만든 Chips 컴포저블을 어떻게 적용하냐?

val elements by remember {
     mutableStateOf(
        listOf(
            ChipState("item1", mutableStateOf(false)),
            ChipState("item2", mutableStateOf(false)),
            ChipState("item3", mutableStateOf(false)),
            ChipState("item4", mutableStateOf(false))
        )
     )
 }

 Column(modifier = Modifier.padding(10.dp)) {
     Chips(
         modifier = Modifier.padding(8.dp),
         elements = elements,
         onChipClicked = { content, isSelected, idx ->
             // called when chip clicked
         	elements[idx].isSelected.value = !elements[idx].isSelected.value
 		 },
         onImageClicked = { content, isMain, idx ->
            // called when delete button clicked
         }
     )
}

Chip들을 관리할 상태 배열을 생성하여 Chips의 매개변수로 정보를 전달합니다.
chip 하나를 클릭한 경우 해당 chip의 isSelected 변수를 바꿔 주어야 하므로 onChipClicked에 위와 같은 코드가 들어갔습니다.

추가로 위 Chips를 응용하여 추가 / 제거가 가능한 Chip 목록을 만든 예시를 작성해 보겠습니다.

val elements = remember {
    mutableStateListOf(
        ChipState("item1", mutableStateOf(false)),
        ChipState("item2", mutableStateOf(false)),
        ChipState("item3", mutableStateOf(false)),
        ChipState("item4", mutableStateOf(false))
    )
}

var newChipText by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(10.dp)) {
    TextField(
        modifier = Modifier.fillMaxWidth(),
        value = newChipText,
        onValueChange = { newChipText = it },
        trailingIcon = {
            IconButton(
                onClick = {
                    elements.add(ChipState(newChipText, mutableStateOf(false)))
                    newChipText = ""
                }
            ) {
                Icon(imageVector = Icons.Default.Search, contentDescription = "")
            }
        }
    )

    Chips(
        modifier = Modifier.padding(8.dp),
        elements = elements,
        onChipClicked = { content, isSelected, idx ->
            elements[idx].isSelected.value = !elements[idx].isSelected.value
        },
        onDeleteButtonClicked = { content, isMain, idx ->
            elements.removeAt(idx)
        }
    )
}

chip의 추가 / 제거가 가능한 chip 목록이므로 mutableStateListOf를 통해 추가 / 제거할 경우 상태 변화가 인지되도록 하였습니다.
(mutableStateOf(mutableListOf())를 사용할 경우 list의 상태가 변화했음을 인지하지 못할 수 있기 때문에 의도대로 작동하지 않을 수 있음)

다음으로 chip 추가를 위한 TextField를 만들었으며, 아이콘을 클릭할 경우 TextField에 입력된 text를 담은 chip이 추가되도록 하였습니다.

또한 Chips의 X 아이콘을 누르면 호출되는 onDeleteButtonClicked 에서 elements의 idx번째 ChipState를 삭제하도록 하였습니다.

위 예시의 결과물은 다음과 같습니다.


초기 상태에서 item2의 X버튼을 눌러 삭제할 수 있으며


TextField에 test를 입력한 후 버튼을 클릭해 Chips에 추가할 수 있습니다.


또한 새로 생성한 chip에 대해서도 선택 상태를 조정할 수 있습니다.

0개의 댓글