Jetpack compose 的公开下拉菜单

Posted

技术标签:

【中文标题】Jetpack compose 的公开下拉菜单【英文标题】:Exposed drop-down menu for jetpack compose 【发布时间】:2021-07-10 15:23:30 【问题描述】:

我想知道是否有针对 jetpack compose 的 Exposed 下拉菜单的解决方案? 我在 jetpack compose 中找不到这个组件的合适解决方案。有什么帮助吗?

【问题讨论】:

【参考方案1】:

1.1.0-alpha06版本在ExposedDropdownMenuBox的基础上引入了ExposedDropdownMenu的实现,里面有TextFieldDropdownMenu

类似:

    val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5")
    var expanded by remember  mutableStateOf(false) 
    var selectedOptionText by remember  mutableStateOf(options[0]) 
    
    ExposedDropdownMenuBox(
        expanded = expanded,
        onExpandedChange = 
            expanded = !expanded
        
    ) 
        TextField(
            readOnly = true,
            value = selectedOptionText,
            onValueChange =  ,
            label =  Text("Label") ,
            trailingIcon = 
                ExposedDropdownMenuDefaults.TrailingIcon(
                    expanded = expanded
                )
            ,
            colors = ExposedDropdownMenuDefaults.textFieldColors()
        )
        ExposedDropdownMenu(
            expanded = expanded,
            onDismissRequest = 
                expanded = false
            
        ) 
            options.forEach  selectionOption ->
                DropdownMenuItem(
                    onClick = 
                        selectedOptionText = selectionOption
                        expanded = false
                    
                ) 
                    Text(text = selectionOption)
                
            
        
    

1.0.x 版本没有内置组件。 您可以使用OutlinedTextField + DropdownMenu

这只是一个基本的(非常基本的)实现:

var expanded by remember  mutableStateOf(false) 
val suggestions = listOf("Item1","Item2","Item3")
var selectedText by remember  mutableStateOf("") 

var textfieldSize by remember  mutableStateOf(Size.Zero)

val icon = if (expanded)
    Icons.Filled.ArrowDropUp //it requires androidx.compose.material:material-icons-extended
else
    Icons.Filled.ArrowDropDown


Column() 
    OutlinedTextField(
        value = selectedText,
        onValueChange =  selectedText = it ,
        modifier = Modifier
            .fillMaxWidth()
            .onGloballyPositioned  coordinates ->
                //This value is used to assign to the DropDown the same width
                textfieldSize = coordinates.size.toSize()
            ,
        label = Text("Label"),
        trailingIcon = 
            Icon(icon,"contentDescription",
                 Modifier.clickable  expanded = !expanded )
        
    )
    DropdownMenu(
        expanded = expanded,
        onDismissRequest =  expanded = false ,
        modifier = Modifier
            .width(with(LocalDensity.current)textfieldSize.width.toDp())
    ) 
        suggestions.forEach  label ->
            DropdownMenuItem(onClick = 
                selectedText = label
            ) 
                Text(text = label)
            
        
    

【讨论】:

在 Google 的 issuetracker 中提交了一个错误:https://issuetracker.google.com/issues/173532272 希望它在稳定版发布之前实现。 有没有办法让DropdownMenu的宽度和OutlinedTextField的宽度一样? 有没有办法将菜单放置在文本字段上方而不是下方? 如何让textview变亮而不是变暗? 我在 1.1.0 最终版本上,ExposedDropdownMenu()ExposedDropdownMenuBox() 都没有为我导入,但很可能我缺少一个依赖项。【参考方案2】:

这是我为使宽度与文本字段相同所做的操作:复制和修改 Gabriele 的答案。

var expanded by remember  mutableStateOf(false) 
val suggestions = listOf("Item1","Item2","Item3")
var selectedText by remember  mutableStateOf("") 

var dropDownWidth by remember  mutableStateOf(0) 

val icon = if (expanded)
    Icons.Filled.....
else
    Icons.Filled.ArrowDropDown


Column() 
    OutlinedTextField(
        value = selectedText,
        onValueChange =  selectedText = it ,
        modifier = Modifier.fillMaxWidth()
            .onSizeChanged 
                dropDownWidth = it.width
            ,
        label = Text("Label"),
        trailingIcon = 
            Icon(icon,"contentDescription", Modifier.clickable  expanded = !expanded )
        
    )
    DropdownMenu(
        expanded = expanded,
        onDismissRequest =  expanded = false ,
        modifier = Modifier
                .width(with(LocalDensity.current)dropDownWidth.toDp())
    ) 
        suggestions.forEach  label ->
            DropdownMenuItem(onClick = 
                selectedText = label
            ) 
                Text(text = label)
            
        
    

【讨论】:

【参考方案3】:

这是我的版本。 我在没有使用TextField 的情况下实现了这一点(所以没有键盘)。 有“常规”和“轮廓”两种版本。

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch


// ExposedDropDownMenu will be added in Jetpack Compose 1.1.0.
// This is a reimplementation while waiting.
// See https://***.com/questions/67111020/exposed-drop-down-menu-for-jetpack-compose/6904285

