Reputation: 1358
FlatList
of React Native
has a property viewabilityConfigCallbackPairs
where you can set:
viewabilityConfig: {
itemVisiblePercentThreshold: 50,
waitForInteraction: true,
}
to detect visible items of the list with threshold of 50% and after interaction or scroll.
Does Jetpack Compose also have something similar to this?
There is LazyListState
with some layout info. But I wonder if there is anything built-in component/property for this use case.
Edit
I have a list of cardviews and I want to detect which card items (at least 50% of card is visible) are visible on display. But it needs to be detected only when the card is clicked or list is scrolled by user.
Upvotes: 3
Views: 9636
Reputation: 31
Visibility Tracker works like the viewabilityConfigCallbackPairs.
VisibilityTracker(
threshold = 0.5f,
onVisibilityChanged = { isVisible ->
if (isVisible) yourLogic()
},
treatOnStopAsInvisible = true
) {
// Your composable content here
}
Upvotes: 0
Reputation: 1674
To get an updating list of currently visible items with a certain threshold LazyListState
can be used.
LazyListState
exposes the list of currently visible items List<LazyListItemInfo>
. It's easy to calculate visibility percent
using
offset
and size
properties, and thus apply a filter to the visible list for visibility >= threshold
.
LazyListItemInfo
has index
property, which can be used for mapping LazyListItemInfo
to the actual data item in the list passed to LazyColumn
.
fun LazyListState.visibleItems(itemVisiblePercentThreshold: Float) =
layoutInfo
.visibleItemsInfo
.filter {
visibilityPercent(it) >= itemVisiblePercentThreshold
}
fun LazyListState.visibilityPercent(info: LazyListItemInfo): Float {
val cutTop = max(0, layoutInfo.viewportStartOffset - info.offset)
val cutBottom = max(0, info.offset + info.size - layoutInfo.viewportEndOffset)
return max(0f, 100f - (cutTop + cutBottom) * 100f / info.size)
}
Usage
val list = state.visibleItems(50f) // list of LazyListItemInfo
This list has to be mapped first to corresponding items in LazyColumn
.
val visibleItems = state.visibleItems(50f)
.map { listItems[it.index] }
@Composable
fun App() {
val listItems = remember { generateFakeListItems().toMutableStateList() }
val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state = state) {
items(listItems.size) {
Item(listItems[it])
}
}
val visibleItems by remember(state) {
derivedStateOf {
state.visibleItems(50f)
.map { listItems[it.index] }
}
}
LaunchedEffect(visibleItems) {
Log.d(TAG, "App: $visibleItems")
}
}
fun generateFakeListItems() = (0..100).map { "Item $it" }
Upvotes: 17
Reputation: 364948
The LazyListState#layoutInfo
contains information about the visible items.
Since you want to apply a threshold you need to check the first and last item positions and size according to viewport size. All other items are for sure visible.
It is important to note that since you are reading the state
you should use derivedStateOf
to avoid redundant recompositions.
Something like:
@Composable
private fun LazyListState.visibleItemsWithThreshold(percentThreshold: Float): List<Int> {
return remember(this) {
derivedStateOf {
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (layoutInfo.totalItemsCount == 0) {
emptyList()
} else {
val fullyVisibleItemsInfo = visibleItemsInfo.toMutableList()
val lastItem = fullyVisibleItemsInfo.last()
val viewportHeight = layoutInfo.viewportEndOffset + layoutInfo.viewportStartOffset
if (lastItem.offset + (lastItem.size*percentThreshold) > viewportHeight) {
fullyVisibleItemsInfo.removeLast()
}
val firstItemIfLeft = fullyVisibleItemsInfo.firstOrNull()
if (firstItemIfLeft != null &&
firstItemIfLeft.offset + (lastItem.size*percentThreshold) < layoutInfo.viewportStartOffset) {
fullyVisibleItemsInfo.removeFirst()
}
fullyVisibleItemsInfo.map { it.index }
}
}
}.value
}
and then just use:
val state = rememberLazyListState()
LazyColumn( state = state ){
//items
}
val visibleItems = state.visibleItemsWithThreshold(percentThreshold = 0.5f)
In this way you have the list of all the visible items with a threshold of 50%. You can observe the list using something:
LaunchedEffect(visibleItems){
Log.d(TAG, "App: $visibleItems")
}
Upvotes: 2