kotlin 如何检查Composable的可见性百分比?

mjqavswn  于 2023-10-23  发布在  Kotlin
关注(0)|答案(2)|浏览(169)

我有一个Composable是在一个LazyColumn,但我试图做的可见性检查出的LazyListState我用这个

fun LazyListState.visibleItems(itemVisiblePercentThreshold: Float) =
    layoutInfo
        .visibleItemsInfo
        .filter {
            visibilityPercent(it) >= itemVisiblePercentThreshold
        }

private 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)
}

但是现在我想让它在Composable里面,因为我没有访问列表的权限,其他功能使用我的Composables,它可能在LazyListSurface,无论什么里面,所以我使用onGloballyPositioned来确定它是否可见,但是我想知道它是否至少30%可见。有什么想法吗?

说明

我有一个@Composable,我想提供给我的应用程序的其他功能,例如:
LazyColumn打印它的项目,但是他们想把我的@Composable添加到列表的顶部,所以我想知道我的@Composable何时可见(至少30%)在这种情况下,如果它在顶部,通常应该始终是100%当加载列表时,但例如在Appendix 2中,他们希望将我的@Composable添加到列表的中间,所以我应该知道它何时开始可见,这就是为什么我需要阈值来发送一些事件。
它不会总是一个LazyColumn,这就是为什么我不知道我是否想连接到一个LazyListState,但如果需要,我可以收到一个LazyListState对我的@Composable

vbkedwbf

vbkedwbf1#

如果你想在任何Compoasble中使用它,你可以使用Modifier.onGloballyPositioned{}

fun Modifier.isVisible(
    parentCoordinates: LayoutCoordinates?,
    threshold: Int,
    onVisibilityChange: (Boolean) -> Unit
) = composed {

    val view = LocalView.current

    Modifier.onGloballyPositioned { layoutCoordinates: LayoutCoordinates ->

        if (parentCoordinates == null) return@onGloballyPositioned

        val layoutHeight = layoutCoordinates.size.height
        val thresholdHeight = layoutHeight * threshold / 100
        val layoutTop = layoutCoordinates.positionInRoot().y

        val parentTop = parentCoordinates.positionInParent().y

        val parentHeight = parentCoordinates.size.height
        val parentBottom = (parentTop + parentHeight).coerceAtMost(view.height.toFloat())
        println(
            "layoutTop: $layoutTop, " +
                    " parentTop: $parentTop, " +
                    " parentBottom: $parentBottom, " +
                    "parentHeight: $parentHeight, " +
                    "SECTION: ${parentBottom - layoutTop}"
        )

        if (
            parentBottom - layoutTop > thresholdHeight &&
            (layoutTop - parentTop > thresholdHeight - layoutHeight)
        ) {
            onVisibilityChange(true)
        } else {
            onVisibilityChange(false)

        }
    }
}

我发布了一个有点复杂的例子来展示你可以得到位置,不管parent在它的parent中的位置。
您可以使用它与LazyColumn或列与垂直滚动。

@Preview
@Composable
private fun ScrollTest() {

    var isVisible by remember {
        mutableStateOf(false)
    }

    var coordinates by remember {
        mutableStateOf<LayoutCoordinates?>(null)
    }

    val context = LocalContext.current

    var visibleTime by remember {
        mutableLongStateOf(0L)
    }

    LaunchedEffect(isVisible) {

        if (isVisible) {
            visibleTime = System.currentTimeMillis()
            Toast.makeText(context, "😆 Item 30% threshold is passed $isVisible", Toast.LENGTH_SHORT)
                .show()
        } else if (visibleTime != 0L) {
            val currentTime = System.currentTimeMillis()
            val totalTime = currentTime - visibleTime
            Toast.makeText(context, "🥵 Item was visible for $totalTime ms", Toast.LENGTH_SHORT)
                .show()
        }
    }

    Column {

        Box(modifier = Modifier.height(100.dp))

        LazyColumn(
            modifier = Modifier
                .onPlaced { layoutCoordinates: LayoutCoordinates ->
                    coordinates = layoutCoordinates
                }
                .weight(1f)
                .fillMaxSize()
                .border(2.dp, Color.Black)
        ) {
            items(60) { index: Int ->
                if (index == 15) {
                    Column(
                        modifier = Modifier.fillMaxWidth().height(300.dp)
                            .border(6.dp, if (isVisible) Color.Green else Color.Red)
                            .isVisible(parentCoordinates = coordinates, threshold = 30) {
                                isVisible = it
                            }
                    ) {
                        Box(modifier = Modifier.fillMaxWidth().weight(3f).background(Color.Yellow))
                        Box(modifier = Modifier.fillMaxWidth().weight(4f).background(Color.Cyan))
                        Box(modifier = Modifier.fillMaxWidth().weight(3f).background(Color.Magenta))
                    }
                } else {
                    Text(
                        text = "Row $index",
                        fontSize = 24.sp,
                        modifier = Modifier.fillMaxWidth().padding(8.dp)
                    )
                }
            }
        }

//        Column(
//            modifier = Modifier
//                .onPlaced { layoutCoordinates: LayoutCoordinates ->
//                    coordinates = layoutCoordinates
//                }
//                .weight(1f)
//                .fillMaxSize()
//                .border(2.dp, Color.Black)
//                .verticalScroll(rememberScrollState())
//        ) {
//            repeat(60) { index ->
//                if (index == 15) {
//                    Column(
//                        modifier = Modifier.fillMaxWidth().height(300.dp)
//                            .border(6.dp, if (isVisible) Color.Green else Color.Red)
//                            .isVisible(parentCoordinates = coordinates, threshold = 30) {
//                                isVisible = it
//                            }
//                    ) {
//                        Box(modifier = Modifier.fillMaxWidth().weight(3f).background(Color.Yellow))
//                        Box(modifier = Modifier.fillMaxWidth().weight(4f).background(Color.Cyan))
//                        Box(modifier = Modifier.fillMaxWidth().weight(3f).background(Color.Magenta))
//                    }
//                } else {
//                    Text(
//                        text = "Row $index",
//                        fontSize = 24.sp,
//                        modifier = Modifier.fillMaxWidth().padding(8.dp)
//                    )
//                }
//            }
//        }
        Box(modifier = Modifier.height(100.dp))

    }
}

