Jetpack Compose:如何仅为LazyColumn顶部粘性头部添加滚动动画
实现仅顶部粘性头部的滚动动画效果
问题背景
想要实现用户滚动粘性头部所属分组列表项时,仅顶部的粘性头部添加动画效果,但当前代码上下滚动时所有粘性头部都会触发动画。尝试通过firstVisibleItemIndex获取第一个可见头部索引,但该值对应的是分组的第一个列表项而非头部,LazyListState本身不支持直接获取粘性头部索引。
解决方案
思路一:通过滚动偏移量计算顶部头部索引
适合头部和列表项高度固定的场景,通过预计算分组总高度,结合滚动偏移量定位当前顶部粘性头部:
1. 预计算分组总高度
val listState = rememberLazyListState() // 转换固定高度为像素值 val headerHeight = with(LocalDensity.current) { 60.dp.toPx() } val itemHeight = with(LocalDensity.current) { 48.dp.toPx() } // 计算每个分组的总高度(头部+所有列表项) val groupTotalHeights = remember(categories) { categories.map { headerHeight + it.items.size * itemHeight } } // 生成累计高度列表,用于快速匹配滚动位置 val cumulativeHeights = remember(groupTotalHeights) { mutableListOf<Float>().apply { var sum = 0f add(sum) groupTotalHeights.forEach { sum += it add(sum) } } }
2. 计算当前顶部头部索引
val currentTopHeaderIndex = remember(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { var totalOffset = 0f var currentGroupIndex = 0 var accumulatedItemCount = categories[currentGroupIndex].items.size // 确定firstVisibleItemIndex所属的分组 while (listState.firstVisibleItemIndex >= accumulatedItemCount) { totalOffset += groupTotalHeights[currentGroupIndex] currentGroupIndex++ accumulatedItemCount += categories[currentGroupIndex].items.size } // 添加当前分组内的滚动偏移 totalOffset += listState.firstVisibleItemScrollOffset // 匹配对应的顶部头部索引 cumulativeHeights.indexOfLast { it <= totalOffset } }
3. 绑定动画到顶部头部
val topHeaderScale = remember { mutableStateOf(1f) } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // 仅处理拖拽滚动,忽略惯性滚动 if (source == NestedScrollSource.Drag) { topHeaderScale.value = when { available.y < 0 -> max(0.8f, topHeaderScale.value - 0.01f) // 上滚缩小 available.y > 0 -> min(1f, topHeaderScale.value + 0.01f) // 下滚恢复 else -> topHeaderScale.value } } return Offset.Zero } } } // 构建LazyColumn LazyColumn( state = listState, modifier = modifier.nestedScroll(nestedScrollConnection) ) { categories.forEachIndexed { index, category -> stickyHeader { SessionCard( scale = if (index == currentTopHeaderIndex) topHeaderScale.value else 1f, index = index ) } items(category.items) { text -> CategoryItem(text) } } }
思路二:利用布局可见性回调判断顶部头部
适合高度不固定的场景,通过onGloballyPositioned实时检测头部位置:
val listState = rememberLazyListState() val topHeaderIndex = remember { mutableStateOf(0) } val topHeaderScale = remember { mutableStateOf(1f) } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { if (source == NestedScrollSource.Drag) { topHeaderScale.value = when { available.y < 0 -> max(0.8f, topHeaderScale.value - 0.01f) available.y > 0 -> min(1f, topHeaderScale.value + 0.01f) else -> topHeaderScale.value } } return Offset.Zero } } } LazyColumn( state = listState, modifier = modifier.nestedScroll(nestedScrollConnection) ) { categories.forEachIndexed { index, category -> stickyHeader { SessionCard( scale = if (index == topHeaderIndex.value) topHeaderScale.value else 1f, index = index ).apply { modifier.onGloballyPositioned { coordinates -> val viewportTop = listState.layoutInfo.viewportStartOffset val headerTop = coordinates.positionInParent().y val headerBottom = headerTop + coordinates.size.height // 判断当前头部是否处于视口顶部(粘性头部的特征位置) if (headerTop <= viewportTop && headerBottom >= viewportTop) { topHeaderIndex.value = index } } } } items(category.items) { text -> CategoryItem(text) } } }
优化建议
- 如果需要更平滑的动画,建议使用
Animatable替代直接修改scale值,例如:val scaleAnimatable = remember { Animatable(1f) } // 在滚动回调中使用scaleAnimatable.animateTo(...) - 避免在滚动回调中执行复杂计算,尽量用
remember和derivedStateOf缓存计算结果,提升性能
内容的提问来源于stack exchange,提问作者Pitos
相关产品推荐
相关产品推荐

