You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 15:15:59