编辑

如果不想传递父LayoutCoordinates,可以将此修改器更新为

fun Modifier.isVisible(
    threshold: Int,
    onVisibilityChange: (Boolean) -> Unit
) = composed {
    
    Modifier.onGloballyPositioned { layoutCoordinates: LayoutCoordinates ->
        val layoutHeight = layoutCoordinates.size.height
        val thresholdHeight = layoutHeight * threshold / 100
        val layoutTop = layoutCoordinates.positionInRoot().y
        val layoutBottom = layoutTop + layoutHeight

        // This should be parentLayoutCoordinates not parentCoordinates
        val parent =
            layoutCoordinates.parentLayoutCoordinates

        parent?.boundsInRoot()?.let { rect: Rect ->
            val parentTop = rect.top
            val parentBottom = rect.bottom
            
            if (
                parentBottom - layoutTop > thresholdHeight &&
                (parentTop < layoutBottom - thresholdHeight)
            ) {
                onVisibilityChange(true)
            } else {
                onVisibilityChange(false)

            }
        }
    }
}

如果您想将其用于多个Composable,请创建一个自定义Composable,

@Composable
private fun MyCustomBox(
    modifier: Modifier = Modifier,
    threshold: Int = 30,
    content: @Composable () -> Unit
) {
    var isVisible by remember {
        mutableStateOf(false)
    }

    val context = LocalContext.current

    var visibleTime by remember {
        mutableLongStateOf(0L)
    }

    LaunchedEffect(isVisible) {

        if (isVisible) {
            visibleTime = System.currentTimeMillis()
            Toast.makeText(context, "😆 Item 30% threshold is passed $isVisible", Toast.LENGTH_SHORT)
                .show()
        } else if (visibleTime != 0L) {
            val currentTime = System.currentTimeMillis()
            val totalTime = currentTime - visibleTime
            Toast.makeText(context, "🥵 Item was visible for $totalTime ms", Toast.LENGTH_SHORT)
                .show()
        }
    }

    Box(
        modifier = modifier
            .border(6.dp, if (isVisible) Color.Green else Color.Red)
            .isVisible(threshold = threshold) {
                isVisible = it
            }
    ) {
        content()
    }
}

你可以用它作为

@Preview
@Composable
private fun ScrollTest2() {

    Column {

        TopAppBar {
            Text("TopAppbar")
        }
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
                .border(2.dp, Color.Black)
                .verticalScroll(rememberScrollState())
        ) {
            repeat(60) { index ->
                if (index == 15 || index == 22 || index == 35) {
                    MyCustomBox(
                        modifier = Modifier.fillMaxWidth().height(300.dp)
                    ) {
                        Column {
                            Box(
                                modifier = Modifier.fillMaxWidth().weight(3f)
                                    .background(Color.Yellow)
                            )
                            Box(
                                modifier = Modifier.fillMaxWidth().weight(4f).background(Color.Cyan)
                            )
                            Box(
                                modifier = Modifier.fillMaxWidth().weight(3f)
                                    .background(Color.Magenta)
                            )
                        }
                    }
                } else {
                    Text(
                        text = "Row $index",
                        fontSize = 24.sp,
                        modifier = Modifier.fillMaxWidth().padding(8.dp)
                    )
                }
            }
        }
        Box(modifier = Modifier.height(100.dp))

    }

}
cdmah0mi

cdmah0mi2#

您可以使用snapshotFlow轻松监控可见项目:

val listState = rememberLazyListState()
    val visibleItems by remember {
        snapshotFlow {
            val layoutInfo = listState.layoutInfo
            layoutInfo.visibleItemsInfo.map {
                val cutTop = (layoutInfo.viewportStartOffset - it.offset).coerceAtLeast(0)
                val cutBottom = (it.offset + it.size - layoutInfo.viewportEndOffset).coerceAtLeast(0)
                val visiblePart = (it.size.toFloat() - cutTop - cutBottom) / it.size
                it to visiblePart
            }
        }
    }.collectAsState(emptyList())
    LazyColumn(state = listState) {
        items((1..9).toList(), {it}) { key ->
            Box(Modifier.height(100.dp * key).border(2.dp, Color.Black)) {
                val measurement = visibleItems.firstOrNull { it.first.key == key }?.second ?: 0f
                Text("Item $key: Visible part $measurement")
                Text("Item $key: Visible part $measurement", Modifier.align(Alignment.BottomCenter))
            }
        }
    }

这将重新计算每个项目的可见程度,每当滚动状态发生变化时,然后在UI中显示可见部分。

相关问题