You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在ExposedDropDown中实现分页懒加载列表?

如何解决Jetpack Compose下拉菜单中懒加载分页列表的尺寸崩溃问题?

在应用中,我们需要在下拉菜单内展示分页选项列表。由于数据采用懒加载方式,列表尺寸不固定,若不为嵌套的PaginatedLazyColumn组件设置固定尺寸值,可能会导致崩溃。想咨询该如何解决这个问题?

另外,是否可以通过计算数据第0页中最长字符串的宽度(后续出现更长字符串时可截断),将其设为下拉菜单的宽度,同时将高度设置为一个“舒适”的DP值?如果可以,该如何实现?

原组件代码

LyraDropDown Composable

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LyraDropDown(
    modifier: Modifier = Modifier,
    data: List<String>,
    defaultSelectionIndex: Int = 0,
    label: String,
    onClick: (Int) -> Unit,
    paginationCallback: () -> Unit = {},
    itemContent: @Composable (String) -> Unit
) {
    var expanded by remember { mutableStateOf(false) }
    var selectedIndex by remember { mutableIntStateOf(defaultSelectionIndex) }
    val listWithDefaultOption = data.toMutableList()
    listWithDefaultOption.add(0, "No selection")

    ExposedDropdownMenuBox(
        modifier = modifier.background(
            colorResource(id = R.color.white),
            shape = RoundedCornerShape(8.dp)
        ),
        expanded = expanded, onExpandedChange = { expanded = !expanded }) {
        OutlinedTextField(
            modifier = Modifier
                .fillMaxWidth()
                .menuAnchor()
                .background(colorResource(id = R.color.white), shape = RoundedCornerShape(8.dp)),
            readOnly = true,
            label = {
                Text(label, fontSize = 12.sp)
            },
            value = listWithDefaultOption[selectedIndex],
            textStyle = TextStyle(fontSize = 12.sp),
            onValueChange = { },
            trailingIcon = {
                Icon(
                    painter = painterResource(id = if (!expanded) R.drawable.ic_chev_down else R.drawable.ic_chev_up),
                    contentDescription = null,
                    modifier = Modifier
                        .size(24.dp)
                        .padding(4.dp),
                    tint = colorResource(id = R.color.gray_700)
                )
            },
            shape = RoundedCornerShape(8.dp),
            colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(
                unfocusedContainerColor = colorResource(id = R.color.white),
                focusedContainerColor = colorResource(id = R.color.white),
                focusedBorderColor = colorResource(id = R.color.indigo_500),
                unfocusedBorderColor = colorResource(id = R.color.gray_200),
                focusedLabelColor = colorResource(id = R.color.blue_500),
                unfocusedLabelColor = colorResource(id = R.color.black)
            )
        )
        ExposedDropdownMenu(
            expanded = expanded,
            onDismissRequest = {
                expanded = false
            },
            modifier = Modifier
                .background(colorResource(id = R.color.white))
                .width(200.dp)
                .height(300.dp),
        ) {

            PaginatedLazyColumn(
                modifier = Modifier
                    .width(200.dp)
                    .height(300.dp),
                items = listWithDefaultOption,
                itemKey = { UUID.randomUUID() },
                itemContentIndexed = { selectionOption, index ->
                    DropdownMenuItem(
                        onClick = {
                            selectedIndex = index
                            expanded = false
                            if (index != 0) onClick(index - 1) // account for hardcoded option
                        },
                        text = {
                            itemContent(selectionOption)
                        })
                },
                loadingItem = { LyraCircularProgressIndicator() },
                contentWhenEmpty = { }) {
                paginationCallback()
            }
        }
    }
}

PaginatedLazyColumn 定义

@Composable
internal fun <T> PaginatedLazyColumn(
    modifier: Modifier = Modifier,
    loading: Boolean = false,
    listState: LazyListState = rememberLazyListState(),
    items: List<T>,
    itemKey: (T) -> Any,
    itemContentIndexed: @Composable (T, Int) -> Unit,
    loadingItem: @Composable () -> Unit,
    contentWhenEmpty: @Composable () -> Unit,
    loadMore: () -> Unit
) {

    val reachedBottom: Boolean by remember { derivedStateOf { listState.reachedBottom() } }

    // load more if scrolled to bottom
    LaunchedEffect(reachedBottom) {
        if (reachedBottom && !loading) loadMore()
    }

    LazyColumn(modifier = modifier, state = listState) {
        itemsIndexed(
            items = items,
            key = { _, item -> itemKey(item) }
        ) { index, item ->
            itemContentIndexed(item, index)
        }
        if (items.isEmpty()) {
            item {
                contentWhenEmpty()
            }
        }
        if (loading) {
            item {
                loadingItem()
            }
        }
    }
}

private fun LazyListState.reachedBottom(buffer: Int = 1): Boolean {
    val lastVisibleItem = this.layoutInfo.visibleItemsInfo.lastOrNull()
    return lastVisibleItem?.index != 0 && lastVisibleItem?.index == this.layoutInfo.totalItemsCount - buffer
}

