在 Jetpack Compose 上创建 UI 时,需要为 LazyRow 添加粘性标题。但当前的实现是作为行元素嵌入到列表中的。

我希望粘性标题位于元素上方,如下所示:

UPD:我开始尝试自己解决问题,但遇到了以下问题:

当我的 OffsetX 处于离开状态时,为 LazyRow 添加 itemSpacing 时它会中断

此外,对于较长的 StickyHeader,我不知道如何正确设置偏移量以将其长度考虑在内。

@Composable
fun <K, V> LazyRowWithStickyHeader(
    items: Map<K, List<V>>,
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    reverseLayout: Boolean = false,
    horizontalArrangement: Arrangement.Horizontal = if (!reverseLayout) Arrangement.Start else Arrangement.End,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
    userScrollEnabled: Boolean = true,
    stickyHeader: StickyHeaderScope.(K) -> Unit,
    itemContent: @Composable LazyItemScope.(V) -> Unit
) {
    val itemsWithKeys = remember(items) {
        items.flatMap { entry -> entry.value.map { entry.key to it } }
    }
    val textMeasurer = rememberTextMeasurer()
    var itemWidth by remember { mutableIntStateOf(0) }
    var stickyHeaderHeight by remember { mutableStateOf(0.dp) }

    Box(
        modifier = Modifier.drawWithCache {
            onDrawBehind {
                var previousKey: K? = null
                val startPadding = state.layoutInfo.beforeContentPadding
                if (itemWidth == 0) {
                    itemWidth = state.layoutInfo.visibleItemsInfo.firstOrNull()?.size ?: 0
                }
                state.layoutInfo.visibleItemsInfo.forEachIndexed { index, itemInfo ->
                    val currentKey = itemsWithKeys.getOrNull(itemInfo.index)?.first
                    val nextItemKey = itemsWithKeys.getOrNull(itemInfo.index + 1)?.first
                    if (currentKey == null || currentKey == previousKey) {
                        return@forEachIndexed
                    }

                    StickyHeaderScopeImpl(
                        drawScope = this,
                        textMeasurer = textMeasurer,
                        offsetProvider = { size ->
                            stickyHeaderHeight = size.height.toDp()
                            val offsetX = when {
                                //Stickying
                                currentKey == nextItemKey && index == 0 -> {
                                    startPadding
                                }
                                //Coming
                                currentKey == nextItemKey -> {
                                    (itemInfo.offset + startPadding).coerceAtLeast(startPadding)
                                }
                                //Leaving
                                else -> {
                                    itemInfo.offset + startPadding
                                }
                            }
                            Offset(x = offsetX.toFloat(), y = 0f)
                        }
                    ).stickyHeader(currentKey)
                    previousKey = currentKey
                }
            }
        }
    ) {
        LazyRow(
            modifier = modifier.padding(top = stickyHeaderHeight),
            state = state,
            contentPadding = contentPadding,
            reverseLayout = reverseLayout,
            horizontalArrangement = horizontalArrangement,
            verticalAlignment = verticalAlignment,
            flingBehavior = flingBehavior,
            userScrollEnabled = userScrollEnabled
        ) {
            items(
                items = itemsWithKeys
            ) { (_, value) ->
                itemContent(value)
            }
        }
    }
}

fun StickyHeaderScope.drawStickyHeader(
    text: String,
    style: TextStyle,
    color: Color = style.color,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
) {
    val textLayout = textMeasurer.measure(
        text = AnnotatedString(text),
        style = style,
        overflow = overflow,
        softWrap = softWrap,
        maxLines = maxLines
    )
    with(drawScope) {
        drawText(
            textLayoutResult = textLayout,
            color = color,
            topLeft = offsetProvider(textLayout.size)
        )
    }
}

interface StickyHeaderScope {
    val drawScope: DrawScope
    val textMeasurer: TextMeasurer
    val offsetProvider: (IntSize) -> Offset
}

private class StickyHeaderScopeImpl(
    override val drawScope: DrawScope,
    override val textMeasurer: TextMeasurer,
    override val offsetProvider: (IntSize) -> Offset
) : StickyHeaderScope

我的实现演示:

3

  • 请在问题本身中提供所有相关代码。指向外部网站的链接是不够的,因为链接可能会随着时间的推移而中断。


    – 

  • @tyg 我通过提供代码更新了这个问题


    – 

  • 2
    @xephosbot 你想要这样的东西吗这是我开始研究的一个库,我认为它完全满足你的需要(而且它也是多平台的,耶)。


    – 


最佳答案
1

我曾研究过一个 Compose Multiplatform 库,它为此提供了解决方案:

预览显示水平和垂直粘性标题:

附言:仅提供解决方案的链接通常不太好,但在这种情况下,将源代码复制到这里会很困难。