Compose UI (1.0.0-beta06)中没有现成的Spinner可用,当然也可以直接使用androidView的Spinner控件。但是我们需要利用声明式UI的好处,就是自定义非常方便。
回忆之前的Spinner,需要一个String数组的适配器,一个selecter监听,指定下拉的资源(样式),传入一个pos位置信息。so,我们的入参大概就是:
/**
* 自定义下拉列表
* @author markrenChina
*
* @param modifier 约束
* @param onSpinnerItemSelected item点击事件
* @param itemList 显示下拉的字符串列表(可修改为泛型)
* @param position 当前选择的位置
* @param title 下拉标题
* @param itemListRes R.array 优先于itemList
*/
@Composable
fun Spinner(
modifier: Modifier,
onSpinnerItemSelected: (Int) -> Unit = { _ -> },
itemList: List<String> = listOf(),
@ArrayRes itemListRes: Int? = null,
position: Int? = null,
title: String? = null
) {}
其中标题title是我新增的一个效果,具体是没有position时,显示下拉标题,下拉时显示标题。
spinner肯定是需要自身一个状态expanded来判断自身是否展开。这里expanded是不需要父组件管理的,因为是其自身的一个状态。compose关于UI状态,跟VUE.js和flutter基本一脉相承。
// 是否展开
var expanded by remember { mutableStateOf<Boolean>(false) }
然后我们需要在点击时,有一些改变,我们先建一个小组件
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun SpinnerSpacer(visible: Boolean) {
AnimatedVisibility(visible = visible) {
Spacer(modifier = Modifier.height(8.dp))
}
}
我们把整个Spinner用surface包裹,可以更好适配主题。下拉列表,我们暂时只实现下拉(不上拉),于是代码:
// 是否展开
var expanded by remember { mutableStateOf<Boolean>(false) }
SpinnerSpacer(visible = expanded)
Surface(
modifier = modifier
.fillMaxWidth()
.clickable { expanded = !expanded },
elevation = 2.dp,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp)
.animateContentSize()
) {
}
}
SpinnerSpacer(visible = expanded)
分析一下要展示的组件:一个是无论是否下拉都显示的部分,一个是expanded时显示的部分。在都显示的部分,我们需要放置2个东西一个是内容,一个倒三角的icon。expanded显示部分应该根据入参,R.array 优先于itemList,至于为什么放置两种条件,一个是原android就支持R.array,另一个是itemList方便viewmodel管理。
所以,我们需要一个能左右放置的布局,和一个能弹出遮盖下面UI的列表。
AroundRow布局中,我们要对子组件进行测量,然后把内容放左边,倒三角的icon放最右边。
代码如下:
/**
* 自定义左右置顶布局
* @author markrenChina
* @param startPadding 左边留空
* @param endPadding 右边留空
*/
@Composable
fun AroundRow(
modifier: Modifier = Modifier,
startPadding: Dp = 0.dp,
endPadding: Dp = 0.dp,
content: @Composable () -> Unit,
) {
Layout(
modifier = modifier,
content = content
){ measurables, constraints ->
val placeables = measurables.map { measurable ->
//Measure each child
measurable.measure(constraints)
}
if (placeables.size != 2) {
throw RuntimeException("AroundRow illegality")
}
val maxHeight = max(placeables[0].height,placeables[1].height)
layout(
constraints.maxWidth,
maxHeight
){
placeables[0].placeRelative(startPadding.roundToPx(),(maxHeight - placeables[0].height)/2)
placeables[1].placeRelative(
constraints.maxWidth - placeables[1].width - endPadding.roundToPx(),
(maxHeight - placeables[1].height)/2
)
}
}
}
自定义布局代码比较简单,先是测量所有子组件,如果传入超过2个子组件抛异常。根据测量结果计算应该放置的x,y坐标。
//1.
var items = itemList
itemListRes?.let {
items = LocalContext.current.resources.getStringArray(it).toList()
}
var showItem = if (expanded) {
title ?: position?.let { items[it] } ?: "Spinner"
} else {
position?.let { items[it] } ?: title ?: "Spinner"
}
//2.
AroundRow {
Text(
text = showItem,
style = MaterialTheme.typography.h6
)
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = null,
)
}
//3.
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
repeat(items.size) { index ->
DropdownMenuItem(onClick = {
//子传父 index
onSpinnerItemSelected(index)
expanded = false
}) {
Text(text = items[index])
}
}
}
使用只是简单是示例,理论上的数据应该遵从mvvm使用livedata从viewmodel获取,而列表下拉选项,最佳选择是R.array也可以持久化的datastore或是database从获取。
var operatorPos by remember { mutableStateOf(1) }
val operator = remember { listOf("admin", "小明", "小王") }
Spinner(
modifier = Modifier
.weight(1f)
.padding(2.dp),
onSpinnerItemSelected = { operatorPos = it },
position = operatorPos,
itemList = operator,
title = "检测人员"
)
后期如有bug,会在下方更新
————————————这是分割线——————————————
项目版本从1.0.0-bate06升级1.0.0-bate08,出现了modifier.clickable { }失效的问题,修改如下
SpinnerSpacer(visible = expanded)
Surface(
modifier = modifier
.fillMaxWidth(),
//.clickable { expanded = !expanded },
onClick = { expanded = !expanded },
elevation = 2.dp,
)
fun Spinner()上方需要加入注解@OptIn(ExperimentalMaterialApi::class)
另外:fun AroundRow()前面增加 inline