Jordy Mendoza
Jordy Mendoza

Reputation: 432

Jetpack Compose Vertical Grid single item span size

In xml you can use GridLayoutManager.SpanSizeLookup in GridLayoutManager to set the span size on single items (How many columns the item will use in the row, like for example, in a grid of 3 columns I can set the first item to be span size 3 so it will use all the width of the grid), but in Compose I can't find a way to do it, the vertical grid only have a way set the global span count and add items, but not set the span size of an individual item, is there a way to do it?

Upvotes: 4

Views: 4945

Answers (5)

Ashir Mehmood
Ashir Mehmood

Reputation: 1291

I don't know about back then but I still faced this issue in 2025 and spent a lot of time trying to provide dynamic span lookup like we did in GridLayout in Views. I was able to call span for all items but not for individual items. Long story short. I came to see this answer

The documentation on this is a bit scarce, that is true. What you might have missed is: while you can only provide one span lambda, it will be called for each item, with that item as argument. This means you are free to return different span sizes depending on the argument provided. So the implementation path is almost identical to the classic SpanSizeLookup, with the advantage that you don't have to look up items by their index (but can still opt to do it):

// Just for visualization purposes
@Composable
fun GridItem(label: String) {
Box(
    Modifier
        .fillMaxWidth()
        .height(56.dp)
        .border(1.dp, Color.Gray, RoundedCornerShape(16.dp)),
    contentAlignment = Alignment.Center
) {
    Text(text = label)
  }
}

  @Preview
  @Composable
  fun GridSpansSample() {
   LazyVerticalGrid(columns = GridCells.Fixed(3), modifier = 
   Modifier.fillMaxSize()) {

    // based on index
    items(3, span = { index ->
        val spanCount = if (index == 0) 3 else 1
        GridItemSpan(spanCount)
    }) { index ->
        GridItem("Item #$index")
    }

    // based  on list content
    items(listOf("Foo", "Bar", "Baz"), span = { item ->
        val spanCount = if (item == "Foo") 3 else 1
        GridItemSpan(spanCount)
    }) { item ->
        GridItem(item)
    }

    // based on either content or index
    itemsIndexed(listOf("Foo", "Bar", "Baz"), span = { index, item ->
        val spanCount = if (item == "Foo" || index == 1) 3 else 1
        GridItemSpan(spanCount)
    }) { index, item ->
        GridItem(item)
    }

    // Bonus: The span lambda receives additional information as "this" 
    context, which allows for further customization
    items(10 , span = {
        // occupy the available remaining width in the current row, but at most 
    2 cells wide
        GridItemSpan(this.maxCurrentLineSpan.coerceAtMost(2))
    }) { index ->
        GridItem("Item #$index")
    }
    }
    }

But still Android studio was not able to resolve the parameter span lambda because in my specific case I was missing the following dependency.

implementation("androidx.compose.foundation:foundation:1.5.1")

Hope this helps those having a similar issue.

Upvotes: 0

Athar Gul
Athar Gul

Reputation: 11

The first answer seems to work but i think it wasn't concise. Here's how i solved this problem:

 LazyVerticalGrid(columns = GridCells.Fixed(count = columns)) {
        myItemsList.forEach { myItem ->
            item(
                key = myItem.hashCode(),
                span = {
                    GridItemSpan(myItem.spanSize)
                }
            ) {
                Text(text=myItem.title)
            }

        }
    }

I was building a dynamic app where i was loading list from the firebase. So in these type of scenarios iterating over the list is the best

Upvotes: 0

Francesc
Francesc

Reputation: 29260

There is no support for this out of the box at present. The way I have solved this for now is to use a LazyColumn then the items are Rows and in each Row you can decide how wide an item is, using weight.

I have implemented and in my case I have headers (full width), and cells of items of equal width (based on how wide the screen is, there could be 1, 2 or 3 cells per row). It's a workaround, but until there is native support from VerticalGrid this is an option.

My solution is here - look for the LazyListScope extensions.

Edit: this is no longer necessary as LazyVerticalGrid supports spans now, here's an example

LazyVerticalGrid(
    columns = GridCells.Adaptive(
        minSize = WeatherCardWidth,
    ),
    modifier = modifier,
    contentPadding = PaddingValues(all = MarginDouble),
    horizontalArrangement = Arrangement.spacedBy(MarginDouble),
    verticalArrangement = Arrangement.spacedBy(MarginDouble),
) {
    state.forecastItems.forEach { dayForecast ->
        item(
            key = dayForecast.header.id,
            span = { GridItemSpan(maxLineSpan) }
        ) {
            ForecastHeader(
                state = dayForecast.header,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(vertical = MarginDouble),
            )
        }
        items(
            items = dayForecast.forecast,
            key = { hourForecast -> hourForecast.id }
        ) { hourForecast ->
            ForecastWeatherCard(
                state = hourForecast,
                modifier = Modifier.fillMaxWidth(),
            )
        }
    }
}

Upvotes: 2

Mahmudul Hasan Shohag
Mahmudul Hasan Shohag

Reputation: 3131

Jetpack Compose version 1.1.0-beta03 introduced horizontal spans to LazyVerticalGrid.

Example code:

val list by remember { mutableStateOf(listOf("A", "E", "I", "O", "U")) }

LazyVerticalGrid(
    cells = GridCells.Fixed(2)
) {
    // Spanned Item:
    item(
        span = {
            // Replace "maxCurrentLineSpan" with the number of spans this item should take.
            // Use "maxCurrentLineSpan" if you want to take full width.
            GridItemSpan(maxCurrentLineSpan)
        }
    ) {
        Text("Vowels")
    }

    // Other items:
    items(list) { item ->
        Text(item)
    }
}

Upvotes: 14

Jordy Mendoza
Jordy Mendoza

Reputation: 432

Adapting the code from the answer, I created a more "general" purpose method, It can be used with Adaptive and Fixed, I'm very new with Compose so I accept suggestions

@Composable
fun HeaderGrid(cells: GridCells, content: HeaderGridScope.() -> Unit) {
    var columns = 1
    var minColumnWidth = 0.dp
    when (cells) {
        is GridCells.Fixed -> {
            columns = cells.count
            minColumnWidth = cells.minSize
        }
        is GridCells.Adaptive -> {
            val width = LocalContext.current.resources.displayMetrics.widthPixels
            val columnWidthPx = with(LocalDensity.current) { cells.minSize.toPx() }
            minColumnWidth = cells.minSize
            columns = ((width / columnWidthPx).toInt()).coerceAtLeast(1)
        }
    }
    LazyColumn(modifier = Modifier.fillMaxWidth()){
        content(HeaderGridScope(columns, minColumnWidth, this))
    }
}

fun <T>HeaderGridScope.gridItems(items: List<T>, content: @Composable (T) -> Unit) {
    items.chunked(numColumn).forEach {
        listScope.item {
            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
                it.forEach { 
                    content(it)
                }
                if (it.size < numColumn) {
                    repeat(numColumn - it.size) {
                        Spacer(modifier = Modifier.width(columnWidth))
                    }
                }
            }
        }
    }
}

fun HeaderGridScope.header(content: @Composable BoxScope.() -> Unit) {
    listScope.item {
        Box(
            modifier = Modifier
                .fillMaxWidth(),
           content = content
        )
    }
}

data class HeaderGridScope(val numColumn: Int, val columnWidth: Dp, val listScope: LazyListScope)

sealed class GridCells {
    class Fixed(val count: Int, val minSize: Dp) : GridCells()
    class Adaptive(val minSize: Dp) : GridCells()
}

Upvotes: 0

Related Questions