Goal
I want to fit as many items as possible to available space and add "+{num}" badge to the end to indicate if there are more items left. Something like below image.
Problem
Since number and size of the items (i.e chips in this case) and also available space are not known beforehand it is difficult to know exactly how many chips would fit. Additionally, compose measures children only once and lays out right away.
What I've tried
I tried the following approach but it is not quite there yet.
#Composable
fun MainScreen() {
Column {
val states = arrayOf(
"NY", "CA", "NV", "PA", "AZ", "AK", "NE", "CT", "CO", "FL", "IL", "KS", "WA"
)
var chipCount by remember { mutableStateOf(0) }
Row(
modifier = Modifier
.padding(horizontal = 16.dp)
.wrapContentHeight(),
verticalAlignment = Alignment.CenterVertically
) {
ChipRow(
modifier = Modifier
.padding(end = 4.dp)
.weight(1f, fill = false),
onPlacementComplete = { chipCount = it }
) {
for (state in states) {
Chip(
modifier = Modifier
.padding(4.dp)
.wrapContentSize(), text = state
)
}
}
Text(text = "+${states.size - chipCount}", style = MaterialTheme.typography.h6)
}
}
}
#Composable
fun Chip(
text: String,
modifier: Modifier = Modifier
) {
Surface(
color = Color.LightGray,
shape = CircleShape,
modifier = modifier
) {
Text(
text = text,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.body1,
modifier = Modifier.padding(6.dp)
)
}
}
#Composable
fun ChipRow(
modifier: Modifier = Modifier,
onPlacementComplete: (Int) -> Unit,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) };
var counter = 0
layout(constraints.maxWidth, constraints.maxHeight) {
var xPosition = 0
for (placeable in placeables) {
if (xPosition + placeable.width > constraints.maxWidth) break
placeable.placeRelative(x = xPosition, 0)
xPosition += placeable.width
counter++
}
onPlacementComplete(counter)
}
}
}
First of all it looks hacky rather than a legit solution. Secondly, it doesn't work as intended. The output is like below.
Request
I went through official guides for custom layouts and did many google searches. But couldn't come up with anything better. Could you help with this situation? Reference links, pointing errors, etc., anything is appreciated.
Thanks in advance!
You've passed your max constraint values (layout(constraints.maxWidth, constraints.maxHeight)), that's why your view took whole screen.
You need to pass the needed view size instead, for example like this:
data class Item(val placeable: Placeable, val xPosition: Int)
val items = mutableListOf<Item>()
var xPosition = 0
for (placeable in placeables) {
if (xPosition + placeable.width > constraints.maxWidth) break
items.add(Item(placeable, xPosition))
xPosition += placeable.width
}
layout(
width = items.last().let { it.xPosition + it.placeable.width },
height = items.maxOf { it.placeable.height }
) {
items.forEach {
it.placeable.place(it.xPosition, 0)
}
onPlacementComplete(items.count())
}
Result:
i am not too sure either but i think you need to state the column and row width first to make it centervertically.
and i dont think u need 1f in weight, instead 0.8f would be better i believe.
Column (
modifier = Modifier.fillMaxWidth()
){
val states = arrayOf(
"NY", "CA", "NV", "PA", "AZ", "AK", "NE", "CT", "CO", "FL", "IL", "KS", "WA"
)
var chipCount by remember { mutableStateOf(0) }
Row(
modifier = Modifier
.padding(horizontal = 16.dp)
.wrapContentHeight()
.modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
ChipRow(
modifier = Modifier
.padding(end = 4.dp)
.weight(0.8f, fill = false),
onPlacementComplete = { chipCount = it }
) {
for (state in states) {
Chip(
modifier = Modifier
.padding(4.dp)
.wrapContentSize(), text = state
)
}
}
Text(text = "+${states.size - chipCount}", style = MaterialTheme.typography.h6)
}
}
or i would prefer you use constraintlayout instead. here
Related
I want to achieve the following layout
All the headers like Claim requested credentials, Claim received credentials, Pending requests etc are dynamic and will be coming from backend and the items for each header like Module 1:... etc are dynamic as well. Also I need the card to encapsulate both Header and the Body item.
Here is what I have tried
LazyColumn(
modifier = Modifier
.offset(x = 0.dp, y = 110.dp)
.fillMaxSize()
.background(
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
color = MaterialTheme.colors.primaryVariant
)
.padding(12.dp)
) {
itemsIndexed(
listOf(
"Claim requested credentials",
"Claim received credentials",
"Pending Requests"
)
) { index, item ->
Card(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.background(
color = MaterialTheme.colors.primaryVariant,
shape = RoundedCornerShape(10.dp)
)
,elevation = 20.dp,
contentColor = MaterialTheme.colors.primaryVariant,
backgroundColor = MaterialTheme.colors.primaryVariant
) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 12.dp)) {
ManageCredentialHeader(text = item, vcsNo = 2)
LazyColumn(){
itemsIndexed(listOf(1,2,3)){ index, item ->
ManageCredentialItem()
}
}
}
}
}
}
But I get an error saying java.lang.IllegalStateException: Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()). If you want to add a header before the list of items please add a header as a separate item() before the main items() inside the LazyColumn scope. There are could be other reasons for this to happen: your ComposeView was added into a LinearLayout with some weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a custom layout. Please try to remove the source of infinite constraints in the hierarchy above the scrolling container.
How to achieve this kind of dynamic layout with LazyColumn in compose. In react-native this was easily achieved using SectionList.
In Jetpack Compose when you create layouts like LazyColumn or any other layout you measure your Composables as Measurables with the limits coming from Constraints as min/max width/height. Modifier.scroll or LazyList returns max as Constraints.Infinity which is not permitted for child LazyColumn. That's why you get that exception.
In this answer i explained which modifier returns which Constraints. You can check out Constraints section.
Since you don't have a height for the Column add a size modifier that changes maxHeight of inner LazyColumn from Constraints.Infinity to something finite such as
Column(
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 12.dp)
.heightIn(max = 500.dp) {
}
max height is not the fixed value set, it's the biggest height that child LazyColumns can get. You can change it as required. Your items can grow in height up to this.
Implementation
#Composable
private fun LazyListSample() {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
color = Color(0xffE3F2FD)
)
.padding(12.dp),
contentPadding = PaddingValues(vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
itemsIndexed(
listOf(
"Claim requested credentials",
"Claim received credentials",
"Pending Requests"
)
) { index, item ->
Card(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding(),
elevation = 20.dp,
shape = RoundedCornerShape(10.dp),
) {
Column(
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 12.dp)
.heightIn(max = 500.dp)
) {
ManageCredentialHeader(text = item, vcsNo = 2)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(listOf(1, 2, 3)) { index, item ->
ManageCredentialItem()
}
}
}
}
}
}
}
#Composable
private fun ManageCredentialHeader(text: String, vcsNo: Int) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text)
Spacer(modifier = Modifier.width(4.dp))
Box( modifier = Modifier
.size(24.dp)
.background(Color.Blue,CircleShape)
.padding(2.dp),
contentAlignment = Alignment.Center
) {
Text(
vcsNo.toString(),
color = Color.White
)
}
Spacer(modifier = Modifier.weight(1f))
}
}
#Composable
private fun ManageCredentialItem() {
Row(modifier = Modifier.padding(top = 16.dp)) {
Column() {
Text("Module 1 some text")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("Subtitle")
}
}
Spacer(modifier = Modifier.weight(1f))
Text(
"VIEW",
modifier = Modifier
.background(Color(0xff283593), RoundedCornerShape(8.dp))
.padding(6.dp),
color = Color.White
)
}
}
Looks like you need a single dynamic LazyColumn here.
You don't have to only use items at the topmost level - if you check out its source code, you'll see that it's just adds single item in a loop.
You can build your lazy content by adding multiple items inside an other loop, something like this:
LazyColumn {
listOf(
"Claim requested credentials",
"Claim received credentials",
"Pending Requests"
).forEach { header ->
item(
contentType = "header"
) {
Text(header)
}
items(
count = 10,
contentType = "body",
) { bodyIndex ->
Text(bodyIndex.toString())
}
}
}
I think problem there
LazyColumn(){
itemsIndexed(listOf(1,2,3)){ index, item ->
ManageCredentialItem()
}
}
You cannot nest LasyColumn inside LazyColumn, but you can replace it like this:
Column {
for (index, item in listOf(1, 2, 3).withIndex()) {
ManageCredentialItem()
}
}
In Jetpack Compose, How to find the width of each child of a Row?
I tried using onGloballyPositioned like below, but the size is wrong.
Also, not sure if this is the optimal one.
I need the width of each child and further processing will be done based on that, the below is a simplified code for example.
#Composable
fun MyTabSample() {
MyCustomRow(
items = listOf("Option 1 with long", "Option 2 with", "Option 3"),
)
}
#Composable
fun MyCustomRow(
modifier: Modifier = Modifier,
items: List<String>,
) {
val childrenWidths = remember {
mutableStateListOf<Int>()
}
Box(
modifier = modifier
.background(Color.LightGray)
.height(IntrinsicSize.Min),
) {
// To add more box here
Box(
modifier = Modifier
.widthIn(
min = 64.dp,
)
.fillMaxHeight()
.width(childrenWidths.getOrNull(0)?.dp ?: 64.dp)
.background(
color = DarkGray,
),
)
Row(
horizontalArrangement = Arrangement.Center,
) {
items.mapIndexed { index, text ->
Text(
modifier = Modifier
.widthIn(min = 64.dp)
.padding(
vertical = 8.dp,
horizontal = 12.dp,
)
.onGloballyPositioned {
childrenWidths.add(index, it.size.width)
},
text = text,
color = Black,
textAlign = TextAlign.Center,
)
}
}
}
}
The size you get from Modifier.onSizeChanged or Modifier.onGloballyPositioned is in px with unit Int, not in dp. You should convert them to dp with
val density = LocalDensity.current
density.run{it.size.width.toDp()}
Full code
#Composable
fun MyCustomRow(
modifier: Modifier = Modifier,
items: List<String>,
) {
val childrenWidths = remember {
mutableStateListOf<Dp>()
}
Box(
modifier = modifier
.background(Color.LightGray)
.height(IntrinsicSize.Min),
) {
val density = LocalDensity.current
// To add more box here
Box(
modifier = Modifier
.widthIn(
min = 64.dp,
)
.fillMaxHeight()
.width(childrenWidths.getOrNull(0) ?: 64.dp)
.background(
color = DarkGray,
),
)
Row(
horizontalArrangement = Arrangement.Center,
) {
items.mapIndexed { index, text ->
Text(
modifier = Modifier
.onGloballyPositioned {
childrenWidths.add(index, density.run { it.size.width.toDp() })
}
.widthIn(min = 64.dp)
.padding(
vertical = 8.dp,
horizontal = 12.dp,
),
text = text,
color = Black,
textAlign = TextAlign.Center,
)
}
}
}
}
However this is not the optimal way to get size since it requires at least one more recomposition. Optimal way for getting size is using SubcomposeLayout as in this answer
How to get exact size without recomposition?
TabRow also uses SubcomposeLayout for getting indicator and divider widths
#Composable
fun TabRow(
selectedTabIndex: Int,
modifier: Modifier = Modifier,
containerColor: Color = TabRowDefaults.containerColor,
contentColor: Color = TabRowDefaults.contentColor,
indicator: #Composable (tabPositions: List<TabPosition>) -> Unit = #Composable { tabPositions ->
TabRowDefaults.Indicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
},
divider: #Composable () -> Unit = #Composable {
Divider()
},
tabs: #Composable () -> Unit
) {
Surface(
modifier = modifier.selectableGroup(),
color = containerColor,
contentColor = contentColor
) {
SubcomposeLayout(Modifier.fillMaxWidth()) { constraints ->
val tabRowWidth = constraints.maxWidth
val tabMeasurables = subcompose(TabSlots.Tabs, tabs)
val tabCount = tabMeasurables.size
val tabWidth = (tabRowWidth / tabCount)
val tabRowHeight = tabMeasurables.fold(initial = 0) { max, curr ->
maxOf(curr.maxIntrinsicHeight(tabWidth), max)
}
val tabPlaceables = tabMeasurables.map {
it.measure(
constraints.copy(
minWidth = tabWidth,
maxWidth = tabWidth,
minHeight = tabRowHeight
)
)
}
val tabPositions = List(tabCount) { index ->
TabPosition(tabWidth.toDp() * index, tabWidth.toDp())
}
layout(tabRowWidth, tabRowHeight) {
tabPlaceables.forEachIndexed { index, placeable ->
placeable.placeRelative(index * tabWidth, 0)
}
subcompose(TabSlots.Divider, divider).forEach {
val placeable = it.measure(constraints.copy(minHeight = 0))
placeable.placeRelative(0, tabRowHeight - placeable.height)
}
subcompose(TabSlots.Indicator) {
indicator(tabPositions)
}.forEach {
it.measure(Constraints.fixed(tabRowWidth, tabRowHeight)).placeRelative(0, 0)
}
}
}
}
}
I am making a "ToggleGroup" with Jetpack-Compose, using essentially a Row into which I print Text. I manage to make it work if I tune the width manually (.width(70.dp) in the code below), but I would like it to automatically do that.
I essentially want this:
But without manually adding .width(70.dp), I get this:
My current (tuned) code is the following:
Row(
horizontalArrangement = Arrangement.End,
) {
options.forEach { option ->
val isSelected = option == selectedOption
val textColor = if (isSelected) Color.White else MaterialTheme.colors.primary
val backgroundColor = if (isSelected) Color.Gray else Color.White
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(
vertical = 6.dp, horizontal = 1.dp
)
.width(70.dp)
.background(backgroundColor)
.clickable { onSelectionChanged(option) }
) {
Text(
text = option,
color = textColor,
modifier = Modifier.padding(14.dp),
)
}
}
}
It feels similar to this question, but somehow it's different because I use Row and the question uses Column (or at least I did not manage to use Intrinsics correctly).
How could I do that?
Changes required.
1. On the parent Row, use Modifier.width(IntrinsicSize.Min)
(min|max)IntrinsicWidth: Given this height, what's the minimum/maximum width you can paint your content properly?
Source - Docs
2. Use Modifier.weight(1F) on all children.
Size the element's width proportional to its weight relative to other weighted sibling elements in the Row.
The parent will divide the horizontal space remaining after measuring unweighted child elements and distribute it according to this weight.
Source - Docs
3. Use Modifier.width(IntrinsicSize.Max) on all children.
This ensures the Text inside the children composables are not wrapped.
(You can verify this by removing the modifier and adding long text)
Screenshot
Sample code
#Composable
fun AutoWidthRow() {
val items = listOf("Item 1", "Item 2", "Item 300")
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.width(IntrinsicSize.Min),
) {
items.forEach { option ->
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(
vertical = 6.dp, horizontal = 1.dp
)
.width(IntrinsicSize.Max) // Removing this will wrap the text
.weight(1F)
.background(Color.Black)
) {
Text(
text = option,
color = Color.White,
modifier = Modifier
.padding(14.dp),
)
}
}
}
}
You should create a custom Layout
#Composable
fun EqualSizeTiles(
modifier: Modifier = Modifier,
content: #Composable () -> Unit,
) {
Layout(
content = content,
modifier = modifier,
) { measurables, constraints ->
layoutTiles(
measurables,
constraints
)
}
}
private fun MeasureScope.layoutTiles(
measurables: List<Measurable>,
constraints: Constraints,
): MeasureResult {
val tileHeight = constraints.maxHeight
val tileWidths = measurables.map { measurable ->
measurable.maxIntrinsicWidth(tileHeight)
}
val tileWidth = tileWidths.maxOrNull() ?: 0
val tileConstraints = Constraints(
minWidth = tileWidth,
minHeight = 0,
maxWidth = tileWidth,
maxHeight = constraints.maxHeight,
)
val placeables = measurables.map { measurable ->
measurable.measure(tileConstraints)
}
val width = (placeables.size * tileWidth).coerceAtMost(constraints.maxWidth)
return layout(width = width, height = tileHeight) {
placeables.forEachIndexed { index, placeable ->
placeable.place(tileWidth * index, 0)
}
}
}
#Preview(showBackground = true, widthDp = 512)
#Composable
private fun EqualSizeTilesPreview() {
WeatherSampleTheme {
Surface(
modifier = Modifier
.fillMaxWidth()
.background(color = Color.Yellow)
) {
EqualSizeTiles(
modifier = Modifier
.height(64.dp)
.background(color = Color.Green)
.padding(all = 8.dp)
) {
Text(
text = "Left",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Red)
.padding(all = 8.dp)
.fillMaxHeight(),
)
Text(
text = "Center",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Yellow)
.padding(all = 8.dp)
.fillMaxHeight(),
)
Text(
text = "Right element",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Blue)
.padding(all = 8.dp)
.fillMaxHeight(),
)
}
}
}
}
#Composable
fun PreviewLayout() {
fun getRandomString(length: Int): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
val horizontalScrollState = rememberScrollState()
LazyColumn(
modifier = Modifier
.background(Color.Blue)
.fillMaxHeight()
.wrapContentWidth()
.horizontalScroll(horizontalScrollState)
) {
items(5) { index ->
Text(
text = getRandomString((index + 1) * 4).uppercase(),
color = Color.Black,
fontSize = 16.sp,
modifier = Modifier
.padding(8.dp)
.background(Color.Yellow)
)
}
}
}
Preview of the layout:
I'd like to have the items width be the same as the largest item in the list.
Notice the .horizontalScroll(horizontalScrollState), this is to allow horizontal scrolling.
What I'd like:
I need to use a LazyColumn but if I could use a Column I'd write it this way:
Column(
modifier = Modifier
.background(Color.Blue)
.horizontalScroll(horizontalScrollState)
.fillMaxHeight()
.width(IntrinsicSize.Min)
) {
repeat(5) { index ->
Text(
text = getRandomString((index + 1) * 4).uppercase(),
color = Color.Black,
fontSize = 16.sp,
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.background(Color.Yellow)
)
}
}
You need to calculate width of the widest element separately. You can do it by placing an invisible copy of you cell with widest content in a Box along with LazyColumn.
In your sample it's easy - just get the longest string. If in the real project you can't decide which of contents is gonna be the widest one, you have two options:
1.1. Place all of them one on top of each other. You can do it only if you have some limited number of cells,
1.2. Otherwise you have to made some approximation and filter a short list of the ones you expect to be the widest ones.
Because of horizontalScroll maxWidth constraint is infinity, you have to pass calculated width manually. You can get it with onSizeChanged:
#Composable
fun TestScreen(
) {
fun getRandomString(length: Int): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
val items = remember {
List(30) { index ->
getRandomString((index + 1) * 4).uppercase()
}
}
val maxLengthItem = remember(items) {
items.maxByOrNull { it.length }
}
val (maxLengthItemWidthDp, setMaxLengthItemWidthDp) = remember {
mutableStateOf<Dp?>(null)
}
val horizontalScrollState = rememberScrollState()
Box(
Modifier
.background(Color.Blue)
.horizontalScroll(horizontalScrollState)
) {
LazyColumn(
Modifier.fillMaxWidth()
) {
items(items) { item ->
Cell(
item,
modifier = if (maxLengthItemWidthDp != null) {
Modifier.width(maxLengthItemWidthDp)
} else {
Modifier
}
)
}
}
if (maxLengthItem != null) {
val density = LocalDensity.current
Cell(
maxLengthItem,
modifier = Modifier
.requiredWidthIn(max = Dp.Infinity)
.onSizeChanged {
setMaxLengthItemWidthDp(with(density) { it.width.toDp() })
}
.alpha(0f)
)
}
}
}
#Composable
fun Cell(
item: String,
modifier: Modifier,
) {
Text(
text = item,
color = Color.Black,
fontSize = 16.sp,
modifier = modifier
.padding(8.dp)
.background(Color.Yellow)
)
}
Result:
This is not possible when horizontal srolling is enabled.
Regular Modifier.fillMaxWidth can't work inside the scrolling
horizontally layouts as the items are measured with
Constraints.Infinity as the constraints for the main axis.
If you want your Column solution to work, you need to place it inside another contain (like a Box) and apply horizontal scrolling to the parent. Scrolling on the Column itself needs to be removed:
Box(
modifier = Modifier
.requiredWidth(250.dp)
.fillMaxHeight()
.horizontalScroll(rememberScrollState())
) {
Column(
modifier = Modifier
.background(Color.Blue)
.fillMaxHeight()
.width(IntrinsicSize.Min)
) {
repeat(5) { index ->
Text(
text = getRandomString((index + 1) * 40).uppercase(),
color = Color.Black,
fontSize = 16.sp,
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.background(Color.Yellow)
)
}
}
}
#Composable
fun PreviewLayout() {
fun getRandomString(length: Int): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
val horizontalScrollState = rememberScrollState()
LazyColumn(
modifier = Modifier
.background(Color.Blue)
.fillMaxHeight()
.horizontalScroll(horizontalScrollState)
) {
items(5) { index ->
Text(
text = getRandomString((index + 1) * 4).uppercase(),
color = Color.Black,
fontSize = 16.sp,
modifier = Modifier
.padding(8.dp)
.width(width = 200.dp)
.background(Color.Yellow)
)
}
}
}
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
}
}
}
)
}