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

如何在Flutter中实现带吸附与选中指示器的横向滚动组件?

Flutter 实现带吸附与选中指示器的横向滚动组件

ListWheelScrollView更适合模拟滚轮选择器的交互,横向普通列表场景下用它确实容易出现适配问题,推荐用下面两种方案实现需求:

方案一:PageView + 自定义指示器(全屏宽度项)

如果你的滚动项是全屏宽度的,PageView是最优选择,它默认自带pageSnapping(吸附)功能,配合自定义指示器就能快速实现效果:

import 'package:flutter/material.dart';

class SnapScrollWithIndicator extends StatefulWidget {
  const SnapScrollWithIndicator({super.key});

  @override
  State<SnapScrollWithIndicator> createState() => _SnapScrollWithIndicatorState();
}

class _SnapScrollWithIndicatorState extends State<SnapScrollWithIndicator> {
  final PageController _pageController = PageController();
  int _currentPage = 0;
  final List<String> _items = ['项1', '项2', '项3', '项4', '项5'];

  @override
  void initState() {
    super.initState();
    // 监听页面切换,更新选中索引
    _pageController.addListener(() {
      final page = _pageController.page?.round() ?? 0;
      if (page != _currentPage) {
        setState(() {
          _currentPage = page;
        });
      }
    });
  }

  @override
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: PageView.builder(
            controller: _pageController,
            // 开启吸附,默认就是true,可显式声明
            pageSnapping: true,
            itemCount: _items.length,
            itemBuilder: (context, index) {
              return Container(
                alignment: Alignment.center,
                color: index % 2 == 0 ? Colors.blue[100] : Colors.green[100],
                child: Text(
                  _items[index],
                  style: const TextStyle(fontSize: 24),
                ),
              );
            },
          ),
        ),
        // 底部选中指示器
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: List.generate(_items.length, (index) {
              return Container(
                width: 8,
                height: 8,
                margin: const EdgeInsets.symmetric(horizontal: 4),
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: _currentPage == index ? Colors.blue : Colors.grey[300],
                ),
              );
            }),
          ),
        ),
      ],
    );
  }
}

方案二:自定义横向滚动列表(非全屏宽度项)

如果你的滚动项宽度小于屏幕,需要滚动到中心时吸附并显示指示器,可以用SingleChildScrollView + ListView配合ScrollController实现:

import 'package:flutter/material.dart';

class SnapHorizontalList extends StatefulWidget {
  const SnapHorizontalList({super.key});

  @override
  State<SnapHorizontalList> createState() => _SnapHorizontalListState();
}

class _SnapHorizontalListState extends State<SnapHorizontalList> {
  final ScrollController _scrollController = ScrollController();
  int _selectedIndex = 0;
  final List<String> _items = ['卡片1', '卡片2', '卡片3', '卡片4', '卡片5'];
  final double _itemWidth = 180;
  final double _itemSpacing = 16;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }

  void _onScroll() {
    // 计算当前中心位置对应的item索引
    final centerOffset = _scrollController.offset + MediaQuery.of(context).size.width / 2;
    final itemIndex = (centerOffset / (_itemWidth + _itemSpacing)).floor();
    if (itemIndex != _selectedIndex && itemIndex >= 0 && itemIndex < _items.length) {
      setState(() {
        _selectedIndex = itemIndex;
      });
    }

    // 滚动停止时自动吸附到选中项中心
    if (!_scrollController.position.isScrollingNotifier.value) {
      final targetOffset = (_itemWidth + _itemSpacing) * _selectedIndex +
          (MediaQuery.of(context).size.width - _itemWidth) / 2;
      _scrollController.animateTo(
        targetOffset,
        duration: const Duration(milliseconds: 300),
        curve: Curves.ease,
      );
    }
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        SizedBox(
          height: 200,
          child: SingleChildScrollView(
            controller: _scrollController,
            scrollDirection: Axis.horizontal,
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              children: List.generate(_items.length, (index) {
                return Container(
                  width: _itemWidth,
                  margin: EdgeInsets.only(left: index == 0 ? 0 : _itemSpacing),
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: _selectedIndex == index ? Colors.blue[100] : Colors.grey[100],
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Text(
                    _items[index],
                    style: const TextStyle(fontSize: 18),
                  ),
                );
              }),
            ),
          ),
        ),
        // 选中指示器
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: List.generate(_items.length, (index) {
              return Container(
                width: 10,
                height: 10,
                margin: const EdgeInsets.symmetric(horizontal: 4),
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: _selectedIndex == index ? Colors.blue : Colors.grey[300],
                  border: _selectedIndex == index ? Border.all(color: Colors.blue[700], width: 2) : null,
                ),
              );
            }),
          ),
        ),
      ],
    );
  }
}

关键说明

  • 方案一适合全屏宽度的页面切换场景,PageView的pageSnapping默认开启,无需额外处理吸附逻辑
  • 方案二适合卡片式横向列表,通过监听滚动位置计算选中项,滚动停止时自动吸附到中心位置
  • 两种方案都可以根据需求自定义指示器样式(比如改成下划线、数字标签等)

内容的提问来源于stack exchange,提问作者Opeyemi Lawal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 13:22:50