Compose LazyList section background

Viewed 1576

I'm having some trouble figuring out if my problem is a Jetpack Compose missing feature or if I can't find how it is done.

Let's say I want to make this page

enter image description here

It needs to be scrollable, because the content is long.

I also would like to use a lazy column to load the list of users that is shown in the image.

The issue is that you can't have a LazyColumn inside a vertically scrollable layout, so i thought i would simply make the whole page a LazyColumn. Now there is another issue, i want the user list to have a box around it with the background color and rounded borders as shown, but you can't put a box around the LazyListScope.items() and if you load the list as a single composable like item { UserList() } then it just makes it a column, losing the lazy part.

How would one do this?

5 Answers

If i understood the problem correctly, what you need to do is to define separately the layout of the "item" you represent in your LazyColumn. Lets say as an example you have some lectures to present in your list. The way you can define this is:

LazyColumn(
   //modifiers etc
) {
    items(
        items = **lectures** -> your list of items,
        itemContent = {
            **LectureListItem**(it) -> your specified layout
        }
    )
}

And underneath you create your composable LectureListItem which has the layout you want(wheather it is a box, column, row and all the things in it). Example:

@Composable
fun LectureListItem(
    lecture: Lecture
) {
    Box(
        modifier = Modifier
            .padding(8.dp)
            .background(//exampleColor)
    ) {
        //Other elements of your layout
    }
}

This assumes that you know the indices of the particular items you want to encase in a box.

val list = ... //Contains all the items
val lowerBound = ... // Start of User List
val upperBound = ...//"
LazyColumn{
    list.forEachIndexed{index, item ->
          if(lowerBound == index){
              item{
                Text(Modifier.clip(RoundedCornerShape(topStartPercent = 50, topEndPercent = 50)) //User
.background (color)
)
              }
else if(upperBound == index){
Text(Modifier.clip(RoundedCornerShape(bottomStartPercent = 50, bottomEndPercent = 50)) //User
.background (color)
}
else if(lowerBound < index < upperBound){
Text(Modifier.background(color)
else{
//Regular List Items
}
          }
    }
}

This is only a workaround. This will give the exact effect, but is not really a box.

A cleaner solution would be to actually construct a custom Composable and upon detection of the lowerBound, add that Composable as a whole item, because although you can't use items inside a box, you can use vice versa.

Here's how:-

val list = ... //Contains all the items
val lowerBound = ... // Start of User List
val upperBound = ...//"
LazyColumn{
var x = 0
while(x++ != list.size()){
   if(index == lowerBound){
       item{
         UserList(list.subList(lowerBound, upperBound))
       }
   }
   else if(lowerBound < index < upperBound){
   continue
   }
   else{
     item{
        User()
     }
    }
}
}

@Composable
fun UserList(list){
  LazyColumn{
    //Display List
  }
}

This keeps the column lazy. No performance impacts

If you want to have a background on group of items, you can set background only on the items of that group.

The trick is to set the background before your other modifiers, so it's applied before item is styled, like this:

ListItem(modifier = Modifier
    .background(GrayBackground)
    .padding(horizontal = 20.dp)
)

As of 5/16/2022 the best way to add a uniformed background to a list of items is through a custom composable that nests a *listOf<Any //your data class/>() within a LazyColumn() - "item { }" block.

Below is an example of a function that extends the scope of a LazyColumn()

fun LazyListScope.itemGroup(list: List<Any>) {

   return this.item {
       //**Some composable you want to display*/
   }

}

within the return block of the function above, include a Column()

Column(
    modifier = Modifier
        .clip(RoundedCornerShape(5))
        .background(DarkShade.copy(0.15f))
        .padding(horizontal = padding.medium, vertical = padding.small),
    verticalArrangement = Arrangement.spacedBy(padding.small)
) { 
  
  //**your list of items*/

}

This Column() allows you to style your background in anyway you'd like; color, padding, shape and more. Once you have your preferred background you can instantiate your list items with a for loop.

list.forEach { item ->
    ListItem(
        icon = item.icon,
        title = item.title,
        subtitle = item.subtitle,
        onClick = item.onClick
    )
}

Finally you can simply call itemGroup() in any LazyColumn() to use

LazyColumn {

   itemGroup(emptyList())

}

I have achieved this behaviour with a bit of a hack:

  1. Use a Card for every item
  2. The first card must have RoundedCornerShape with topStart and topEnd radius
  3. The first card must have RoundedCornerShape with bottomStart and bottomEnd radius
  4. All Other Cards are of RectangleShape
  5. (Special case: If there is only one item use a regular RoundedCornerShape)
private val topShape = RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp)
private val bottomShape = RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp)

@Composable
fun LazyColumnCategory(
    itemCount: Int,
    idx: Int,
    content: @Composable (Shape) -> Unit,
) {
    Column {
        content(
            if (itemCount == 1)
                RoundedCornerShape(4.dp)
            else if (idx == 0)
                topShape
            else if (idx == itemCount - 1)
                bottomShape
            else
                RectangleShape
        )
        if (idx != itemCount - 1)
            Divider()
    }
}

Use this within your LazyColumn the following way:

LazyColumn {
    itemsIndexed(yourList) { idx, data ->
        LazyColumnCategory(itemCount = yourList.size, idx = idx) { shape ->
            Card(shape = shape) {
                // your content here
            }
        }
    }

}

Be careful when applying padding to your cards. Always define the padding before the height, otherwise there will be gaps. (see the docs)

Related