@Composable
fun SimpleExposedDropDownMenu(
    values: List<String>,
    selectedIndex: Int,
    onChange: (Int) -> Unit,
    label: @Composable () -> Unit,
    modifier: Modifier,
    backgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity),
    shape: Shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
) 
    SimpleExposedDropDownMenuImpl(
        values = values,
        selectedIndex = selectedIndex,
        onChange = onChange,
        label = label,
        modifier = modifier,
        backgroundColor = backgroundColor,
        shape = shape,
        decorator =  color, width, content ->
            Box(
                Modifier
                    .drawBehind 
                        val strokeWidth = width.value * density
                        val y = size.height - strokeWidth / 2
                        drawLine(
                            color,
                            Offset(0f, y),
                            Offset(size.width, y),
                            strokeWidth
                        )
                    
            ) 
                content()
            
        
    )


@Composable
fun SimpleOutlinedExposedDropDownMenu(
    values: List<String>,
    selectedIndex: Int,
    onChange: (Int) -> Unit,
    label: @Composable () -> Unit,
    modifier: Modifier,
    backgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity),
    shape: Shape = MaterialTheme.shapes.small
) 
    SimpleExposedDropDownMenuImpl(
        values = values,
        selectedIndex = selectedIndex,
        onChange = onChange,
        label = label,
        modifier = modifier,
        backgroundColor = backgroundColor,
        shape = shape,
        decorator =  color, width, content ->
            Box(
                Modifier
                    .border(width, color, shape)
            ) 
                content()
            
        
    )


@Composable
private fun SimpleExposedDropDownMenuImpl(
    values: List<String>,
    selectedIndex: Int,
    onChange: (Int) -> Unit,
    label: @Composable () -> Unit,
    modifier: Modifier,
    backgroundColor: Color,
    shape: Shape,
    decorator: @Composable (Color, Dp, @Composable () -> Unit) -> Unit
) 
    var expanded by remember  mutableStateOf(false) 
    var textfieldSize by remember  mutableStateOf(Size.Zero) 

    val indicatorColor =
        if (expanded) MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high)
        else MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.UnfocusedIndicatorLineOpacity)
    val indicatorWidth = (if (expanded) 2 else 1).dp
    val labelColor =
        if (expanded) MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high)
        else MaterialTheme.colors.onSurface.copy(ContentAlpha.medium)
    val trailingIconColor = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.IconOpacity)

    val rotation: Float by animateFloatAsState(if (expanded) 180f else 0f)

    val focusManager = LocalFocusManager.current

    Column(modifier = modifier.width(IntrinsicSize.Min)) 
        decorator(indicatorColor, indicatorWidth) 
            Box(
                Modifier
                    .fillMaxWidth()
                    .background(color = backgroundColor, shape = shape)
                    .onGloballyPositioned  textfieldSize = it.size.toSize() 
                    .clip(shape)
                    .clickable 
                        expanded = !expanded
                        focusManager.clearFocus()
                    
                    .padding(start = 16.dp, end = 12.dp, top = 7.dp, bottom = 10.dp)
            ) 
                Column(Modifier.padding(end = 32.dp)) 
                    ProvideTextStyle(value = MaterialTheme.typography.caption.copy(color = labelColor)) 
                        label()
                    
                    Text(
                        text = values[selectedIndex],
                        modifier = Modifier.padding(top = 1.dp)
                    )
                
                Icon(
                    imageVector = Icons.Filled.ExpandMore,
                    contentDescription = "Change",
                    tint = trailingIconColor,
                    modifier = Modifier
                        .align(Alignment.CenterEnd)
                        .padding(top = 4.dp)
                        .rotate(rotation)
                )

            
        

        DropdownMenu(
            expanded = expanded,
            onDismissRequest =  expanded = false ,
            modifier = Modifier
                .width(with(LocalDensity.current)  textfieldSize.width.toDp() )
        ) 
            values.forEachIndexed  i, v ->
                val scope = rememberCoroutineScope()
                DropdownMenuItem(
                    onClick = 
                        onChange(i)
                        scope.launch 
                            delay(150)
                            expanded = false
                        
                    
                ) 
                    Text(v)
                
            
        
    

【讨论】:

感谢您分享此内容。这个看起来和工作方式与我期望的 ExposedDropdown 完全一样。我唯一需要改变的是使用Icons.Filled.ArrowDropDown 而不是Icons.Filled.ExpandMore

以上是关于Jetpack compose 的公开下拉菜单的主要内容,如果未能解决你的问题,请参考以下文章

Jetpack Compose : 一学就会的自定义下拉刷新&加载更多

Android-利用Jetpack-Compose-+Paging3+swiperefresh实现分页加载,下拉上拉效果

Jetpack ComposeLazyColumn 使用Paging3分页+SwipeRefresh下拉刷新

下拉菜单在表单中切成两半

Drupal 视图和暴露的过滤器下拉菜单

Compose 下拉刷新