admin管理员组

文章数量:1300010

I have investigated infinate scrolling in compose and have it working as required in a spike i have been working on recently...

heres the code:-

data class AutoScrollingModel(val key: UUID = UUID.randomUUID(), @DrawableRes val iconResource: Int, val contentDescription: String, var isFabricated: Boolean = false)

val autoScrollingList = listOf<AutoScrollingModel>(
    AutoScrollingModel(iconResource = R.drawable.apple, contentDescription = "Apple"),
    AutoScrollingModel(iconResource = R.drawable.banana, contentDescription = "Banana"),
    AutoScrollingModel(iconResource = R.drawable.cherries, contentDescription = "cherries"),
    AutoScrollingModel(iconResource = R.drawable.dates, contentDescription = "dates"),
)

the compose code resembles this:-

    @Composable
    fun MainUi(modifier: Modifier) {

        val lazyListState = rememberLazyListState()
        val coroutineScope = rememberCoroutineScope()

        var source by remember { mutableStateOf(autoScrollingList) }

        LaunchedEffect(lazyListState) {
            val visibleItemsInfo = lazyListState.layoutInfo.visibleItemsInfo
            if (visibleItemsInfo.isEmpty()) return@LaunchedEffect
            val lastVisibleItem = visibleItemsInfo.last()
            val viewportDimension = lazyListState.layoutInfo.viewportEndOffset + lazyListState.layoutInfo.viewportStartOffset

            if (source.size == (viewportDimension / lastVisibleItem.size) + 1) {
                val transitional = (source + source[0].copy(key = UUID.randomUUID(), isFabricated = true))
                source = ArrayList(transitional)
            }
        }

        Box {
            LazyRow(
                state = lazyListState,
                modifier = Modifier
            ) {
                itemsIndexed(source, key = { _, item -> item.key }) { index, item ->
                    AutoScrollingName(autoScrollingModel = source[index])
                    Spacer(modifier = Modifier.width(5.dp))
                }
            }
        }

        LaunchedEffect(Unit) {
            snapshotFlow { lazyListState.layoutInfo }
                .filter { it.visibleItemsInfo.isNotEmpty() }
                .distinctUntilChanged()
//                .onStart {
//                    val visibleItemsInfo = lazyListState.layoutInfo.visibleItemsInfo
//                    if (visibleItemsInfo.isEmpty()) return@onStart
//                    val lastVisibleItem = visibleItemsInfo.last()
//                    val viewportDimension = lazyListState.layoutInfo.viewportEndOffset + lazyListState.layoutInfo.viewportStartOffset
//
//                    if (source.size == (viewportDimension / lastVisibleItem.size) + 1) {
//                        val transitional = (source + source[0].copy(key = UUID.randomUUID(), isFabricated = true))
//                        source = ArrayList(transitional)
//                    }
//                }
                .collectLatest {
                    if (lazyListState.firstVisibleItemIndex == 0) source = ArrayList(source)
                    else {
                        val historicList = ArrayList(source.subList(1, source.size))
                        source =
                            if (source.last().isFabricated)
                                ArrayList(historicList.onEach { it.isFabricated = false } + historicList.first().copy(key = UUID.randomUUID(), isFabricated = true))
                            else
                                ArrayList(historicList + source.first())
                    }
                }
        }

        LaunchedEffect(key1 = Unit) {
            coroutineScope.launch {
                while (true) {
                    lazyListState.autoScroll()
                }
            }
        }
    }

this code works fine whether I use this

        LaunchedEffect(lazyListState) {
                val visibleItemsInfo = lazyListState.layoutInfo.visibleItemsInfo
                if (visibleItemsInfo.isEmpty()) return@LaunchedEffect // ALWAYS TRUE
                val lastVisibleItem = visibleItemsInfo.last()
                val viewportDimension = lazyListState.layoutInfo.viewportEndOffset + lazyListState.layoutInfo.viewportStartOffset
    
                if (source.size == (viewportDimension / lastVisibleItem.size) + 1) {
                    val transitional = (source + source[0].copy(key = UUID.randomUUID(), isFabricated = true))
                    source = ArrayList(transitional)
                }
            }

