[Android Compose] CheckBox, Switch, RadioButton 사용법
사용자에게 선택할 수 있는 요소들을 제공할 때 가장 자주 사용되는 것들이 제목의 3가지 컴포넌트다.
이것들을 컴포즈에선 어떻게 사용할 수 있는지 확인해 본다. 먼저 data class를 만든다.
data class ToggleComponentInfo(
val isChecked: Boolean,
val text: String
)
액티비티의 전체 코드는 아래와 같다.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.composeprac.selection_components.ui.theme.ComposePracTheme
class SelectionComponentsTestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposePracTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
CheckBoxes()
Spacer(modifier = Modifier.height(32.dp))
Switches()
Spacer(modifier = Modifier.height(32.dp))
RadioButtons()
}
}
}
}
}
}
@Composable
private fun CheckBoxes() {
val checkBoxes = remember {
mutableStateListOf(
ToggleComponentInfo(
isChecked = false,
text = "Photos"
),
ToggleComponentInfo(
isChecked = false,
text = "Videos"
),
ToggleComponentInfo(
isChecked = false,
text = "Audio"
),
)
}
var triState by remember {
mutableStateOf(ToggleableState.Indeterminate)
}
val toggleTriState = {
triState = when (triState) {
ToggleableState.Indeterminate -> ToggleableState.On
ToggleableState.On -> ToggleableState.Off
else -> ToggleableState.On
}
checkBoxes.indices.forEach { index ->
checkBoxes[index] = checkBoxes[index].copy(
isChecked = (triState == ToggleableState.On)
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
toggleTriState()
}
.padding(end = 16.dp)
) {
TriStateCheckbox(
state = triState,
onClick = toggleTriState
)
Text("File types")
}
checkBoxes.forEachIndexed { index, info ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 32.dp)
.clickable {
checkBoxes[index] = info.copy(
isChecked = !info.isChecked
)
}
.padding(end = 16.dp)
) {
Checkbox(
checked = info.isChecked,
onCheckedChange = { isChecked ->
checkBoxes[index] = info.copy(
isChecked = isChecked
)
}
)
Text(text = info.text)
}
}
}
@Composable
private fun Switches() {
var switch by remember {
mutableStateOf(
ToggleComponentInfo(
isChecked = false,
text = "다크 모드"
)
)
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(switch.text)
Spacer(modifier = Modifier.weight(1f))
Switch(
checked = switch.isChecked,
onCheckedChange = { isChecked ->
switch = switch.copy(isChecked = isChecked)
},
thumbContent = {
Icon(
imageVector = if (switch.isChecked) {
Icons.Default.Check
} else {
Icons.Default.Close
},
contentDescription = null
)
}
)
}
}
@Composable
private fun RadioButtons() {
val radioButtons = remember {
mutableStateListOf(
ToggleComponentInfo(
isChecked = true,
text = "Photos"
),
ToggleComponentInfo(
isChecked = false,
text = "Videos"
),
ToggleComponentInfo(
isChecked = false,
text = "Audio"
),
)
}
radioButtons.forEachIndexed { index, info ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
radioButtons.replaceAll {
it.copy(
isChecked = (it.text == info.text)
)
}
}
.padding(end = 16.dp)
) {
RadioButton(
selected = info.isChecked,
onClick = {
radioButtons.replaceAll {
it.copy(
isChecked = (it.text == info.text)
)
}
}
)
Text(text = info.text)
}
}
}
적절히 복붙해서 앱을 실행하면 아래와 비슷한 화면이 나타날 것이다.
이제 하나씩 확인해 본다. 먼저 체크박스다.
@Composable
private fun CheckBoxes() {
val checkBoxes = remember {
mutableStateListOf(
ToggleComponentInfo(
isChecked = false,
text = "Photos"
),
ToggleComponentInfo(
isChecked = false,
text = "Videos"
),
ToggleComponentInfo(
isChecked = false,
text = "Audio"
),
)
}
var triState by remember {
mutableStateOf(ToggleableState.Indeterminate)
}
val toggleTriState = {
triState = when (triState) {
ToggleableState.Indeterminate -> ToggleableState.On
ToggleableState.On -> ToggleableState.Off
else -> ToggleableState.On
}
checkBoxes.indices.forEach { index ->
checkBoxes[index] = checkBoxes[index].copy(
isChecked = (triState == ToggleableState.On)
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
toggleTriState()
}
.padding(end = 16.dp)
) {
TriStateCheckbox(
state = triState,
onClick = toggleTriState
)
Text("File types")
}
checkBoxes.forEachIndexed { index, info ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 32.dp)
.clickable {
checkBoxes[index] = info.copy(
isChecked = !info.isChecked
)
}
.padding(end = 16.dp)
) {
Checkbox(
checked = info.isChecked,
onCheckedChange = { isChecked ->
checkBoxes[index] = info.copy(
isChecked = isChecked
)
}
)
Text(text = info.text)
}
}
}
먼저 checkBoxes라는 mutableStateList를 만들어 각 체크박스의 체크 상태, 이름을 초기화했다.
이후 var, val 변수를 각각 하나씩 만들었다.
var triState by remember {
mutableStateOf(ToggleableState.Indeterminate)
}
val toggleTriState = {
triState = when (triState) {
ToggleableState.Indeterminate -> ToggleableState.On
ToggleableState.On -> ToggleableState.Off
else -> ToggleableState.On
}
checkBoxes.indices.forEach { index ->
checkBoxes[index] = checkBoxes[index].copy(
isChecked = (triState == ToggleableState.On)
)
}
}
좀 생소한 클래스가 있는데 ToggleableState는 enum class로 아래와 같이 구현돼 있다.
/**
* Enum that represents possible toggleable states.
*/
enum class ToggleableState {
/**
* State that means a component is on
*/
On,
/**
* State that means a component is off
*/
Off,
/**
* State that means that on/off value of a component cannot be determined
*/
Indeterminate
}
/**
* Return corresponding ToggleableState based on a Boolean representation
*
* @param value whether the ToggleableState is on or off
*/
fun ToggleableState(value: Boolean) = if (value) On else Off
File Types 체크박스는 다른 체크박스들과 다르게 흰색 가로선 하나가 그어져 있다. 이걸 클릭하면 모든 체크박스들이 체크된 상태로 변하며 다시 누르면 모든 체크박스들이 체크 해제된다. 이 과정에서 File types 체크박스도 다른 체크박스들의 상태를 따라 변한다.
이 때 흰색 가로선이 표시되고 있다는 건 모두 체크된 게 아닌 최소 1개는 체크되지 않은 상태일 경우를 의미한다. 최상위 체크박스인 File types의 온오프 상태를 결정할 수 없기 때문에 이렇게 표시해 두는 것이다.
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
toggleTriState()
}
.padding(end = 16.dp)
) {
TriStateCheckbox(
state = triState,
onClick = toggleTriState
)
Text("File types")
}
그 다음 Row를 만들어 체크박스와 텍스트를 가로 일렬로 표시한다.
checkBoxes.forEachIndexed { index, info ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 32.dp)
.clickable {
checkBoxes[index] = info.copy(
isChecked = !info.isChecked
)
}
.padding(end = 16.dp)
) {
Checkbox(
checked = info.isChecked,
onCheckedChange = { isChecked ->
checkBoxes[index] = info.copy(
isChecked = isChecked
)
}
)
Text(text = info.text)
}
}
그리고 checkBoxes 리스트를 순회하면서 안의 요소를 만날 때마다 가로로 체크박스와 텍스트를 배치한다.
이 때 글자를 클릭하면 체크박스의 온오프 상태를 바꿀 수 있도록 Row의 Modifier로 clickable 람다를 추가했다. 또한 체크박스를 선택하면 체크박스, 텍스트를 감싸는 Row 영역에만 리플 효과가 나타나도록 clickable 람다를 기준으로 위아래에 padding을 추가했다. 각 padding이 없으면 어떻게 작동하는지 직접 확인해 본다.
다음은 switch다.
@Composable
private fun Switches() {
var switch by remember {
mutableStateOf(
ToggleComponentInfo(
isChecked = false,
text = "다크 모드"
)
)
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(switch.text)
Spacer(modifier = Modifier.weight(1f))
Switch(
checked = switch.isChecked,
onCheckedChange = { isChecked ->
switch = switch.copy(isChecked = isChecked)
},
thumbContent = {
Icon(
imageVector = if (switch.isChecked) {
Icons.Default.Check
} else {
Icons.Default.Close
},
contentDescription = null
)
}
)
}
}
switch는 하나만 선언했기 때문에 화면에서도 하나만 표시되고, 이후 코드는 보면 무슨 코드인지 알 것이다.
thumbContent는 스위치 안의 원 안에 표시되는 아이콘으로, 온오프일 때 각각 어떤 아이콘을 표시할지 정하는 옵션이다. 여기선 off일 때 X, on일 때 체크 모양이 표시되도록 했다.
마지막으로 라디오 버튼이다.
@Composable
private fun RadioButtons() {
val radioButtons = remember {
mutableStateListOf(
ToggleComponentInfo(
isChecked = true,
text = "Photos"
),
ToggleComponentInfo(
isChecked = false,
text = "Videos"
),
ToggleComponentInfo(
isChecked = false,
text = "Audio"
),
)
}
radioButtons.forEachIndexed { index, info ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
radioButtons.replaceAll {
it.copy(
isChecked = (it.text == info.text)
)
}
}
.padding(end = 16.dp)
) {
RadioButton(
selected = info.isChecked,
onClick = {
radioButtons.replaceAll {
it.copy(
isChecked = (it.text == info.text)
)
}
}
)
Text(text = info.text)
}
}
}
라디오버튼은 체크박스와 큰 틀은 같고 세부적인 구현 내용만 다르다. 때문에 그리 어려운 코드도 없기 때문에 넘어간다.