I want to implement the MaterialButtonToggleGroup in Jetpack Compose. This component looks like this (image taken from here):
So far, I got the following result:
Note, that the vertical grey border next to the vertical blue border are present. In the original, either the colored border or the grey color are present at a time. To make it more clear, have a look at this image with extra thick borders:
How can I achieve that the vertical borders between two buttons are not present? My current code looks like this:
val cornerRadius = 8.dp
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Spacer(modifier = Modifier.weight(1f))
items.forEachIndexed { index, item ->
OutlinedButton(
onClick = { indexChanged(index) },
shape = when (index) {
// left outer button
0 -> RoundedCornerShape(topStart = cornerRadius, topEnd = 0.dp, bottomStart = cornerRadius, bottomEnd = 0.dp)
// right outer button
items.size - 1 -> RoundedCornerShape(topStart = 0.dp, topEnd = cornerRadius, bottomStart = 0.dp, bottomEnd = cornerRadius)
// middle button
else -> RoundedCornerShape(topStart = 0.dp, topEnd = 0.dp, bottomStart = 0.dp, bottomEnd = 0.dp)
},
border = BorderStroke(1.dp, if(selectedIndex == index) { MaterialTheme.colors.primary } else { Color.DarkGray.copy(alpha = 0.75f)}),
colors = if(selectedIndex == index) {
// selected colors
ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.1f), contentColor = MaterialTheme.colors.primary)
} else {
// not selected colors
ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.surface, contentColor = MaterialTheme.colors.primary)
},
) {
Text(
text = "Some text",
color = if(selectedIndex == index) { MaterialTheme.colors.primary } else { Color.DarkGray.copy(alpha = 0.9f) },
modifier = Modifier.padding(horizontal = 8.dp)
)
}
}
Spacer(modifier = Modifier.weight(1f))
}
In the MaterialButtonToggleGroup to prevent a double-width stroke there is a negative marginStart on all except the first child drawing the adjacent strokes directly on top of each other.
Using the same solution:
OutlinedButton(
modifier = when (index) {
0 ->
Modifier
.offset(0.dp, 0.dp)
.zIndex(if (selectedIndex == index) 1f else 0f)
else ->
Modifier
.offset((-1 * index).dp, 0.dp)
.zIndex(if (selectedIndex == index) 1f else 0f)
},
// Your code here
Related
I have lazy list and a floating action button in the bottom end, the FAB has a color. But the floating action button in jetpack compose is showing the list in the background even thought it has a color. Even after adding the elevation it is not working. How to solve it?
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
) {
androidx.compose.material3.FloatingActionButton(
containerColor = appColors.primary,
contentColor = appColors.primary,
elevation = FloatingActionButtonDefaults.elevation(8.dp),
shape = CircleShape,
onClick = {},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
) {
Icon(Icons.Filled.Add, null, tint = Color.White)
}
if (viewModel.state.value.patientsList.isEmpty()) {
Text(
text = "No Patients Available",
style = FontLibrary.regularBody16(textAlign = TextAlign.Center)
)
} else {
LazyColumn(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp)
.fillMaxSize()
) {
items(
items = viewModel.state.value.patientsList,
key = { patient ->
patient.id
}
) { patient ->
PatientCard(patient)
Divider(color = appColors.secondary, thickness = 1.dp)
}
}
}
}
In Jetpack compose UI components are placed in the order as you place them, so in box you have first placed Floating action button, and then the list.
You should place FAB in the end, i.e., move its code to the bottom.
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
) {
if (viewModel.state.value.patientsList.isEmpty()) {
Text(
text = "No Patients Available",
style = FontLibrary.regularBody16(textAlign = TextAlign.Center)
)
} else {
LazyColumn(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp)
.fillMaxSize()
) {
items(
items = viewModel.state.value.patientsList,
key = { patient ->
patient.id
}
) { patient ->
PatientCard(patient)
Divider(color = appColors.secondary, thickness = 1.dp)
}
}
}
androidx.compose.material3.FloatingActionButton(
containerColor = appColors.primary,
contentColor = appColors.primary,
shape = CircleShape,
onClick = {},
modifier = Modifier
.align(BottomEnd)
.padding(16.dp),
) {
Icon(Icons.Filled.Add, null, tint = Color.White)
}
}
I have a Column with some rows, and I want to align the last row at the botton, but this row is never located at the bottom of the screen, it stays right after the previous row:
Column {
// RED BOX
Row(
modifier = Modifier
.fillMaxWidth()
.height(130.dp)
.padding(vertical = 15.dp, horizontal = 30.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = stringResource(id = R.string.app_name),
style = TextStyle(fontSize = 40.sp),
color = Color.White
)
Text(
text = stringResource(id = R.string.app_description),
style = TextStyle(fontSize = 13.sp),
fontWeight = FontWeight.Bold,
color = Color.Black
)
}
}
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(15.dp)
)
// GREEN BOX
val currentRoute = currentRoute(navController)
items.forEach { item ->
DrawerItem(item = item, selected = currentRoute == item.route) {
navController.navigate(item.route) {
launchSingleTop = true
}
scope.launch {
scaffoldState.drawerState.close()
}
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 15.dp, horizontal = 30.dp),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.Center
) {
Text(
text = BuildConfig.VERSION_NAME,
style = TextStyle(fontSize = 11.sp),
color = Color.Black,
)
}
}
I want to get the same as I show in the picture. I want to have the first row (red), then the second row (green) and then a third row that fits at the bottom of the screen (blue)
You can do it in many ways.
You can use a Column with verticalArrangement = Arrangement.SpaceBetween assigning a weight(1f,false) to the last row:
Column(
Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween) {
//All elements
Column {
// RED BOX
//...
Spacer(
modifier = Modifier
.fillMaxWidth()
.background(Green)
.height(15.dp)
)
//... Green box
}
//LAST ROW
Row(
modifier = Modifier
.weight(1f, false)
) {
//...
}
}
You can use a Spacer(modifier.weight(1f)) between GreenBox and Blue Box to create space between them or you can create your custom column with Layout function and set y position of last Placeable as height of Composable - height of last Composble
Column(modifier = Modifier
.fillMaxHeight()
.background(Color.LightGray)) {
Text(
"First Text",
modifier = Modifier
.background(Color(0xffF44336)),
color = Color.White
)
Text(
"Second Text",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
Spacer(modifier = Modifier.weight(1f))
Text(
"Third Text",
modifier = Modifier
.background(Color(0xff2196F3)),
color = Color.White
)
}
Result:
Custom Layout
#Composable
private fun CustomColumn(
modifier: Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val looseConstraints = constraints.copy(
minWidth = 0,
maxWidth = constraints.maxWidth,
minHeight = 0,
maxHeight = constraints.maxHeight
)
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each child
measurable.measure(looseConstraints)
}
// Track the y co-ord we have placed children up to
var yPosition = 0
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Place children in the parent layout
placeables.forEachIndexed { index, placeable ->
println("Placeable width: ${placeable.width}, measuredWidth: ${placeable.measuredWidth}")
// Position item on the screen
if (index == placeables.size - 1) {
placeable.placeRelative(x = 0, y = constraints.maxHeight - placeable.height)
} else {
placeable.placeRelative(x = 0, y = yPosition)
}
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
Usage
CustomColumn(
modifier = Modifier
.fillMaxHeight()
.background(Color.LightGray)
) {
Text(
"First Text",
modifier = Modifier
.background(Color(0xffF44336)),
color = Color.White
)
Text(
"Second Text",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
Spacer(modifier = Modifier.weight(1f))
Text(
"Third Text",
modifier = Modifier
.background(Color(0xff2196F3)),
color = Color.White
)
}
Result:
In this example with Layout you should consider how you measure your measureables with Constraints and your total width and height. It requires a little bit practice but you get more unique designs and with less work(more optimised) composables than ready ones. Here i set layout as maxWidth so no matter which width you assign it takes whole width. It's for demonstration you can set max width or height in layout based on your needs.
I'm trying to handle Horizontal Pager but I'm having a problem with additional padding from the top. It only happens when I add HorizontalPager() to my layout.
As you can see in the attachment I have grey indicators which I want to put below my pager but when I do that Pager goes lower and indicators become invisible. It looks like HorizontalPager() has some top padding (?)
Box(
modifier = Modifier
.fillMaxWidth()
.height(350.dp)
.clip(RoundedCornerShape(0.dp, 0.dp, 12.dp, 12.dp))
.background(MaterialTheme.colors.onBackground)
) {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(10.dp)
) {
val (backButton, favouriteButton, photosPager) = createRefs()
BackNavigationSingleButton(
backButtonSelected = { }, modifier = Modifier
.height(42.dp)
.width(60.dp)
.clip(RoundedCornerShape(10.dp))
.background(Color.Red)
.constrainAs(backButton) {
top.linkTo(parent.top)
start.linkTo(parent.start)
}
)
IconButton(onClick = {}, modifier = Modifier
.size(42.dp)
.constrainAs(favouriteButton) {
top.linkTo(parent.top)
end.linkTo(parent.end)
}) {
Icon(
imageVector = Icons.Outlined.Favorite
)
}
Column(
modifier = Modifier
.constrainAs(photosPager) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
}) {
val pagerState = rememberPagerState()
val items = 3
HorizontalPager(
count = items,
state = pagerState,
contentPadding = PaddingValues(0.dp)
) { page ->
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green)
)
}
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
for (i in 1..items) {
Canvas(modifier = Modifier.size(20.dp)) {
drawCircle(
color = Color.Gray,
center = Offset(x = 10f, y = 10f),
radius = size.minDimension / 4,
)
}
}
}
}
}
}
Each page inside HorizontalPager is using Modifier.fillParentMaxSize, which makes it fill all available height, same what Modifier.fillMaxHeight does.
When Column has an item with Modifier.fillMaxHeight, this item will push all next items. To prevent this you can use Modifier.weight: in this case height of this view will be calculated after all other Column children.
Also most of time ConstraintLayout is redundant in Compose, here's how you can build same layout without it:
Box(
modifier = Modifier
.fillMaxWidth()
.height(350.dp)
.clip(RoundedCornerShape(0.dp, 0.dp, 12.dp, 12.dp))
.background(MaterialTheme.colors.onBackground.copy(0.5f))
.padding(10.dp)
) {
Row {
TextButton(
onClick = { },
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.background(Color.Red)
) {
Text("button")
}
Spacer(Modifier.weight(1f))
IconButton(
onClick = {},
modifier = Modifier
.size(42.dp)
) {
Icon(
imageVector = Icons.Outlined.Favorite,
contentDescription = null
)
}
}
Column {
val pagerState = rememberPagerState()
val items = 3
HorizontalPager(
count = items,
state = pagerState,
contentPadding = PaddingValues(0.dp),
modifier = Modifier.weight(1f)
) { page ->
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green)
)
}
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
for (i in 1..items) {
Canvas(modifier = Modifier.size(20.dp)) {
drawCircle(
color = Color.Gray,
center = Offset(x = 10f, y = 10f),
radius = size.minDimension / 4,
)
}
}
}
}
}
Result:
I am building a chat app with firebase and I need to align the chat bubbles in the end when I write the message and in the start when I receive, like in whatsapp. If I use the horizontalArrangement in the lazyColumn it affects all the items. I tried using the modifier.align in the chat bubbles but nothing happens. How can I do this?
below is my lazyColumn
LazyColumn(
modifier = Modifier.fillMaxWidth(),
) {
if (list != null && list.isNotEmpty()) {
items(items = list) {
if (it.user1id == args.userId) {
ChatCard(
message = it,
color = Color.Magenta,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(
start = 32.dp,
end = 4.dp,
top = 4.dp
)
)
} else {
ChatCard(
message = it,
color = Color.White,
Modifier.padding(
start = 4.dp,
end = 32.dp,
top = 4.dp
)
)
}
}
}
}
#Composable
fun ChatCard(message: Message, color: Color, modifier: Modifier = Modifier){
Card(
modifier = modifier,
backgroundColor = color,
shape = RoundedCornerShape(10.dp)
) {
Row(
modifier = Modifier.padding(4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
modifier = Modifier
.padding(4.dp)
.widthIn(0.dp, 280.dp),
text = message.message
)
Text(
modifier = Modifier.padding(4.dp),
text = message.time,
style = TextStyle(
fontSize = 12.sp,
color = Color.LightGray
)
)
}
}
}
You can add a Row for each item applying a different horizontalArrangement removing the Modifier.align in your Card.
Something like:
items(items = itemsList) {
Row(Modifier.fillMaxWidth(),
horizontalArrangement = if (it == ....)
Arrangement.Start else
Arrangement.End) {
if (it == ....) {
ChatCard(
color = Color.Magenta,
modifier = Modifier
.padding(
start = 32.dp,
end = 4.dp,
top = 4.dp
)
)
} else {
ChatCard(
color = Color.White,
Modifier.padding(
start = 4.dp,
end = 32.dp,
top = 4.dp
)
)
}
}
I need to implement LazyColumn with top fading edge effect. On Android I use fade gradient for ListView or RecyclerView, but couldn't find any solution for Jetpack Compose!
I tried to modify canvas:
#Composable
fun Screen() {
Box(
Modifier
.fillMaxWidth()
.background(color = Color.Yellow)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.drawWithContent {
val colors = listOf(Color.Transparent, Color.Black)
drawContent()
drawRect(
brush = Brush.verticalGradient(colors),
blendMode = BlendMode.DstIn
)
}
) {
itemsIndexed((1..1000).toList()) { item, index ->
Text(
text = "Item $item: $index value",
modifier = Modifier.padding(12.dp),
color = Color.Red,
fontSize = 24.sp
)
}
}
}
}
But have wrong result:
What you could do is place a Spacer on top of the list, and draw a gradient on that Box. Make the Box small so only a small portion of the list has the overlay. Make the color the same as the background of the screen, and it will look like the content is fading.
val screenBackgroundColor = MaterialTheme.colors.background
Box(Modifier.fillMaxSize()) {
LazyColumn(Modifier.fillMaxSize()) {
//your items
}
//Gradient overlay
Spacer(
Modifier
.fillMaxWidth()
.height(32.dp)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
screenBackgroundColor
)
)
)
//.align(Alignment) to control the position of the overlay
)
}
Here's how it would look like:
However, this doesn't seem like quite what you asked for since it seems like you want the actual list content to fade out.
I don't know how you would apply an alpha to only a portion of a view. Perhaps try to dig into the .alpha sources to figure out.
Quick hack which fixes the issue: add .graphicsLayer { alpha = 0.99f } to your modifer
By default Jetpack Compose disables alpha compositing for performance reasons (as explained here; see the "Custom Modifier" section). Without alpha compositing, blend modes which affect transparency (e.g. DstIn) don't have the desired effect. Currently the best workaround is to add .graphicsLayer { alpha = 0.99F } to the modifier on the LazyColumn; this forces Jetpack Compose to enable alpha compositing by making the LazyColumn imperceptibly transparent.
With this change, your code looks like this:
#Composable
fun Screen() {
Box(
Modifier
.fillMaxWidth()
.background(color = Color.Yellow)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
// Workaround to enable alpha compositing
.graphicsLayer { alpha = 0.99F }
.drawWithContent {
val colors = listOf(Color.Transparent, Color.Black)
drawContent()
drawRect(
brush = Brush.verticalGradient(colors),
blendMode = BlendMode.DstIn
)
}
) {
itemsIndexed((1..1000).toList()) { item, index ->
Text(
text = "Item $item: $index value",
modifier = Modifier.padding(12.dp),
color = Color.Red,
fontSize = 24.sp
)
}
}
}
}
which produces the correct result
Just a little nudge in the right direction. What this piece of code does is place a Box composable at the top of your LazyColumn with an alpha modifier for fading. You can make multiple of these Box composables in a Column again to create a smoother effect.
#Composable
fun FadingExample() {
Box(
Modifier
.fillMaxWidth()
.requiredHeight(500.dp)) {
LazyColumn(Modifier.fillMaxSize()) {
}
Box(
Modifier
.fillMaxWidth()
.height(10.dp)
.alpha(0.5f)
.background(Color.Transparent)
.align(Alignment.TopCenter)
) {
}
}
}
I optimised the #user3872620 solution. You have just to put this lines below your LazyColumn, VerticalPager.. and just adapt your offset / height, usually offset = height
Box(
Modifier
.fillMaxWidth()
.offset(y= (-10).dp)
.height(10.dp)
.background(brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
MaterialTheme.colors.background
)
))
)
You will got this render:
There is the render
This is a very simple implementation of FadingEdgeLazyColumn using AndroidView. Place AndroidView with gradient background applied to the top and bottom of LazyColumn.
#Stable
object GradientDefaults {
#Stable
val Color = androidx.compose.ui.graphics.Color.Black
#Stable
val Height = 30.dp
}
#Stable
sealed class Gradient {
#Immutable
data class Top(
val color: Color = GradientDefaults.Color,
val height: Dp = GradientDefaults.Height,
) : Gradient()
#Immutable
data class Bottom(
val color: Color = GradientDefaults.Color,
val height: Dp = GradientDefaults.Height,
) : Gradient()
}
#Composable
fun FadingEdgeLazyColumn(
modifier: Modifier = Modifier,
gradients: Set<Gradient> = setOf(Gradient.Top(), Gradient.Bottom()),
contentGap: Dp = 0.dp,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit,
) {
val topGradient =
remember(gradients) { gradients.find { it is Gradient.Top } as? Gradient.Top }
val bottomGradient =
remember(gradients) { gradients.find { it is Gradient.Bottom } as? Gradient.Bottom }
ConstraintLayout(modifier = modifier) {
val (topGradientRef, lazyColumnRef, bottomGradientRef) = createRefs()
GradientView(
modifier = Modifier
.constrainAs(topGradientRef) {
top.linkTo(parent.top)
width = Dimension.matchParent
height = Dimension.value(topGradient?.height ?: GradientDefaults.Height)
}
.zIndex(2f),
colors = intArrayOf(
(topGradient?.color ?: GradientDefaults.Color).toArgb(),
Color.Transparent.toArgb()
),
visible = topGradient != null
)
LazyColumn(
modifier = Modifier
.constrainAs(lazyColumnRef) {
top.linkTo(
anchor = topGradientRef.top,
margin = when (topGradient != null) {
true -> contentGap
else -> 0.dp
}
)
bottom.linkTo(
anchor = bottomGradientRef.bottom,
margin = when (bottomGradient != null) {
true -> contentGap
else -> 0.dp
}
)
width = Dimension.matchParent
height = Dimension.fillToConstraints
}
.zIndex(1f),
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
content = content
)
GradientView(
modifier = Modifier
.constrainAs(bottomGradientRef) {
bottom.linkTo(parent.bottom)
width = Dimension.matchParent
height = Dimension.value(bottomGradient?.height ?: GradientDefaults.Height)
}
.zIndex(2f),
colors = intArrayOf(
Color.Transparent.toArgb(),
(bottomGradient?.color ?: GradientDefaults.Color).toArgb(),
),
visible = bottomGradient != null
)
}
}
#Composable
private fun GradientView(
modifier: Modifier = Modifier,
#Size(value = 2) colors: IntArray,
visible: Boolean = true,
) {
AndroidView(
modifier = modifier,
factory = { context ->
val gradientBackground = GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
colors
).apply {
cornerRadius = 0f
}
View(context).apply {
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT
)
background = gradientBackground
visibility = when (visible) {
true -> View.VISIBLE
else -> View.INVISIBLE
}
}
}
)
}