compose UI(三)自定义控件和布局实现简易下拉列表Spinner

Compose UI (1.0.0-beta06)中没有现成的Spinner可用,当然也可以直接使用androidView的Spinner控件。但是我们需要利用声明式UI的好处,就是自定义非常方便。

首先 建一个Spinner.kt

回忆之前的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.kt

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. 展示数据处理
  2. 放置长期显示的组件
  3. 放置下拉组件
			//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,会在下方更新
————————————这是分割线——————————————

6月9号修改内容:

项目版本从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

你可能感兴趣的:(android,列表,android)