1

I am currently in the process of evaluating whether or not we can migrate our rather complex UI to jetpack compose at this stage and I am struggling with the following problem.

I am having an infinite scrolling vertical List of various different conceptual components. Some of them are headers, then there can be some text, some horizontally scrolling (infinite) lists and then there are some grouped components that are also stacked vertically but conceptionally belong to a group.

The conceptual design

@Compose
fun MyComplexList() {
 LazyColumn {
  item {
   // some header
  }
  item {
   // some horizontal content
   LazyRow {
    item {}
   }
  }
  item {
   // some other header
  }
  items(x) {
   // Some text for each item
  }
 }
}

As one can see this thing is rather trivial to do using compose and a lot less code than writing this complex RecyclerView + Adapter... with one exception: that background gradient, spanning (grouping) the Some infinite list of things component. (the tilted gradient in the image)

In the past (:D) I would use an ItemDecoration on the RecyclerView to draw something across multiple items, but I can't find anything similar to that in Compose.

Does anyone have any idea on how one would achieve this with compose?

saberrider
  • 585
  • 3
  • 16
  • Does this answer helps you? https://stackoverflow.com/questions/66908737/what-is-the-equivalent-of-nestedscrollview-recyclerview-or-nested-recyclerv/66913480#66913480 – nglauber May 06 '22 at 14:39
  • Not really. It's not really a question about how to put those items on screen but rather on how to draw a single background behind a group of items. – saberrider May 06 '22 at 14:50

2 Answers2

1

After your answer, this is what I understood...

@Composable
fun ListWithGradientBgScreen() {
    val lazyListState = rememberLazyListState()
    val firstVisibleIndex by remember {
        derivedStateOf {
            lazyListState.firstVisibleItemIndex
        }
    }
    val totalVisibleItems by remember {
        derivedStateOf {
            lazyListState.layoutInfo.visibleItemsInfo.size
        }
    }
    val itemsCount = 50
    BoxWithConstraints(Modifier.fillMaxSize()) {
        ListBg(firstVisibleIndex, totalVisibleItems, itemsCount, maxHeight)
        LazyColumn(state = lazyListState, modifier = Modifier.fillMaxSize()) {
            item {
                Column(
                    Modifier
                        .fillMaxWidth()
                        .background(Color.White)
                ) {
                    Text(
                        text = "Some header",
                        style = MaterialTheme.typography.h5,
                        modifier = Modifier.padding(16.dp)
                    )
                }
            }

            item {
                Text(
                    text = "Some infinite list of things",
                    style = MaterialTheme.typography.h5,
                    modifier = Modifier.padding(16.dp)
                )
            }

            items(itemsCount) {
                Text(
                    text = "Item $it",
                    Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 16.dp, vertical = 6.dp)
                        .background(Color.LightGray)
                        .padding(8.dp)
                )
            }
        }
    }
}

and to change the background in according to the background, you can define something like the following:

@Composable
private fun ListBg(
    firstVisibleIndex: Int,
    totalVisibleItems: Int,
    itemsCount: Int,
    maxHeight: Dp
) {
    val hasNoScroll = itemsCount <= totalVisibleItems
    val totalHeight = if (hasNoScroll) maxHeight else maxHeight * 3
    val scrollableBgHeight = if (hasNoScroll) maxHeight else totalHeight - maxHeight
    val scrollStep = scrollableBgHeight / (itemsCount + 2 - totalVisibleItems)
    val yOffset = if (hasNoScroll) 0.dp else -(scrollStep * firstVisibleIndex)
    Box(
        Modifier
            .wrapContentHeight(unbounded = true, align = Alignment.Top)
            .background(Color.Yellow)
            .offset { IntOffset(x = 0, y = yOffset.roundToPx()) }
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(totalHeight)
                .drawBehind {
                    drawRoundRect(
                        Brush.linearGradient(
                            0f to Color.Red,
                            0.6f to Color.DarkGray,
                            1.0f to Color.Green,
                        ),
                    )
                }
        )
    }
}

Here is the result:

enter image description here

nglauber
  • 18,674
  • 6
  • 70
  • 75
  • This is close, but somehow still not what I am going for. The thing is, the background should move with the list. In this example it's essentially the background of the list and the white header is drawn on top of it. This also means the gradient background is actually cut off at the top. – saberrider May 06 '22 at 17:27
  • I will update my question to be a bit clearer – saberrider May 06 '22 at 17:28
  • 1
    Since the background should move in according to the list, I can see two options: 1) each item can have the background in order to form the list background; 2) or you move the background (using `offset` or `graphicsLayer`) based on the `LazyListState` object (get from `rememberLazyListState`) – nglauber May 06 '22 at 17:38
  • Oh... Option 1 is not possible since the backgrounds i have to use cannot be broken into pieces. That's why I used the angular gradient in the example. But I will investigate Option 2. That sounds promising! – saberrider May 06 '22 at 17:43
  • 1
    I edited my answer... Please, check if it makes sense now ;) – nglauber May 07 '22 at 00:27
  • 1
    That works quite nicely to be honest! I had to make a few changes for my usecase since sadly all those "item 1... " entries are not having necessarily the same size, so I am using the acutall offsets of the visible items on screen to offset the background, but your answer solves the stated problem perfectly. I will accept your answer. Thanks! – saberrider May 08 '22 at 15:19
  • This response will have rather poor performance, it can be improved so that `ListBg` is not recomposed at all. Please see [this video](https://www.youtube.com/watch?v=EOQB8PTLkpY) and update it – Phil Dukhov Jun 07 '22 at 13:20
  • I updated the code to use `derivedStateOf` as the video suggests and it's already using `drawBehind`. Please, let me know if I missed something else. – nglauber Jun 07 '22 at 13:38
  • 1
    I didn't get a notification about this comment, in such case it's better to tag me =). Next part after derived state says about layout/draw stages: in this case `Modifier.offset { IntOffset(...) }` should be used - it would only move the cached view without remeasuring it. – Phil Dukhov Jun 08 '22 at 07:53
-1

One of the options:

Replace:

items(x) {
   // Some text for each item
}

with:

item {
   Column(modifier = Modifier.border(...).background(...)) { //Shape, color etc...
      x.forEach {
          // Some text for each item
      }
   }
}
bylazy
  • 1,055
  • 1
  • 5
  • 16
  • hmmm.... But that would make them non-lazy I would assume, right? Meaning if I have `1000` of those, it won't be performant anymore. Or am i misunderstanding something? – saberrider May 06 '22 at 13:09