Jetpack Compose 公开的下拉菜单
Exposed drop-down menu for jetpack compose
我想知道是否有针对 Exposed drop-down menu for jetpack compose 的解决方案?
我在 Jetpack Compose 中找不到适合此组件的解决方案。有帮助吗?
版本1.1.0-alpha06
在ExposedDropdownMenuBox
的基础上引入了ExposedDropdownMenu
的实现TextField
和 DropdownMenu
里面。
类似于:
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)
}
}
}
}
这就是我为使宽度与文本字段相同所做的工作:复制和修改 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)
}
}
}
}
这是我的版本。
我在没有使用 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
@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)
}
}
}
}
}
除了这里写的内容之外,我的案例可能对某些人有用,对于我个人的备忘笔记以备下次使用,我已经意识到这个 drop-down 菜单功能组件使用 BasicTextField 进行无装饰和没有默认填充,没有箭头图标,项目选择的文本右对齐 (.End),用列表中的单行填充最大文本宽度 (.fillMaxWidth())。
data class DropDownMenuParameter(
var options: List<String>,
var expanded: Boolean,
var selectedOptionText: String,
var backgroundColor: Color
)
@ExperimentalMaterialApi
@Composable
fun DropDownMenuComponent(params: DropDownMenuParameter) {
var expanded by remember { mutableStateOf(params.expanded) }
var selectedOptionText by remember { mutableStateOf(params.selectedOptionText) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {
BasicTextField(
modifier = Modifier
.background(params.backgroundColor)
.fillMaxWidth(),
readOnly = true,
value = selectedOptionText,
onValueChange = { },
textStyle = TextStyle(
color = Color.White,
textAlign = TextAlign.End,
fontSize = 16.sp,
),
singleLine = true
)
ExposedDropdownMenu(
modifier = Modifier
.background(params.backgroundColor),
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
params.options.forEach { selectionOption ->
DropdownMenuItem(
modifier = Modifier
.background(params.backgroundColor),
onClick = {
selectedOptionText = selectionOption
expanded = false
},
) {
Text(
text = selectionOption,
color = Color.White,
)
}
}
}
}
}
我的用法:
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
@Composable
fun SubscribeSubscriptionDetails(selectedSubscription : Subscription){
val categoryOptions = listOf("Entertainment", "Gaming", "Business", "Utility", "Music", "Food & Drink", "Health & Fitness", "Bank", "Transport", "Education", "Insurance", "News")
val expanded by remember { mutableStateOf(false) }
val selectedOptionText by remember { mutableStateOf(selectedSubscription.category) }
// ....
Row { // categoria
Text(
modifier = Modifier
.padding(textMargin_24, 0.dp, 0.dp, 0.dp)
.weight(0.5f),
text = "Categoria",
fontWeight = FontWeight.Bold,
color = Color.White,
textAlign = TextAlign.Left,
fontSize = 16.sp,
)
Row(
modifier = Modifier
.padding(0.dp, 0.dp, 24.dp, 0.dp)
.weight(0.5f),
horizontalArrangement = Arrangement.End
){
DropDownMenuComponent(
DropDownMenuParameter(
options = categoryOptions,
expanded = expanded,
selectedOptionText = selectedOptionText,
backgroundColor = Color.Red
)
)
}
}
// .....
}
我想知道是否有针对 Exposed drop-down menu for jetpack compose 的解决方案? 我在 Jetpack Compose 中找不到适合此组件的解决方案。有帮助吗?
版本1.1.0-alpha06
在ExposedDropdownMenuBox
的基础上引入了ExposedDropdownMenu
的实现TextField
和 DropdownMenu
里面。
类似于:
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)
}
}
}
}
这就是我为使宽度与文本字段相同所做的工作:复制和修改 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)
}
}
}
}
这是我的版本。
我在没有使用 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
@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)
}
}
}
}
}
除了这里写的内容之外,我的案例可能对某些人有用,对于我个人的备忘笔记以备下次使用,我已经意识到这个 drop-down 菜单功能组件使用 BasicTextField 进行无装饰和没有默认填充,没有箭头图标,项目选择的文本右对齐 (.End),用列表中的单行填充最大文本宽度 (.fillMaxWidth())。
data class DropDownMenuParameter(
var options: List<String>,
var expanded: Boolean,
var selectedOptionText: String,
var backgroundColor: Color
)
@ExperimentalMaterialApi
@Composable
fun DropDownMenuComponent(params: DropDownMenuParameter) {
var expanded by remember { mutableStateOf(params.expanded) }
var selectedOptionText by remember { mutableStateOf(params.selectedOptionText) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {
BasicTextField(
modifier = Modifier
.background(params.backgroundColor)
.fillMaxWidth(),
readOnly = true,
value = selectedOptionText,
onValueChange = { },
textStyle = TextStyle(
color = Color.White,
textAlign = TextAlign.End,
fontSize = 16.sp,
),
singleLine = true
)
ExposedDropdownMenu(
modifier = Modifier
.background(params.backgroundColor),
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
params.options.forEach { selectionOption ->
DropdownMenuItem(
modifier = Modifier
.background(params.backgroundColor),
onClick = {
selectedOptionText = selectionOption
expanded = false
},
) {
Text(
text = selectionOption,
color = Color.White,
)
}
}
}
}
}
我的用法:
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
@Composable
fun SubscribeSubscriptionDetails(selectedSubscription : Subscription){
val categoryOptions = listOf("Entertainment", "Gaming", "Business", "Utility", "Music", "Food & Drink", "Health & Fitness", "Bank", "Transport", "Education", "Insurance", "News")
val expanded by remember { mutableStateOf(false) }
val selectedOptionText by remember { mutableStateOf(selectedSubscription.category) }
// ....
Row { // categoria
Text(
modifier = Modifier
.padding(textMargin_24, 0.dp, 0.dp, 0.dp)
.weight(0.5f),
text = "Categoria",
fontWeight = FontWeight.Bold,
color = Color.White,
textAlign = TextAlign.Left,
fontSize = 16.sp,
)
Row(
modifier = Modifier
.padding(0.dp, 0.dp, 24.dp, 0.dp)
.weight(0.5f),
horizontalArrangement = Arrangement.End
){
DropDownMenuComponent(
DropDownMenuParameter(
options = categoryOptions,
expanded = expanded,
selectedOptionText = selectedOptionText,
backgroundColor = Color.Red
)
)
}
}
// .....
}