Jetpack Compose | 控件篇(三) -- Switch、CheckBox、RadioButton

写在最前

在之前的文章中,我们学习过Compose 中的 ImageButton

本篇我们将继续学习 SwitchCheckBoxRadioButton , 这三个控件在 人机交互界面 中也是由来已久。

文中代码均基于 1.0.1版本

如无特殊说明,文中的 Compose 均指代 Jetpack compose

文中代码均可在 WorkShop 中获取,本篇代码集中于 post28 包下

完整系列目录: Github Pages | 掘金 | csdn

Switch

当功能含义如同 开关 一般时,我们可以使用该控件,例如:"深色模式"、"飞行模式"

使用方式

fun Switch(
    checked: Boolean,
    onCheckedChange: ((Boolean) -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: SwitchColors = SwitchDefaults.colors()
)

经过前面的学习,这些参数的含义应该不陌生了。

修改配色可以通过如下API构建 SwitchColors 对象实例

object SwitchDefaults {
    fun colors(
        checkedThumbColor: Color = MaterialTheme.colors.secondaryVariant,
        checkedTrackColor: Color = checkedThumbColor,
        checkedTrackAlpha: Float = 0.54f,
        uncheckedThumbColor: Color = MaterialTheme.colors.surface,
        uncheckedTrackColor: Color = MaterialTheme.colors.onSurface,
        uncheckedTrackAlpha: Float = 0.38f,
        disabledCheckedThumbColor: Color = checkedThumbColor
            .copy(alpha = ContentAlpha.disabled)
            .compositeOver(MaterialTheme.colors.surface),
        disabledCheckedTrackColor: Color = checkedTrackColor
            .copy(alpha = ContentAlpha.disabled)
            .compositeOver(MaterialTheme.colors.surface),
        disabledUncheckedThumbColor: Color = uncheckedThumbColor
            .copy(alpha = ContentAlpha.disabled)
            .compositeOver(MaterialTheme.colors.surface),
        disabledUncheckedTrackColor: Color = uncheckedTrackColor
            .copy(alpha = ContentAlpha.disabled)
            .compositeOver(MaterialTheme.colors.surface)
    ): SwitchColors
}

结合前文学习的知识,我们不难推测,下述的代码并不能真正起到 开关 的功能:

Switch(checked = false, onCheckedChange = {
    Log.d("tag", "onCheckChanged:$it")
})

实际效果可体验WorkShop

我们需要构建一个 "被观测" 的数据源 [注意:这个说法虽然比较形象但并不准确] ,它被 Compose构建的树的节点 所观测,即 该Switch,当其内容发生变化 后,可反映到UI上。

例如:

var checked by rememberSaveable { mutableStateOf(false) }

Switch(checked = checked, onCheckedChange = {
    checked = it
    Log.d("tag", "onCheckChanged:$it")
})

或者:

val checked = remember { mutableStateOf(false) }

Switch(checked = checked.value, onCheckedChange = {
    checked.value = it
    Log.d("tag", "onCheckChanged:$it")
})

当然,按照Compose的用法,这些函数必须要同时存在于 @Compose 的函数块中,这样才能被目标节点所 "观测"

效果将在文末给出

CheckBox

当形式为:选中/不选中 时,可以使用该控件。

使用方式

fun Checkbox(
    checked: Boolean,
    onCheckedChange: ((Boolean) -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: CheckboxColors = CheckboxDefaults.colors()
)

Switch 非常相似,同样的,我们可以修改配色:

object CheckboxDefaults {
   
    @Composable
    fun colors(
        checkedColor: Color = MaterialTheme.colors.secondary,
        uncheckedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
        checkmarkColor: Color = MaterialTheme.colors.surface,
        disabledColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
        disabledIndeterminateColor: Color = checkedColor.copy(alpha = ContentAlpha.disabled)
    ): CheckboxColors
}

至此,我们意识到,Compose 固定了 Switch , Checkbox 的样式风格,如果定制图标,可以使用 ImageButton

如下代码将得到一个最简单可用的 CheckBox

var checked by rememberSaveable { mutableStateOf(false) }

Checkbox(checked = checked, onCheckedChange = {
    checked = it
    Log.d("tag", "onCheckChanged:$it")
})

不同于Android原生控件,Compose 中的 Checkbox 无法添加文字 -- 这很 Compose!.

Compose 贯穿始终的一个原则就是 组合 , 显然:文字部分应该使用 Text。

RadioButton

Compose 中并没有 RadioButtonGroup 的内容。 也有可能是我没有找到

但是,这并不重要!Compose 并不推崇在控件内部维护状态,否则会违背 纯函数 的理念。而且,我们可以很轻易地利用 组合 方式外挂一段逻辑,实现单选。

注意:按照一般性的UI设计语言,多选应该利用Checkbox,实现思路有类似之处

如何使用

@Composable
fun RadioButton(
    selected: Boolean,
    onClick: (() -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: RadioButtonColors = RadioButtonDefaults.colors()
)

和前两者不同,RadioButton 的回调函数是 点击回调 含义。

遵照UI设计语言,被点击则意味着 选中,而在单选中,通过逻辑保持该组仅目标项被选中。

通过以下API可以修改配色

object RadioButtonDefaults {
    
    @Composable
    fun colors(
        selectedColor: Color = MaterialTheme.colors.secondary,
        unselectedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
        disabledColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
    ): RadioButtonColors
}

构建一个单选组:

val tags = arrayListOf("A", "B", "C", "D")
val selectedTag = remember { mutableStateOf("") }

Row {
    tags.forEach {
        Row {
            RadioButton(
                selected = it == selectedTag.value,
                onClick = {
                    selectedTag.value = it
                }
            )

            Text(text = it)
        }

        Spacer(modifier = Modifier.width(20.dp))
    }
}

同样,读者可以按照喜好利用代理的方式创建 可观测数据源

val tags = arrayListOf("A", "B", "C", "D")
var selectedTag by rememberSaveable { mutableStateOf("") }


Row {
    tags.forEach {
        Row {
            RadioButton(
                selected = selectedTag == it,
                onClick = {
                    selectedTag = it
                }
            )

            Text(text = it,modifier = Modifier.clickable {
                selectedTag = it
            })
        }

        Spacer(modifier = Modifier.width(20.dp))
    }
}

而且,按照UX的设计风格,可以灵活的为文字部分添加触发。

思考:如下代码可以实现多选吗?

val tags = arrayListOf("A", "B", "C", "D")
var selectedItems by rememberSaveable { mutableStateOf(linkedSetOf<String>()) }

Row {
    tags.forEach {
        Row {
            Checkbox(checked = selectedItems.contains(it), onCheckedChange = {selected->
                if (selected) {
                    selectedItems.add(it)
                } else {
                    selectedItems.remove(it)
                }
                Log.d("tag", "onCheckChanged:$it,$selected")
            })

            Text(text = it)
        }

        Spacer(modifier = Modifier.width(20.dp))
    }
}

当然不可以!Set 集合内容的变化并无法被观测!那么怎么处理呢?

以目前我们掌握的知识,可以:

  • 利用位运算
  • 添加一个观测源、组合判断,但这会被lint校验警告
//组合判断 会被警告的做法
val tags = arrayListOf("A", "B", "C", "D")
val selectedItems by rememberSaveable { mutableStateOf(linkedSetOf<String>()) }
var version by rememberSaveable { mutableStateOf(0) }

Row {
    tags.forEach {
        Row {
            Checkbox(
                checked = version != null && selectedItems.contains(it),
                onCheckedChange = { selected ->
                    if (selected) {
                        selectedItems.add(it)
                    } else {
                        selectedItems.remove(it)
                    }
                    version++
                    Log.d("tag", "onCheckChanged:$it,$selected")
                })

            Text(text = it)
        }

        Spacer(modifier = Modifier.width(20.dp))
    }
}

是否还有更加优雅的解法呢?我们先保留这一疑惑,使得我们的探索过程更加的 平稳有趣 , 而不必在开始阶段花费过多的精力。

效果

效果

结语

很快又到了这一阶段,这意味着我们又掌握了新的知识,同时也产生了更多的疑惑,不过不用心急,所有的谜题最终都会解开。