解决方案

你的思路完全可行,下面是具体实现步骤和修改后的代码:

1. 测量第一页最长字符串的宽度

使用Compose的TextMeasurer计算文本宽度,确保测量时使用和下拉菜单项一致的文本样式(字体大小、字重等),保证宽度计算准确。

2. 设置下拉菜单宽度和固定高度

将测量得到的最大宽度作为下拉菜单的基础宽度,加上额外内边距(比如16dp)避免内容贴边;高度设置为固定舒适值(比如300dp),同时保留滚动功能。

3. 处理超长文本截断

在下拉菜单项的文本中添加maxLines = 1和overflow = TextOverflow.Ellipsis,确保后续加载的更长字符串不会溢出,自动截断显示省略号。

修改后的代码

调整后的LyraDropDown

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LyraDropDown(
    modifier: Modifier = Modifier,
    data: List<String>,
    defaultSelectionIndex: Int = 0,
    label: String,
    onClick: (Int) -> Unit,
    paginationCallback: () -> Unit = {},
    itemTextStyle: TextStyle = TextStyle(fontSize = 12.sp) // 提取统一文本样式
) {
    var expanded by remember { mutableStateOf(false) }
    var selectedIndex by remember { mutableIntStateOf(defaultSelectionIndex) }
    val listWithDefaultOption = data.toMutableList()
    listWithDefaultOption.add(0, "No selection")

    // 测量最长字符串宽度
    val textMeasurer = rememberTextMeasurer()
    val maxTextWidth = remember(listWithDefaultOption, itemTextStyle) {
        listWithDefaultOption.maxOfOrNull { text ->
            textMeasurer.measure(
                text = text,
                style = itemTextStyle
            ).size.width
        } ?: 0
    }
    // 转换为Dp并加上内边距
    val menuWidth = with(LocalDensity.current) { maxTextWidth.toDp() } + 16.dp
    // 固定舒适高度
    val menuHeight = 300.dp

    ExposedDropdownMenuBox(
        modifier = modifier.background(
            colorResource(id = R.color.white),
            shape = RoundedCornerShape(8.dp)
        ),
        expanded = expanded, onExpandedChange = { expanded = !expanded }) {
        OutlinedTextField(
            modifier = Modifier
                .fillMaxWidth()
                .menuAnchor()
                .background(colorResource(id = R.color.white), shape = RoundedCornerShape(8.dp)),
            readOnly = true,
            label = {
                Text(label, fontSize = 12.sp)
            },
            value = listWithDefaultOption[selectedIndex],
            textStyle = itemTextStyle,
            onValueChange = { },
            trailingIcon = {
                Icon(
                    painter = painterResource(id = if (!expanded) R.drawable.ic_chev_down else R.drawable.ic_chev_up),
                    contentDescription = null,
                    modifier = Modifier
                        .size(24.dp)
                        .padding(4.dp),
                    tint = colorResource(id = R.color.gray_700)
                )
            },
            shape = RoundedCornerShape(8.dp),
            colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(
                unfocusedContainerColor = colorResource(id = R.color.white),
                focusedContainerColor = colorResource(id = R.color.white),
                focusedBorderColor = colorResource(id = R.color.indigo_500),
                unfocusedBorderColor = colorResource(id = R.color.gray_200),
                focusedLabelColor = colorResource(id = R.color.blue_500),
                unfocusedLabelColor = colorResource(id = R.color.black)
            )
        )
        ExposedDropdownMenu(
            expanded = expanded,
            onDismissRequest = { expanded = false },
            modifier = Modifier
                .background(colorResource(id = R.color.white))
                .width(menuWidth)
                .height(menuHeight)
        ) {
            PaginatedLazyColumn(
                modifier = Modifier.fillMaxSize(), // 直接继承父布局尺寸
                items = listWithDefaultOption,
                itemKey = { it }, // 用文本本身作为key,避免频繁重组
                itemContentIndexed = { selectionOption, index ->
                    DropdownMenuItem(
                        onClick = {
                            selectedIndex = index
                            expanded = false
                            if (index != 0) onClick(index - 1)
                        },
                        text = {
                            // 处理超长文本截断
                            Text(
                                text = selectionOption,
                                style = itemTextStyle,
                                maxLines = 1,
                                overflow = TextOverflow.Ellipsis
                            )
                        }
                    )
                },
                loadingItem = { LyraCircularProgressIndicator() },
                contentWhenEmpty = { }) {
                paginationCallback()
            }
        }
    }
}

注意事项

  • 提取itemTextStyle参数,确保测量和显示的文本样式一致,避免宽度计算偏差。
  • PaginatedLazyColumn改用fillMaxSize(),直接继承下拉菜单的固定尺寸,无需重复设置宽高。
  • 将itemKey从UUID.randomUUID()改为文本本身,提升列表性能,避免不必要的重组。

内容的提问来源于stack exchange,提问作者Stelios Papamichail

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 11:19:54