or this:-

//                .onStart {
//                    val visibleItemsInfo = lazyListState.layoutInfo.visibleItemsInfo
//                    if (visibleItemsInfo.isEmpty()) return@onStart // ALWAYS TRUE
//                    val lastVisibleItem = visibleItemsInfo.last()
//                    val viewportDimension = lazyListState.layoutInfo.viewportEndOffset + lazyListState.layoutInfo.viewportStartOffset
//
//                    if (source.size == (viewportDimension / lastVisibleItem.size) + 1) {
//                        val transitional = (source + source[0].copy(key = UUID.randomUUID(), isFabricated = true))
//                        source = ArrayList(transitional)
//                    }
//                }

However when i have incorporated this code into my production application niether the LaunchedEffect or onStart work.

this results in the side show animation only running once and not looping indefinately. one difference is that my production application has to employ androidxpose.ui.platform.ComposeView as the majority of my production application is still using xml layouts.

The issue appears to be that visibleItemsInfo.isEmpty() is always true in both the onStart and LaunchedEffect blocks in my production app where in my spike app the list is populated.

I have investigated infinate scrolling in compose and have it working as required in a spike i have been working on recently...

heres the code:-

data class AutoScrollingModel(val key: UUID = UUID.randomUUID(), @DrawableRes val iconResource: Int, val contentDescription: String, var isFabricated: Boolean = false)

val autoScrollingList = listOf<AutoScrollingModel>(
    AutoScrollingModel(iconResource = R.drawable.apple, contentDescription = "Apple"),
    AutoScrollingModel(iconResource = R.drawable.banana, contentDescription = "Banana"),
    AutoScrollingModel(iconResource = R.drawable.cherries, contentDescription = "cherries"),
    AutoScrollingModel(iconResource = R.drawable.dates, contentDescription = "dates"),
)

the compose code resembles this:-

    @Composable
    fun MainUi(modifier: Modifier) {

        val lazyListState = rememberLazyListState()
        val coroutineScope = rememberCoroutineScope()

        var source by remember { mutableStateOf(autoScrollingList) }

        LaunchedEffect(lazyListState) {
            val visibleItemsInfo = lazyListState.layoutInfo.visibleItemsInfo
            if (visibleItemsInfo.isEmpty()) return@LaunchedEffect
            val lastVisibleItem = visibleItemsInfo.last()
            val viewportDimension = lazyListState.layoutInfo.viewportEndOffset + lazyListState.layoutInfo.viewportStartOffset

            if (source.size == (viewportDimension / lastVisibleItem.size) + 1) {
                val transitional = (source + source[0].copy(key = UUID.randomUUID(), isFabricated = true))
                source = ArrayList(transitional)
            }
        }

        Box {
            LazyRow(
                state = lazyListState,
                modifier = Modifier
            ) {
                itemsIndexed(source, key = { _, item -> item.key }) { index, item ->
                    AutoScrollingName(autoScrollingModel = source[index])
                    Spacer(modifier = Modifier.width(5.dp))
                }
            }
        }

        LaunchedEffect(Unit) {
            snapshotFlow { lazyListState.layoutInfo }
                .filter { it.visibleItemsInfo.isNotEmpty() }
                .distinctUntilChanged()
//                .onStart {
//                    val visibleItemsInfo = lazyListState.layoutInfo.visibleItemsInfo
//                    if (visibleItemsInfo.isEmpty()) return@onStart
//                    val lastVisibleItem = visibleItemsInfo.last()
//                    val viewportDimension = lazyListState.layoutInfo.viewportEndOffset + lazyListState.layoutInfo.viewportStartOffset
//
//                    if (source.size == (viewportDimension / lastVisibleItem.size) + 1) {
//                        val transitional = (source + source[0].copy(key = UUID.randomUUID(), isFabricated = true))
//                        source = ArrayList(transitional)
//                    }
//                }
                .collectLatest {
                    if (lazyListState.firstVisibleItemIndex == 0) source = ArrayList(source)
                    else {
                        val historicList = ArrayList(source.subList(1, source.size))
                        source =
                            if (source.last().isFabricated)
                                ArrayList(historicList.onEach { it.isFabricated = false } + historicList.first().copy(key = UUID.randomUUID(), isFabricated = true))
                            else
                                ArrayList(historicList + source.first())
                    }
                }
        }

        LaunchedEffect(key1 = Unit) {
            coroutineScope.launch {
                while (true) {
                    lazyListState.autoScroll()
                }
            }
        }
    }

