在Flutter中实现带固定索引项的垂直列表
实现带固定着色索引容器的索引式ListView(Flutter)
核心思路
要实现你描述的索引式ListView(带固定分类标题容器+侧边索引栏),需要结合CustomScrollView+SliverPersistentHeader实现固定着色的分类标题,配合侧边索引栏的手势监听控制列表滚动,同时同步索引高亮状态。
1. 定义数据结构
先统一管理分类和对应列表项的数据:
class FoodCategory { final String title; final String indexChar; final List<String> items; FoodCategory({required this.title, required this.indexChar, required this.items}); } // 模拟业务数据 final List<FoodCategory> foodCategories = [ FoodCategory(title: "Breakfast", indexChar: "B", items: ["Pancakes", "Omelette", "Avocado Toast"]), FoodCategory(title: "Lunch", indexChar: "L", items: ["Classic Burger", "Grilled Chicken Salad", "Tomato Pasta"]), FoodCategory(title: "Dinner", indexChar: "D", items: ["Steak", "Salmon", "Vegetable Curry"]), FoodCategory(title: "Desserts", indexChar: "D", items: ["Cheesecake", "Ice Cream", "Tiramisu"]), ];
2. 实现固定着色的分类标题容器
自定义SliverPersistentHeaderDelegate,让分类标题在滚动时固定在顶部,并且使用固定着色背景:
class CategoryHeaderDelegate extends SliverPersistentHeaderDelegate { final String categoryTitle; final Color bgColor; CategoryHeaderDelegate({required this.categoryTitle, required this.bgColor}); @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return Container( color: bgColor, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), alignment: Alignment.centerLeft, child: Text( categoryTitle, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white, ), ), ); } @override double get maxExtent => 50; @override double get minExtent => 50; @override bool shouldRebuild(CategoryHeaderDelegate oldDelegate) { return categoryTitle != oldDelegate.categoryTitle || bgColor != oldDelegate.bgColor; } }
3. 组装完整的索引式ListView
结合CustomScrollView构建主列表,同时添加侧边索引栏,实现点击跳转和滚动同步高亮:
class IndexedFoodListView extends StatefulWidget { const IndexedFoodListView({super.key}); @override State<IndexedFoodListView> createState() => _IndexedFoodListViewState(); } class _IndexedFoodListViewState extends State<IndexedFoodListView> { final ScrollController _scrollController = ScrollController(); int _activeIndex = 0; late List<double> _categoryScrollOffsets; @override void initState() { super.initState(); // 预计算每个分类的滚动偏移量(可根据实际布局调整) _categoryScrollOffsets = List.generate(foodCategories.length, (index) { double offset = 0; for (int i = 0; i < index; i++) { // 每个标题占50高度,每个列表项占60高度 offset += 50 + foodCategories[i].items.length * 60; } return offset; }); // 监听滚动,更新当前活跃的分类索引 _scrollController.addListener(() { for (int i = foodCategories.length - 1; i >= 0; i--) { if (_scrollController.offset >= _categoryScrollOffsets[i]) { if (_activeIndex != i) { setState(() => _activeIndex = i); } break; } } }); } @override Widget build(BuildContext context) { return Row( children: [ // 主列表区域 Expanded( child: CustomScrollView( controller: _scrollController, slivers: [ // 遍历生成每个分类的固定标题和列表项 for (int i = 0; i < foodCategories.length; i++) ...[ SliverPersistentHeader( pinned: true, delegate: CategoryHeaderDelegate( categoryTitle: foodCategories[i].title, bgColor: Colors.orange.shade700, // 固定着色背景 ), ), SliverList( delegate: SliverChildBuilderDelegate( (context, itemIndex) { return ListTile( title: Text(foodCategories[i].items[itemIndex]), onTap: () { // 列表项点击逻辑 }, ); }, childCount: foodCategories[i].items.length, ), ), ], ], ), ), // 侧边索引栏 Container( width: 45, color: Colors.grey.shade100, child: ListView.builder( itemCount: foodCategories.length, itemBuilder: (context, index) { return GestureDetector( onTap: () { // 点击索引,平滑滚动到对应分类 _scrollController.animateTo( _categoryScrollOffsets[index], duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); }, child: Container( height: 45, alignment: Alignment.center, // 当前活跃索引的高亮样式 color: _activeIndex == index ? Colors.orange.shade200 : Colors.transparent, child: Text( foodCategories[index].indexChar, style: TextStyle( fontWeight: _activeIndex == index ? FontWeight.bold : FontWeight.normal, color: _activeIndex == index ? Colors.orange.shade700 : Colors.grey.shade600, ), ), ), ); }, ), ), ], ); } @override void dispose() { _scrollController.dispose(); super.dispose(); } }
关键细节说明
SliverPersistentHeader的pinned: true属性确保分类标题在滚动时固定在顶部,不会随列表滚动消失。- 侧边索引栏通过
GestureDetector监听点击事件,调用ScrollController.animateTo实现平滑滚动到目标分类位置。 - 滚动监听逻辑用于同步当前显示的分类,更新侧边索引的高亮状态,提升交互体验。
- 分类标题容器的背景色可根据需求自定义,实现固定着色效果。
内容的提问来源于stack exchange,提问作者Shehzad Raheem
相关产品推荐
相关产品推荐

