Search code examples
androidandroid-layoutandroid-jetpack-compose

How to know items which are fully visible in the view port of a lazy row in jetpack compose?


I have a lazy row that contains items. Now I want to make an API call for the items which are fully visible in the viewport whenever the user scrolls the lazy row.

I have tried the following code:

    listState = rememberLazyListState()
    LaunchedEffect(listState){
    snapshotFlow { listState.firstVisibleItemIndex }
    .collectLatest{
    Log.d("printed Item",  listState.firstVisibleItemIndex.toString())
    }}

The problems with this code are:

  1. Even though the 2nd item occupies the viewport, it will not be printed unless the 1st item is fully invisible.
  2. For the tablets, due to their large screen sizes, only the API call is made for the first visible item even though there are 2 visible items on the screen. Please refer to the screenshots.

When the first item is partially visible and 2nd item is fully visible [1]: https://i.sstatic.net/l5QcB.jpg

When the 2nd tile is fully visible and the first tile is completely invisible [2]: https://i.sstatic.net/6rmiQ.jpg

For the tablets where 2 items are completely visible [3]: https://i.sstatic.net/QYRTI.jpg

Can anyone please tell me how to resolve my issue?


Solution

  • All info about visible items is available in state.layoutInfo. To check wether the item is visible you need to compare first and last item positions(all other items for sure are gonna be visible) according to viewport size.

    To initiate recomposition only for changed cells, you can again use derivedStateOf within item to convert the indexes to a specific boolean state.

    val state = rememberLazyListState()
    val fullyVisibleIndices: List<Int> by remember {
        derivedStateOf {
            val layoutInfo = state.layoutInfo
            val visibleItemsInfo = layoutInfo.visibleItemsInfo
            if (visibleItemsInfo.isEmpty()) {
                emptyList()
            } else {
                val fullyVisibleItemsInfo = visibleItemsInfo.toMutableList()
    
                val lastItem = fullyVisibleItemsInfo.last()
    
                val viewportHeight = layoutInfo.viewportEndOffset + layoutInfo.viewportStartOffset
    
                if (lastItem.offset + lastItem.size > viewportHeight) {
                    fullyVisibleItemsInfo.removeLast()
                }
    
                val firstItemIfLeft = fullyVisibleItemsInfo.firstOrNull()
                if (firstItemIfLeft != null && firstItemIfLeft.offset < layoutInfo.viewportStartOffset) {
                    fullyVisibleItemsInfo.removeFirst()
                }
    
                fullyVisibleItemsInfo.map { it.index }
            }
        }
    }
    LazyColumn(
        state = state,
        contentPadding = PaddingValues(30.dp)
    ) {
        items(100) { index -> 
            val isVisible by remember(index) {
                derivedStateOf {
                    fullyVisibleIndices.contains(index)
                }
            }
            Text(
                index.toString(),
                modifier = Modifier
                    .background(if (isVisible) Color.Green else Color.Transparent)
                    .padding(30.dp)
            )
        }
    }