this code works fine whether I use this

        LaunchedEffect(lazyListState) {
                val visibleItemsInfo = lazyListState.layoutInfo.visibleItemsInfo
                if (visibleItemsInfo.isEmpty()) return@LaunchedEffect // ALWAYS TRUE
                val lastVisibleItem = visibleItemsInfo.last()
                val viewportDimension = lazyListState.layoutInfo.viewportEndOffset + lazyListState.layoutInfo.viewportStartOffset
    
                if (source.size == (viewportDimension / lastVisibleItem.size) + 1) {
                    val transitional = (source + source[0].copy(key = UUID.randomUUID(), isFabricated = true))
                    source = ArrayList(transitional)
                }
            }

or this:-

//                .onStart {
//                    val visibleItemsInfo = lazyListState.layoutInfo.visibleItemsInfo
//                    if (visibleItemsInfo.isEmpty()) return@onStart // ALWAYS TRUE
//                    val lastVisibleItem = visibleItemsInfo.last()
//                    val viewportDimension = lazyListState.layoutInfo.viewportEndOffset + lazyListState.layoutInfo.viewportStartOffset
//
//                    if (source.size == (viewportDimension / lastVisibleItem.size) + 1) {
//                        val transitional = (source + source[0].copy(key = UUID.randomUUID(), isFabricated = true))
//                        source = ArrayList(transitional)
//                    }
//                }

However when i have incorporated this code into my production application niether the LaunchedEffect or onStart work.

this results in the side show animation only running once and not looping indefinately. one difference is that my production application has to employ androidxpose.ui.platform.ComposeView as the majority of my production application is still using xml layouts.

The issue appears to be that visibleItemsInfo.isEmpty() is always true in both the onStart and LaunchedEffect blocks in my production app where in my spike app the list is populated.

Share Improve this question edited Feb 11 at 15:33 Hector asked Feb 11 at 15:28 HectorHector 5,42828 gold badges131 silver badges251 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 1

Delay Execution Until the Layout is Ready Try using snapshotFlow with an additional .debounce() to wait for the layout to settle before processing

LaunchedEffect(lazyListState) {
    snapshotFlow { lazyListState.layoutInfo }
        .debounce(100) // Delay slightly to ensure measurement happens
        .filter { it.visibleItemsInfo.isNotEmpty() }
        .distinctUntilChanged()
        .collectLatest {
            val visibleItemsInfo = it.visibleItemsInfo
            if (visibleItemsInfo.isNotEmpty()) {
                val lastVisibleItem = visibleItemsInfo.last()
                val viewportDimension = it.viewportEndOffset + it.viewportStartOffset

                if (source.size == (viewportDimension / lastVisibleItem.size) + 1) {
                    val transitional = source + source[0].copy(key = UUID.randomUUID(), isFabricated = true)
                    source = ArrayList(transitional)
                }
            }
        }
}

Or

Try Adding a Timeout in LaunchedEffect If ComposeView initializes late, you can try adding a timeout to wait before checking visibleItemsInfo

LaunchedEffect(Unit) {
    delay(500) // Allow ComposeView to initialize
    snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
        .filter { it.isNotEmpty() }
        .collectLatest { visibleItems ->
            println("Visible Items: $visibleItems")
        }
}

本文标签: