galex
02/06/2022, 5:56 PMLazyColumn
and another callback when it becomes invisible? I’m at lost with the side effects… 😕mattinger
02/06/2022, 6:24 PMDisposableEffect(key1 = true) {
// Do something when it appears
onDispose {
// Do something when it goes away
}
}
Keep in mind though that this is "approximate" with LazyColumn because it will keep a few compositions on either side of the currently visible scroll region. So if you first start the screen and you have 5 rows visible, you'll actually have composed around 6 or 7 rows and the DisposableEffect will fire. Likewise, if you scroll down so rows 2 - 6 on the screen, the dispose of row 1 probably won't have fired yet.galex
02/06/2022, 7:21 PMmattinger
02/06/2022, 7:21 PMgalex
02/06/2022, 7:45 PMFunkyMuse
02/06/2022, 8:52 PMgalex
02/06/2022, 9:02 PMLazyColumn(
scrollState = scrollState,
) {
itemsIndexed(photos) { index, item: Photo ->
scrollState.ItemTracker(
trackedIndex = index,
trackedItem = item,
onItemShown = { indexShown: Int, itemShown: Photo -> Log.d("TrackerList", "item shown - $indexShown - $itemShown") },
onItemHidden = { indexHidden: Int, itemHidden: Photo -> Log.d("TrackerList", "item hidden - $indexHidden - $itemHidden") },
)
PhotoItem(
photo = item,
onPhotoClick = onPhotoClick,
modifier = Modifier.padding(2.dp),
)
}
}
And the ItemTracker
I am trying to build:
@Composable
fun <T> LazyListState.ItemTracker(
trackedIndex: Int,
trackedItem: T,
onItemShown: (indexShown: Int, itemShown: T) -> Unit,
onItemHidden: (indexHidden: Int, itemHidden: T) -> Unit
) {
val visible = remember(layoutInfo.visibleItemsInfo) {
val info = layoutInfo.visibleItemsInfo.getOrNull(trackedIndex)
return@remember info?.let {
Log.d("Tracker", "$trackedIndex has info at visibility = ${visibilityPercent(info)}")
visibilityPercent(info) == 100f
}
}
LaunchedEffect(visible) {
Log.d("Tracker", "$trackedIndex is visible = $visible")
when (visible) {
true -> onItemShown(trackedIndex, trackedItem)
false -> onItemHidden(trackedIndex, trackedItem)
}
}
}
And how it calculates the visibility:
fun LazyListState.visibilityPercent(info: LazyListItemInfo): Float {
val cutTop = maxOf(0, layoutInfo.viewportStartOffset - info.offset)
val cutBottom = maxOf(0, info.offset + info.size - layoutInfo.viewportEndOffset)
return maxOf(0f, 100f - (cutTop + cutBottom) * 100f / info.size)
}
The results are weird… I am def missing something here, but it’s late so I’ll leave that for tomorrow…. 🤔mattinger
02/07/2022, 5:46 AMval state = rememberLazyListState()
val flow = snapshotFlow { state.layoutInfo }
LaunchedEffect(key1 = true) {
flow.collect {
val firstIndex = it.visibleItemsInfo.first().index
val lastIndex = it.visibleItemsInfo.last().index
}
}
galex
02/07/2022, 5:50 AMmattinger
02/07/2022, 6:04 AMfun Modifier.trackVisibleItems(
state: LazyListState,
onVisibleItems: (Int, Int) -> Unit
) = composed {
val flow = snapshotFlow { state.layoutInfo }
LaunchedEffect(key1 = true) {
flow.collect {
val firstIndex = it.visibleItemsInfo.first().index
val lastIndex = it.visibleItemsInfo.last().index
onVisibleItems(firstIndex, lastIndex)
}
}
Modifier
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.trackVisibleItems(
state = state,
onVisibleItems = { firstIndex, lastIndex ->
Timber.tag("LazyColumn").d("$firstIndex $lastIndex")
}
),
galex
02/07/2022, 6:04 AMmattinger
02/07/2022, 6:05 AMgalex
02/07/2022, 6:05 AMmattinger
02/07/2022, 6:07 AMgalex
02/07/2022, 6:11 AMmattinger
02/07/2022, 6:16 AMTimber.tag("LazyColumn").d("viewport=${it.viewportStartOffset} ${it.viewportEndOffset}")
it.visibleItemsInfo.forEach {
Timber.tag("LazyColumn").d("index=${it.index} offset=${it.offset}")
2022-02-07 01:16:07.149 7018-7018/com.example.myapplication D/LazyColumn: index=31 offset=1829
2022-02-07 01:16:07.149 7018-7018/com.example.myapplication D/LazyColumn: index=32 offset=1888
2022-02-07 01:16:07.149 7018-7018/com.example.myapplication D/LazyColumn: index=33 offset=1947
2
galex
02/07/2022, 6:21 AMmattinger
02/07/2022, 6:25 AMfun Modifier.trackVisibleItems(
state: LazyListState,
onVisibleItems: (Int, Int) -> Unit
) = composed {
val flow = snapshotFlow { state.layoutInfo }
LaunchedEffect(key1 = true) {
flow.collect { layoutInfo ->
Timber.tag("LazyColumn").d("viewport=${layoutInfo.viewportStartOffset} ${layoutInfo.viewportEndOffset}")
layoutInfo.visibleItemsInfo.forEach { item ->
val isFullyVisible =
item.offset >= layoutInfo.viewportStartOffset &&
item.size + item.offset <= layoutInfo.viewportEndOffset
Timber.tag("LazyColumn").d("index=${item.index} size=${item.size} offset=${item.offset} isFullyVisible=${isFullyVisible}")
}
val firstIndex = layoutInfo.visibleItemsInfo.first().index
val lastIndex = layoutInfo.visibleItemsInfo.last().index
onVisibleItems(firstIndex, lastIndex)
}
}
Modifier
}
galex
02/07/2022, 6:31 AMmattinger
02/07/2022, 6:51 AMfun Modifier.trackVisibleItems(
state: LazyListState,
onNewlyVisible: (List<Int>) -> Unit,
onNewlyHidden: (List<Int>) -> Unit
) = composed {
val flow = snapshotFlow { state.layoutInfo }
val visibleItemIndexes = remember { mutableStateOf(emptyList<Int>()) }
LaunchedEffect(key1 = true) {
flow.collect { layoutInfo ->
val newVisibleItemIndexes = layoutInfo.visibleItemsInfo.filter { item ->
item.offset >= layoutInfo.viewportStartOffset &&
item.size + item.offset <= layoutInfo.viewportEndOffset
}.map { item ->
item.index
}
val newlyVisible = newVisibleItemIndexes.filter {
!visibleItemIndexes.value.contains(it)
}
val newlyHidden = visibleItemIndexes.value.filter {
!newVisibleItemIndexes.contains(it)
}
if (newlyVisible.isNotEmpty()) {
onNewlyVisible(newlyVisible)
}
if (newlyHidden.isNotEmpty()) {
onNewlyHidden(newlyHidden)
}
visibleItemIndexes.value = newVisibleItemIndexes
}
}
Modifier
}
2022-02-07 01:49:50.635 8329-8329/com.example.myapplication D/LazyColumn: newlyHidden=[0]
2022-02-07 01:49:50.669 8329-8329/com.example.myapplication D/LazyColumn: newlyVisible=[33]
2022-02-07 01:49:50.746 8329-8329/com.example.myapplication D/LazyColumn: newlyHidden=[1]
2022-02-07 01:49:50.827 8329-8329/com.example.myapplication D/LazyColumn: newlyVisible=[34]
2022-02-07 01:49:51.014 8329-8329/com.example.myapplication D/LazyColumn: newlyHidden=[2]
2022-02-07 01:49:51.127 8329-8329/com.example.myapplication D/LazyColumn: newlyVisible=[35]
2022-02-07 01:49:51.309 8329-8329/com.example.myapplication D/LazyColumn: newlyHidden=[3]
2022-02-07 01:49:51.376 8329-8329/com.example.myapplication D/LazyColumn: newlyVisible=[36]
2022-02-07 01:49:51.441 8329-8329/com.example.myapplication D/LazyColumn: newlyHidden=[4]
2022-02-07 01:49:51.548 8329-8329/com.example.myapplication D/LazyColumn: newlyVisible=[37]
2022-02-07 01:49:51.578 8329-8329/com.example.myapplication D/LazyColumn: newlyHidden=[5]
2022-02-07 01:49:51.702 8329-8329/com.example.myapplication D/LazyColumn: newlyVisible=[38]
2022-02-07 01:49:51.743 8329-8329/com.example.myapplication D/LazyColumn: newlyHidden=[6]
2022-02-07 01:49:51.759 8329-8329/com.example.myapplication D/LazyColumn: newlyVisible=[39]
2022-02-07 01:49:51.818 8329-8329/com.example.myapplication D/LazyColumn: newlyHidden=[7]
2022-02-07 01:49:51.870 8329-8329/com.example.myapplication D/LazyColumn: newlyVisible=[40]
galex
02/07/2022, 7:05 AMLaunchedEffect
with DisposableEffect
with a callback call in onDispose
should work right?fun Modifier.trackVisibleItems(
state: LazyListState,
scope: CoroutineScope,
onItemsVisible: (List<Int>) -> Unit,
onItemsHidden: (List<Int>) -> Unit
) = composed {
val flow = snapshotFlow { state.layoutInfo }
val visibleItemIndexes = remember { mutableStateOf(emptyList<Int>()) }
DisposableEffect(key1 = true) {
scope.launch {
flow.collect { layoutInfo ->
val newVisibleItemIndexes = layoutInfo.visibleItemsInfo.filter { item ->
item.offset >= layoutInfo.viewportStartOffset && item.size + item.offset <= layoutInfo.viewportEndOffset
}.map { item -> item.index }
val newlyVisible = newVisibleItemIndexes.filter {
!visibleItemIndexes.value.contains(it)
}
val newlyHidden = visibleItemIndexes.value.filter {
!newVisibleItemIndexes.contains(it)
}
if (newlyVisible.isNotEmpty()) {
onItemsVisible(newlyVisible)
}
if (newlyHidden.isNotEmpty()) {
onItemsHidden(newlyHidden)
}
visibleItemIndexes.value = newVisibleItemIndexes
}
}
onDispose {
onItemsHidden(visibleItemIndexes.value)
}
}
Modifier
}