如何在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
相关产品推荐
相关产品推荐

