I need to reveal some floating action buttons at the bottom of the screen when some other buttons, in a scrollable list, are no longer visible on the screen. Naturally, I feel like using LazyColumn, AnimatedVisibility and Scaffold's bottom bar for this (I need the snackbar and the screen's content be above the floating action buttons). But the animation doesn't work as expected. As the floating buttons appear/dissapear, the bottom bar's height is not animated (but I think it could actually be that the content padding isn't animated).
The result I'm getting:
The code:
val lazyListState = rememberLazyListState()
val isFloatingButtonVisible by derivedStateOf {
lazyListState.firstVisibleItemIndex >= 2
}
Scaffold(
bottomBar = {
AnimatedVisibility(
visible = isFloatingButtonVisible,
enter = slideInVertically { height -> height },
exit = slideOutVertically { height -> height }
) {
Row(
modifier = Modifier.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
repeat(2) {
Button(
modifier = Modifier
.height(48.dp)
.weight(1f),
onClick = {}
) {
Text(text = "Something")
}
}
}
}
}
) { contentPadding ->
LazyColumn(
modifier = Modifier.padding(contentPadding),
state = lazyListState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Text(
text = LoremIpsum(50).values.joinToString(" ")
)
}
item {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
repeat(2) {
Button(onClick = {}) {
Text(text = "Bla Bla")
}
}
}
}
item {
Text(
text = LoremIpsum(700).values.joinToString(" ")
)
}
}
}
I don't think it's possible to do this with AnimatedVisibility. I got it like this
#Composable
fun Test() {
val lazyListState = rememberLazyListState()
val isFloatingButtonVisible by remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex >= 1
}
}
var barHeight by remember { mutableStateOf(0.dp) }
var listSizeDelta by remember { mutableStateOf(0.dp) }
val bottomBarOffset = remember { Animatable(barHeight, Dp.VectorConverter) }
LaunchedEffect(isFloatingButtonVisible) {
if (isFloatingButtonVisible) {
bottomBarOffset.animateTo(barHeight)
listSizeDelta = barHeight
} else {
listSizeDelta = 0.dp
bottomBarOffset.animateTo(0.dp)
}
}
val density = LocalDensity.current
BoxWithConstraints(Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier
.size(width = maxWidth, height = maxHeight - listSizeDelta)
.background(Color.Yellow),
state = lazyListState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
repeat(15) {
item {
Text(
text = LoremIpsum(50).values.joinToString(" ")
)
}
}
}
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.graphicsLayer {
translationY =
density.run { (-bottomBarOffset.value + barHeight).toPx() }
}
.background(Color.Green)
.onSizeChanged {
barHeight = density.run { it.height.toDp()}
}
.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
repeat(2) {
Button(
modifier = Modifier
.height(48.dp)
.weight(1f),
onClick = {}
) {
Text(text = "Something")
}
}
}
}
}
The content padding in lazycolumn modifier breaks the animation;
LazyColumn(
modifier = Modifier.padding(contentPadding) // -> Delete this.
)
For the same view;
Row(
modifier = Modifier
.background(Color.White) //-> Add this.
.padding(8.dp)
)
Got the right result using AnimatedContent instead of AnimatedVisibility.
val lazyListState = rememberLazyListState()
val isFloatingButtonVisible by derivedStateOf {
lazyListState.firstVisibleItemIndex >= 2
}
Scaffold(
bottomBar = {
AnimatedContent(
targetState = isFloatingButtonVisible,
transitionSpec = {
slideInVertically { height -> height } with
slideOutVertically { height -> height }
}
) { isVisible ->
if (isVisible) {
Row(
modifier = Modifier
.border(1.dp, Color.Red)
.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
repeat(2) {
Button(
modifier = Modifier
.height(48.dp)
.weight(1f),
onClick = {}
) {
Text(text = "Something")
}
}
}
} else {
Box(modifier = Modifier.fillMaxWidth())
}
}
}
) { contentPadding ->
LazyColumn(
modifier = Modifier.padding(contentPadding),
state = lazyListState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Text(
text = LoremIpsum(50).values.joinToString(" ")
)
}
item {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
repeat(2) {
Button(onClick = {}) {
Text(text = "Bla Bla")
}
}
}
}
item {
Text(
text = LoremIpsum(700).values.joinToString(" ")
)
}
}
}
Related
My scrollable column moves button if text is too long. I want to keep my green button at the bottom of the screen.
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween
) {
Column {
Spacer(modifier = Modifier.height(Padding.contentBig))
Column(modifier = Modifier.verticalScroll(rememberScrollState()).fillMaxSize().padding(bottom = 60.dp)) {
title.Widget()
Spacer(modifier = Modifier.height(Padding.contentBig))
description.Widget()
}
}
Row(modifier = Modifier.weight(1f, false)) {
button.Widget(
onClick = { onClickListener?.invoke(it) }
)
}
}
You can apply the weight modifier to the 1st nested Column:
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f, true)) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
){
//....
}
}
Row() {
Button(
onClick = { }
){}
}
}
You can use a Scaffold with a bottomBar:
val scaffoldState = rememberScaffoldState()
val scrollState = rememberScrollState()
Scaffold(
backgroundColor = Color.White,
scaffoldState = scaffoldState,
modifier = Modifier.fillMaxSize(),
bottomBar = {
Button(onClick = onClickListener) { // <-- your button
}
},
content = { paddingValues ->
val padding = paddingValues
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween
) {
Column {
Spacer(modifier = Modifier.height(Padding.contentBig))
Column(modifier = Modifier.verticalScroll(rememberScrollState()).fillMaxSize().padding(bottom = 60.dp)) {
title.Widget()
Spacer(modifier = Modifier.height(Padding.contentBig))
description.Widget()
}
}
}
}
)
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
) {
...
}
My problem is, that i can't manage to navigate to another Screen inside a foreach-loop altough it's possible in another Screen to route to the wanted Screen, but not in a foreach loop.
When i click on the button, it says, that he can't find the article with the id, but i checked and the id, which he says he can't find does exist
This is my Code (which produces the error):
Surface(
modifier = Modifier
.fillMaxSize()
)
{
Box()
{
shopping.forEach {
Button(
modifier = Modifier
.offset(it.coordinates.x.dp, it.coordinates.y.dp)
.size(10.dp),
onClick = { navController.navigate(
Screen.ArtikelDetailScreen.route
+ "/${it.id}" })
)
{
Text(
modifier = Modifier
.padding(
top = ProjectTheme.paddings.tiny,
bottom = ProjectTheme.paddings.tiny
),
style = ProjectTheme.typography.h6,
text = it.name
)
}
}
}
}
This is the code to which it should route:
#Composable
fun ArtikelDetailScreen(
id: Long,
navController: NavController,
viewModel: ArtikelViewModel = hiltViewModel()
) {
val artikel = viewModel.shoppingState.value.find { p -> p.id == id }
if(artikel == null) {
Log.e(tag, "Artikel mit der id = $id wurde nicht gefunden")
return
}
viewModel.artikel = artikel
viewModel.setState()
val scrollState = rememberScrollState()
ProjectTheme {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.ArtDetailScreenName))
}
)
}
) {
Column(
modifier = Modifier
.wrapContentWidth()
.padding(all = ProjectTheme.paddings.small)
.verticalScroll(
state = scrollState,
enabled = true,
reverseScrolling = true
)
) {
ArtikelInfos(viewModel)
Button(
modifier = Modifier
.fillMaxWidth()
.padding(top = ProjectTheme.paddings.default),
onClick = {
viewModel.remove(id)
viewModel.add()
navController.navigate(
route = Screen.ShoppingListScreen.route)
{
popUpTo(
route = Screen.ShoppingListScreen.route
)
{
inclusive = true
}
}
}
) {
Text(
modifier = Modifier
.padding(
top = ProjectTheme.paddings.tiny,
bottom = ProjectTheme.paddings.tiny
),
style = ProjectTheme.typography.h6,
text = stringResource(R.string.back)
)
}
}
}
}
}
And this is where it works:
LazyColumn {
items(
viewModel.shoppingState.value
)
{
artikel ->
var artikelRemoved: Artikel?
val dismissState = rememberDismissState(
confirmStateChange =
{
if (it == DismissValue.DismissedToEnd) {
logThread("==>SwipeToDismiss().", "-> Edit")
navController.navigate(
Screen.ArtikelDetailScreen.route
+ "/${artikel.id}"
)
return#rememberDismissState true
}
else if (it == DismissValue.DismissedToStart)
{
logThread("==>SwipeToDismiss().", "-> Delete")
artikelRemoved = artikel
viewModel.remove(artikel.id)
navController.navigate(
Screen.ShoppingListScreen.route
)
return#rememberDismissState true
}
return#rememberDismissState false
}
)
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 4.dp),
directions = setOf(
DismissDirection.StartToEnd,
DismissDirection.EndToStart
),
dismissThresholds =
{
direction ->
FractionalThreshold(
if (direction == DismissDirection.StartToEnd)
{
0.25f
}
else
{
0.25f
}
)
},
background =
{
val direction =
dismissState.dismissDirection ?: return#SwipeToDismiss
val colorBox by animateColorAsState(
when (dismissState.targetValue)
{
DismissValue.Default -> ProjectTheme.colors.secondary
DismissValue.DismissedToEnd -> Color.Green
DismissValue.DismissedToStart -> Color.Red
}
)
val colorIcon by animateColorAsState(
when (dismissState.targetValue)
{
DismissValue.Default -> Color.Black
DismissValue.DismissedToEnd -> Color.DarkGray
DismissValue.DismissedToStart -> Color.White
}
)
val alignment = when (direction)
{
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction)
{
DismissDirection.StartToEnd -> Icons.Default.Edit
DismissDirection.EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (dismissState.targetValue == DismissValue.Default)
{
1.25f
}
else
{
2.0f
}
)
Box(
Modifier
.fillMaxSize()
.background(colorBox)
.padding(horizontal = 20.dp),
contentAlignment = alignment
) {
Icon(
icon,
contentDescription = "Localized description",
modifier = Modifier
.scale(scale),
tint = colorIcon
)
}
},
dismissContent = {
Column {
ArtikelCard(
artikel = artikel,
elevation = animateDpAsState(
if (dismissState.dismissDirection != null)
{
8.dp
}
else
{
0.dp
}
).value,
) {
// onClick
// THIS WORKS navController.navigate(
Screen.ArtikelDetailScreen.route
+ "/${artikel.id}"
)
}
Divider()
}
}
)
}
}
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:
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