Is there an api with LazyVerticalGrid to bring an item into view (i.e. ensure that an item is visibl...
a
Is there an api with LazyVerticalGrid to bring an item into view (i.e. ensure that an item is visible)? It's useful when navigating a grid view with keyboard arrows, to make an item visible when navigating outside the viewport. But if the item is in view, then not scroll
a
Yes - BringIntoViewRequester
Works with any scrollable.
a
Yes - BringIntoViewRequester
Oh, that's really cool! And is there some api that can give me the Rect coordinates for an item, or I need to calculate that myself?
a
There’s Modifier.inGloballyPositioned and onPlaced.
Btw, not 100% sure BringIntoViewRequester will work with a lazy layout. If not, you have a scrollToItem function in LazyGridState. But you’ll need to check yourself how much to scroll, if at all, via its LayoutInfo.
a
@Alexander Maryanovsky Thank you. So I wrote some logic to do the scrolling manually, but it seems LazyGridLayoutInfo.viewportStartOffset is always 0 even when I scrolled down
I think i'll open a bug
a
It’s supposed to be 0, unless you use content padding.
What you want is
firstVisibleItemIndex
and
firstVisibleItemScrollOffset
a
Hmm, I thought viewport means the current viewable area within the control? How can I access this area then?
a
You can’t. It’s not well defined for a lazy list.
You only have the two properties above and the info in
LazyGridLayoutInfo
a
Okay. Hopefully the API will become a little more rich in the future 🥲
a
This one is pretty set, I think.
a
I was able to write some code to bring an item into view as needed:
I think because compose wasn't made for desktop originally, this is harder than it should be. Right now, there is only
scrollToItem
which is great when you want to e.g. scroll to a letter in Contacts app on a phone (when doing fast scrolling). For desktop navigating up and down with the keyboard and bringing the item into view is a common use case
a
I have this, for a LazyList:
Copy code
/**
 * Scrolls the minimum distance to make the item at the given index fully visible.
 */
suspend fun LazyListState.scrollToMakeVisible(index: Int) {
    val viewportHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
    val visibleItemsInfo = layoutInfo.visibleItemsInfo
    val firstVisibleItem = visibleItemsInfo.first()
    val lastVisibleItem = visibleItemsInfo.last()

    if (index <= firstVisibleItem.index) {  // The item is before the first visible item
        animateScrollToItem(index, 0)
    }
    else if (index == lastVisibleItem.index){  // The item is the last visible item (it may be partially visible)
        // The last visible item is already fully visible, such as when the viewport is taller than the contents
        if (lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight)
            return

        // Walk up until we overshoot the viewport height.
        // The item we've reached is the item we need to scroll to, and the offset is the amount we overshoot
        var visibleItemIndex = visibleItemsInfo.lastIndex
        var distanceToItem = 0
        while (distanceToItem < viewportHeight){
            distanceToItem += visibleItemsInfo[visibleItemIndex].size
            visibleItemIndex -= 1
        }
        animateScrollToItem(visibleItemsInfo[visibleItemIndex+1].index, distanceToItem - viewportHeight)
    }
    else if (index == lastVisibleItem.index + 1){  // The item is the first non-visible item below
        // We can't accurately align the bottom of the target item with the bottom of the list because we don't know
        // the height of its view. Best we can do is assume it's the same as the last visible item's view
        var visibleItemIndex = visibleItemsInfo.lastIndex
        var distanceToItem = lastVisibleItem.size  // <-- Assumption is here
        while (distanceToItem < viewportHeight){
            distanceToItem += visibleItemsInfo[visibleItemIndex].size
            visibleItemIndex -= 1
        }
        animateScrollToItem(visibleItemsInfo[visibleItemIndex+1].index, distanceToItem - viewportHeight)
    }
    else if (index > lastVisibleItem.index) {
        // We assume all the items are of the same size, which is hopefully true.
        // There's nothing better we can do here because the views for these items don't actually exist, so we can't
        // know their sizes.
        val itemHeight = lastVisibleItem.size
        val scrollIndex = index - viewportHeight / itemHeight
        val scrollOffset = itemHeight - viewportHeight % itemHeight
        animateScrollToItem(scrollIndex, scrollOffset)
    }
}
But this only works for the simple case (no content padding, no spacing between items, all items are the same size)
a
But this only works for the simple case
Thanks for sharing. Yes, that's why I think eventually there should be an API for this.
I tried your code adapted for a LazyGrid and it doesn't work well when scrolling down for some reason
Okay I figured something: when doing
viewportWidth / itemWidth
to get the column count I need to subtract 1 from
itemWidth
, because sometimes it could be off, e.g. I get itemWidth=204 and viewportWidth=610, the division then is 2.99, rounds to 2 instead of 3
a
To get the column count you probably should look at the visibleItemsInfo in layoutInfo and take the maximum of their column.