How to create a dropdown menu items on a button click. In Jetpack compose?
Like here but for buttons :
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
toggle = iconButton,
dropdownOffset = Position(24.dp, 0.dp),
toggleModifier = modifier
) {
options.forEach {
DropdownMenuItem(onClick = {}) {
Text(it)
}
}
}
The previous answer is correct, but the key part is missing. Both, DropdownMenu and the button that opens it suppose to be wrapped in Box. Only this way the opening button will be used as an anchor for the menu.
This is my version:
#Composable
fun DropdownMenu(
colorSelected: Color = scColors.primary,
colorBackground: Color = scColors.onSurface,
expanded: Boolean,
selectedIndex: Int,
items: List<String>,
onSelect: (Int) -> Unit,
onDismissRequest: () -> Unit,
content: #Composable () -> Unit
) {
Box {
content()
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = Modifier
.height(300.dp)
.fillMaxWidth()
.background(
color = colorBackground,
shape = RoundedCornerShape(16.dp)
)
) {
items.forEachIndexed { index, s ->
if (selectedIndex == index) {
DropdownMenuItem(
modifier = Modifier
.fillMaxWidth()
.background(
color = colorSelected,
shape = RoundedCornerShape(16.dp)
),
onClick = { onSelect(index) }
) {
Text(
text = s,
color = Color.Black,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
} else {
DropdownMenuItem(
modifier = Modifier.fillMaxWidth(),
onClick = { onSelect(index) }
) {
Text(
text = s,
color = Color.DarkGray,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
}
}
And, then a DropdownMenu accepts the opening anchor button as a content:
val items = listOf(
"English",
"Russian",
"Spanish",
"French",
"German",
"Hebrew"
)
#Preview
#Composable
fun TestDropdownMenu() {
var expanded by remember { mutableStateOf(false) }
var selectedIndex by remember { mutableStateOf(0) }
val buttonTitle = items[selectedIndex]
DropdownMenu(
colorSelected = scColors.onSurface,
colorBackground = scColors.primary,
expanded = expanded,
selectedIndex = selectedIndex,
items = items,
onSelect = { index ->
selectedIndex = index
expanded = false
},
onDismissRequest = {
expanded = false
}) {
Button(
onClick = {
expanded = true
}
) {
Text(
text = buttonTitle,
color = Color.Black,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
You can use something like:
var expanded by remember { mutableStateOf(false) }
Button(onClick = { expanded = true }){
Text ("...")
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
//....
) {
items.forEachIndexed { index, s ->
//....
}
}
you can create a dropdown list in compose by using this
list : list you want to show
label : label is the hint to show in the textview
default : to set default value in textview
validateInput = you can validate the input by changing the validateInput state to true on the button clicked and handle it accordingly
fun dropdownList(
list: List<String>,
label: String,
defaultValue: String = "",
validateInput: Boolean
): String {
var expanded by remember { mutableStateOf(false) }
var selectedText by remember { mutableStateOf(defaultValue) }
var textFieldSize by remember { mutableStateOf(Size.Zero) }
var isError by remember { mutableStateOf(false) }
if (validateInput && selectedText.isEmpty())
isError = true
val icon = if (expanded)
Icons.Filled.ArrowDropUp
else
Icons.Filled.ArrowDropDown
Column(modifier = Modifier.padding(bottom = 2.dp, top = 2.dp)) {
OutlinedTextField(
value = selectedText,
onValueChange = {
selectedText = it
},
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
textFieldSize = coordinates.size.toSize()
},
label = { Text(label) },
trailingIcon = {
Icon(icon, "contentDescription",
Modifier.clickable { expanded = !expanded })
},
isError = isError
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.width(with(LocalDensity.current) { textFieldSize.width.toDp() })
) {
list.forEach { label ->
DropdownMenuItem(onClick = {
selectedText = label
expanded = false
}) {
Text(text = label)
}
}
}
if (isError) {
Text(
text = "$label can't be empty",
color = Color.Red,
textAlign = TextAlign.End,
modifier = Modifier.fillMaxWidth()
)
}
}
return selectedText
}
Github gist link DropdownList.kt
Related
For some reason the Text components are not exactly aligned vertically as the radio buttons in my dialog. I tried adjusting the padding values in every part but there is still not any effect.
#Composable
fun CommonDialog(
title: String?,
state: MutableState<Boolean>,
content: #Composable (() -> Unit)? = null
) {
AlertDialog(
onDismissRequest = {
state.value = false
},
title = title?.let {
{
Column(
Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
Text(text = title)
}
}
},
text = content,
confirmButton = {
TextButton(onClick = { state.value = false }) {
Text(stringResource(id = R.string.button_cancel))
}
}, modifier = Modifier.padding(vertical = 5.dp)
)
}
#Composable
fun AlertSingleChoiceView(state: MutableState<Boolean>) {
CommonDialog(title = stringResource(id = R.string.theme), state = state) {
SingleChoiceView()
}
}
#Composable
fun SingleChoiceView() {
val radioOptions = listOf(
stringResource(id = R.string.straight),
stringResource(id = R.string.curly),
stringResource(id = R.string.wavy))
val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[2]) }
Column(
Modifier.fillMaxWidth()
) {
radioOptions.forEach { text ->
Row(
Modifier
.fillMaxWidth()
.selectable(
selected = (text == selectedOption),
onClick = {
onOptionSelected(text)
}
)
.padding(vertical = 5.dp)
) {
RadioButton(
selected = (text == selectedOption),
onClick = { onOptionSelected(text) }
)
Text(
text = text
)
}
}
}
}
Current result
You just need to set the verticalAlignment param for the Row.
Row(
Modifier
.fillMaxWidth()
.selectable(
selected = (text == selectedOption),
onClick = {
onOptionSelected(text)
}
)
.padding(vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically // <<<< THIS
) {
...
}
How can I add more elements to the static list in the jetpack compose
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun AddNotesToList(notesList: List<String>) {
val listState = rememberScrollState()
Log.d("TAG", notesList.toString())
LazyColumn() {
items(notesList.size) {
Box(contentAlignment = Alignment.Center,
modifier = Modifier
.padding(start = 15.dp, top = 15.dp, bottom = 1.dp, end = 15.dp)
.fillMaxSize() .horizontalScroll(listState)
.background(Color.White)
.clip(RoundedCornerShape(10.dp)) .padding(15.dp)
.animateItemPlacement(animationSpec = tween(1000))) {
Text(text = notesList[it],
color = Color.Black,
modifier = Modifier.align( Alignment.BottomCenter)
.animateItemPlacement(animationSpec = tween(10000)))
}
}
}
}
this is my addition to the Ui function, this is now I add elements
AddNotesToList(notesList = listOf(
"Drink water",
"Read Books",
"Eat fruits",
"Go for a Walk",
"Drink water",
"Read Books",
"Eat fruits",
"Go for## Heading ## a Walk",
"Go for a Walk",
"Drink water",
"Read Books",
"Eat fruits",
"Go for a Walk"))
now I want to add one more element and I am trying this
function
#Composable
fun AddNewNote(noteDescription: String) {
Log.d("noteDescription", noteDescription)
AddNotesToList(notesList = listOf(noteDescription))
}
Solution:
val _noteList = remember { MutableStateFlow(listOf<String>()) }
val noteList by remember { _noteList }.collectAsState()
// Add note
fun addItem(item: String) {
val newList = ArrayList(noteList)
newList.add(yourItem)
_noteList.value = newList
}
And then you can pass noteList to your LazyColumn
This is how i call my function
AddNewNote { item -> //updating state with added item noteListState = noteListState + listOf(item) }
This is my function
#OptIn(ExperimentalAnimationApi::class)
#Composable
fun AddNewNote(onNewNoteAdded: (String) -> Unit) {
val openDialog = remember { mutableStateOf(true) }
val (visible) = remember { mutableStateOf(true) }
var text by remember { mutableStateOf("") }
AnimatedVisibility(
visible = visible,
enter = slideInVertically(initialOffsetY = { 9000 * it }),
exit = fadeOut()
) {
if (openDialog.value) {
AlertDialog(
onDismissRequest = {
openDialog.value = false
},
title = {
Text(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
initialOffsetY = { 9000 * it },
),
exit = slideOutVertically()
),
text = "Add Note Description"
)
},
text = {
Column() {
TextField(
value = text,
onValueChange = { text = it }
)
Text("Note description")
}
},
buttons = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
val addNoteButtonState by remember { mutableStateOf(false) }
if (addNoteButtonState) {
onNewNoteAdded(text)
} else {
Box(contentAlignment = Alignment.Center) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
if (text != "") {
onNewNoteAdded(text)
}
// addNoteButtonState = true
openDialog.value = false
}
) {
Text(
"Add Note To The List",
)
}
}
}
}
},
)
}
}
}
I'm trying to do a ConstraintLayout in jetpack compose as I am having problems doing too many nested Columns and Rows.
Here is what I have:
#Composable
fun StateAndZipLayout(
modifier: Modifier,
onFormChanged: (FormType, String) -> Unit,
selectedLocation: Address,
stateError: Boolean,
zipError: Boolean
) {
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
val componentWidth = (screenWidth - 48.dp)/2
ConstraintLayout(modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()) {
val rightGuideline = createGuidelineFromStart(0.5f)
val (stateDropDown, shippingField) = createRefs()
StateSelection(
modifier = modifier
.constrainAs(stateDropDown) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
end.linkTo(rightGuideline, margin = 8.dp)
}
.requiredWidth(componentWidth)
.wrapContentHeight(),
onFormChanged = onFormChanged,
selectedLocation = selectedLocation,
label = "State",
error = stateError,
)
ShippingField(
modifier = modifier
.constrainAs(shippingField) {
start.linkTo(rightGuideline, margin = 8.dp)
top.linkTo(stateDropDown.top)
bottom.linkTo(stateDropDown.bottom)
end.linkTo(parent.end)
}
.requiredWidth(componentWidth),
onFormChanged = onFormChanged,
formType = FormType.SHIPPING_ZIP,
label = "Zip",
valueField = selectedLocation.zipCode,
error = zipError
)
}
}
Here is my state selection view:
#Composable
fun StateSelection(
modifier: Modifier,
onFormChanged: (FormType, String) -> Unit,
selectedLocation: Address,
error: Boolean,
label: String
) {
// State variables
val statesMap = AddressUtils.mapOfAmericanStatesToValue
var stateName: String by remember { mutableStateOf(selectedLocation.shippingState) }
var expanded by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
var errorState by remember { mutableStateOf(error) }
Column {
Row(
Modifier
.clickable {
expanded = !expanded
},
) { // Anchor view
TextField(
modifier = Modifier
.fillMaxWidth(),
value = stateName,
onValueChange = {
onFormChanged(FormType.SHIPPING_COUNTRY, it)
},
label = { Text(text = label) },
textStyle = MaterialTheme.typography.subtitle1,
singleLine = true,
trailingIcon = {
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = Icons.Filled.ArrowDropDown,
contentDescription = "",
tint = if (errorState) MaterialTheme.colors.error
else MaterialTheme.colors.onPrimary
)
}
},
keyboardActions = KeyboardActions(onNext = {
focusManager.moveFocus(
FocusDirection.Down
)
}),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Text
),
colors = TextFieldDefaults.textFieldColors(
cursorColor = MaterialTheme.colors.secondary,
textColor = MaterialTheme.colors.onPrimary,
focusedLabelColor = if (errorState) MaterialTheme.colors.error
else MaterialTheme.colors.secondary,
focusedIndicatorColor = if (errorState) MaterialTheme.colors.error
else MaterialTheme.colors.secondary,
backgroundColor = MaterialTheme.colors.secondaryVariant
)
) // state name label
DropdownMenu(expanded = expanded, onDismissRequest = {
expanded = false
}) {
statesMap.asIterable().iterator().forEach {
val (key, value) = it
DropdownMenuItem(
onClick = {
expanded = false
stateName = key
onFormChanged(FormType.SHIPPING_STATE, key)
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = key)
}
}
}
}
if (errorState && error) {
ErrorMessages(modifier = modifier, message = "$label is required")
}
}
}
This is what it looks like, the state drop down and the zip code field are overlapping:
I have started to learn jetpack compose. I want to show bottomsheet in click of IconButton.But i got error #Composable invocations can only happen from the context of a #Composable function how I can implement this logic.
Here is ui screen
Here is code
#RequiresApi(Build.VERSION_CODES.O)
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun AddTaskScreen(navController: NavController) {
var taskTitle by remember { mutableStateOf("") }
val currentDate = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
var taskDescription by remember { mutableStateOf("") }
val taskDuration by remember { mutableStateOf(currentDate.toString()) }
val taskTypes = listOf("Urgent", "Medium", "High")
var expanded by remember { mutableStateOf(false) }
var selectedOptionText by remember { mutableStateOf(taskTypes[0]) }
val clickHandler: () -> Unit = {
DateBottomSheet()
}
Column() {
AppToolBar(title = "AddTaskScreen") {
navController.navigateUp()
}
OutlinedTextField(
value = taskTitle,
label = { Text(text = "Please input task title") },
onValueChange = { text -> taskTitle = text },
modifier = textFieldModifier
)
OutlinedTextField(
value = taskDescription,
label = { Text(text = "Please input task description") },
onValueChange = { text -> taskDescription = text },
modifier = textFieldModifier.height(200.dp)
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
},
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
OutlinedTextField(
readOnly = true,
value = selectedOptionText,
onValueChange = { },
label = { Text("Task priority") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
taskTypes.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
selectedOptionText = selectionOption
expanded = false
}
) {
Text(text = selectionOption)
}
}
}
}
//task time textField
OutlinedTextField(
value = taskDuration,
readOnly = true,
label = { Text(text = "Please select task duration") },
onValueChange = { text -> taskDescription = text },
modifier = textFieldModifier,
trailingIcon = {
IconButton(
onClick = {
clickHandler.invoke()
}) {
Icon(Icons.Filled.DateRange, contentDescription = "")
}
}
)
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun DateBottomSheet() {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberBottomSheetState(
initialValue = BottomSheetValue.Collapsed
)
)
BottomSheetScaffold(
sheetContent = {
Column() {
Text(text = "ThisIsBottomSheet")
Text(text = "ThisIsBottomSheet")
Text(text = "ThisIsBottomSheet")
Text(text = "ThisIsBottomSheet")
Text(text = "ThisIsBottomSheet")
}
},
scaffoldState = bottomSheetScaffoldState
) {
}
But i got error #Composable invocations can only happen from the context of a #Composable function how I can implement this logic
You can simply use mutabelState for handling your button click event to show Bottom Sheet.
You can do following changes ->
#RequiresApi(Build.VERSION_CODES.O)
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun AddTaskScreen(navController: NavController) {
var taskTitle by remember { mutableStateOf("") }
val currentDate = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
var taskDescription by remember { mutableStateOf("") }
val taskDuration by remember { mutableStateOf(currentDate.toString()) }
val taskTypes = listOf("Urgent", "Medium", "High")
var expanded by remember { mutableStateOf(false) }
var selectedOptionText by remember { mutableStateOf(taskTypes[0]) }
var openBottomSheet by rememberSaveable { mutableStateOf(false) }
Column() {
AppToolBar(title = "AddTaskScreen") {
navController.navigateUp()
}
OutlinedTextField(
value = taskTitle,
label = { Text(text = "Please input task title") },
onValueChange = { text -> taskTitle = text },
modifier = textFieldModifier
)
OutlinedTextField(
value = taskDescription,
label = { Text(text = "Please input task description") },
onValueChange = { text -> taskDescription = text },
modifier = textFieldModifier.height(200.dp)
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
},
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
OutlinedTextField(
readOnly = true,
value = selectedOptionText,
onValueChange = { },
label = { Text("Task priority") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
taskTypes.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
selectedOptionText = selectionOption
expanded = false
}
) {
Text(text = selectionOption)
}
}
}
}
//task time textField
OutlinedTextField(
value = taskDuration,
readOnly = true,
label = { Text(text = "Please select task duration") },
onValueChange = { text -> taskDescription = text },
modifier = textFieldModifier,
trailingIcon = {
IconButton(
onClick = {
openBottomSheet = true
}) {
Icon(Icons.Filled.DateRange, contentDescription = "")
}
}
)
if (openBottomSheet) {
DateBottomSheet()
}
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun DateBottomSheet() {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberBottomSheetState(
initialValue = BottomSheetValue.Collapsed
)
)
BottomSheetScaffold(
sheetContent = {
Column() {
Text(text = "ThisIsBottomSheet")
Text(text = "ThisIsBottomSheet")
Text(text = "ThisIsBottomSheet")
Text(text = "ThisIsBottomSheet")
Text(text = "ThisIsBottomSheet")
}
},
scaffoldState = bottomSheetScaffoldState
) {
}
I am trying to make a custom TopBar for my application and I want to have a DropdownMenu displayed in the top right corner of the screen. I have a Box that contains a Row (with some text and icons) and a DropdownMenu which is initially not displayed. When I click on an icon, the DropdownMenu is displayed, but outside the Box, so not where I intended. The code:
#Composable
private fun TopBar {
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.border(1.dp, Color.Black)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Ride history",
maxLines = 1,
fontSize = 25.sp
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
IconButton(
onClick = {}) {
Icon(imageVector = Icons.Filled.Search, contentDescription = null)
}
IconButton(
onClick = { expanded = !expanded }) {
Icon(imageVector = Icons.Filled.Sort, contentDescription = null)
}
IconButton(
onClick = { findNavController().navigate(RideFragmentDirections.actionRideFragmentToSettingsFragment())}) {
Icon(imageVector = Icons.Filled.Settings, contentDescription = null)
}
}
}
DropdownMenu(
modifier = Modifier.align(Alignment.TopEnd),
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = {}) {
Text(text = "BlaBla")
}
DropdownMenuItem(onClick = {}) {
Text(text = "BlaBla")
}
}
}
}
What I obtain:
(I put a border around the Box to see its bounds)
After I press the Sort button, the DropdownMenu appears, but it is placed outside the Box. I want it to be placed in the top right corner, over everything. What am I missing?
Update
#Composable
private fun TopBar() {
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.border(1.dp, Color.Black)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Ride history",
maxLines = 1,
fontSize = 25.sp
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
IconButton(
onClick = {}
) {
Icon(imageVector = Icons.Filled.Search, contentDescription = null)
}
IconButton(
onClick = { expanded = !expanded }
) {
Icon(imageVector = Icons.Filled.Sort, contentDescription = null)
}
IconButton(
onClick = {}
) {
Icon(imageVector = Icons.Filled.Settings, contentDescription = null)
}
}
}
Box(
modifier = Modifier.fillMaxHeight().align(Alignment.TopEnd),
contentAlignment = Alignment.TopEnd
) {
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = {}) {
Text(text = "BlaBla")
}
DropdownMenuItem(onClick = {}) {
Text(text = "BlaBla")
}
}
}
}
}
This yields:
According to Material guidelines the dropdown menu should be placed below the element.
To get this layout in Compose with DropdownMenu, you need to put it in a Box with the calling button, like this:
Box {
IconButton(
onClick = { expanded = !expanded }
) {
Icon(imageVector = Icons.Filled.Sort, contentDescription = null)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = {}) {
Text(text = "BlaBla")
}
DropdownMenuItem(onClick = {}) {
Text(text = "BlaBla")
}
}
}
Output where Sort is the calling button: