Flutter自定义可滚动列表如何实现顶部项高亮及吸附效果?
Flutter 可变高度列表实现顶部高亮+吸附效果方案
针对你需要的可变高度列表项、滚动时顶部项高亮、滚动停止后吸附到顶部的需求,这里提供一个基于CustomScrollView+ScrollController的自定义实现方案,直接上代码和关键逻辑说明:
核心实现代码
import 'package:flutter/material.dart'; class ListItem { final String content; final double height; ListItem(this.content, this.height); } class StickyHighlightList extends StatefulWidget { const StickyHighlightList({super.key}); @override State<StickyHighlightList> createState() => _StickyHighlightListState(); } class _StickyHighlightListState extends State<StickyHighlightList> { final ScrollController _scrollController = ScrollController(); final List<ListItem> _items = [ ListItem('项1 - 高度150', 150), ListItem('项2 - 高度200', 200), ListItem('项3 - 高度100', 100), ListItem('项4 - 高度250', 250), ListItem('项5 - 高度180', 180), ListItem('项6 - 高度120', 120), ListItem('项7 - 高度220', 220), ]; final List<GlobalKey> _itemKeys = []; int _highlightedIndex = 0; bool _isScrolling = false; @override void initState() { super.initState(); // 给每个列表项分配唯一GlobalKey,用于获取布局位置 for (int i = 0; i < _items.length; i++) { _itemKeys.add(GlobalKey()); } _scrollController.addListener(_onScroll); } @override void dispose() { _scrollController.dispose(); super.dispose(); } void _onScroll() { setState(() { _isScrolling = _scrollController.position.isScrollingNotifier.value; }); // 滚动中实时更新高亮项,滚动结束触发吸附 if (!_isScrolling) { _updateHighlightedIndex(); _stickToTopItem(); } else { _updateHighlightedIndex(); } } void _updateHighlightedIndex() { final scrollOffset = _scrollController.offset; final topPadding = MediaQuery.of(context).padding.top; int newIndex = _highlightedIndex; for (int i = 0; i < _itemKeys.length; i++) { final renderBox = _itemKeys[i].currentContext?.findRenderObject() as RenderBox?; if (renderBox == null) continue; final itemTop = renderBox.localToGlobal(Offset.zero).dy - topPadding; final itemBottom = itemTop + renderBox.size.height; // 判断滚动偏移是否落在当前项的范围内 if (scrollOffset >= itemTop && scrollOffset < itemBottom) { newIndex = i; break; } // 滚动到列表最顶部时,保持第一个项高亮 if (scrollOffset < itemTop && i == 0) { newIndex = 0; break; } } if (newIndex != _highlightedIndex) { setState(() => _highlightedIndex = newIndex); } } void _stickToTopItem() { final renderBox = _itemKeys[_highlightedIndex].currentContext?.findRenderObject() as RenderBox?; if (renderBox == null) return; final topPadding = MediaQuery.of(context).padding.top; final itemTop = renderBox.localToGlobal(Offset.zero).dy - topPadding; final currentOffset = _scrollController.offset; // 如果当前高亮项顶部未对齐滚动视图顶部,触发吸附动画 if (itemTop != currentOffset) { _scrollController.animateTo( itemTop, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('高亮吸附列表')), body: CustomScrollView( controller: _scrollController, slivers: [ SliverList( delegate: SliverChildBuilderDelegate( (context, index) { final item = _items[index]; return Container( key: _itemKeys[index], height: item.height, color: _highlightedIndex == index ? Colors.lightBlue[100] : Colors.white, padding: const EdgeInsets.all(16), child: Text( item.content, style: TextStyle( fontSize: 18, fontWeight: _highlightedIndex == index ? FontWeight.bold : FontWeight.normal, ), ), ); }, childCount: _items.length, ), ), ], ), ); } }
关键逻辑说明
- GlobalKey 跟踪项位置:每个列表项绑定一个
GlobalKey,通过它可以获取项的RenderBox,计算出项在滚动视图中的顶部和底部偏移量,用于判断当前滚动位置对应的顶部项。 - 滚动监听与高亮更新:通过
ScrollController监听滚动状态,滚动过程中实时计算当前顶部可见项,更新高亮样式;滚动停止时触发吸附逻辑。 - 吸附效果实现:滚动停止后,检查当前高亮项的顶部是否与滚动视图顶部对齐,若未对齐,调用
animateTo滚动到对应位置,实现平滑吸附。 - 可变高度兼容:不管是固定高度还是内容自适应高度的项,
RenderBox都会自动获取实际高度,无需额外处理。
优化与注意事项
- 大数据列表优化:如果列表项数量极多,遍历所有
GlobalKey会有性能损耗,可以只跟踪当前可见区域附近的项(结合ScrollController的position.viewportDimension计算可见范围)。 - 动画参数调整:可以修改
animateTo的duration和curve参数,让吸附过渡更符合你的交互需求。 - 自定义高亮样式:除了背景色和字体粗细,还可以添加边框、阴影等样式,强化高亮效果。
内容的提问来源于stack exchange,提问作者Eagle
相关产品推荐
相关产品推荐

