Flutter自定义组件: 为横向列表自定义“进度条”式滚动指示器

之前写过一篇Android 自定义 View :打造一个跟随滑动的丝滑指示器,今天使用 Flutter 来实现一个根据列表滚动动态偏移的自定义指示器。

本文将实现一个根据列表滚动动态偏移的自定义指示器。


1. 核心原理

实现这个效果的关键在于:监听滚动事件,并计算滚动比例。

  1. 监听滚动 :使用 NotificationListener<ScrollNotification> 捕捉滚动进度。
  2. 计算比例滚动比例 = 当前滚动偏移量 / 最大可滚动距离
  3. 联动指示器:根据比例计算指示器"滑块"的位移。

2. 准备工作

我们需要一个基本的横向列表结构。这里建议使用 SingleChildScrollView 配合 ScrollController

Dart 复制代码
final ScrollController _scrollController = ScrollController();
double _progress = 0.0; // 存储滚动比例 (0.0 ~ 1.0)

3. 实现步骤

第一步:构建横向列表

我们使用 NotificationListener 包裹滚动视图,在 onNotification 回调中计算进度。

Dart 复制代码
NotificationListener<ScrollNotification>(
  onNotification: (ScrollNotification notification) {
    // 只有在滚动时才更新进度
    if (notification is ScrollUpdateNotification) {
      setState(() {
        // 计算滚动比例:当前位置 / 最大滚动范围
        _progress = _scrollController.offset / _scrollController.position.maxScrollExtent;
        // 确保进度在 0~1 之间
        _progress = _progress.clamp(0.0, 1.0);
      });
    }
    return true;
  },
  child: SingleChildScrollView(
    controller: _scrollController,
    scrollDirection: Axis.horizontal,
    child: Row(
      children: List.generate(10, (index) => _buildItem(index)),
    ),
  ),
)

第二步:自定义指示器组件

指示器由两部分组成:底槽 (Track)滑块 (Thumb) 。滑块的位置通过 _progress 动态计算。

Dart 复制代码
Widget _buildIndicator() {
  const double trackWidth = 40.0; // 底槽宽度
  const double thumbWidth = 20.0; // 滑块宽度

  return Container(
    width: trackWidth,
    height: 4.0,
    decoration: BoxDecoration(
      color: Colors.grey[300], // 底槽颜色
      borderRadius: BorderRadius.circular(2.0),
    ),
    child: Stack(
      children: [
        Positioned(
          // 核心逻辑:计算滑块的左间距
          // 左间距 = 比例 * (底槽宽度 - 滑块宽度)
          left: _progress * (trackWidth - thumbWidth),
          child: Container(
            width: thumbWidth,
            height: 4.0,
            decoration: BoxDecoration(
              color: Colors.blue, // 滑块颜色
              borderRadius: BorderRadius.circular(2.0),
            ),
          ),
        ),
      ],
    ),
  );
}

4. 完整代码示例

下面是将上述逻辑整合后的一个完整 Widget 示例:

Dart 复制代码
import 'package:flutter/material.dart';

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

  @override
  State<CustomScrollIndicatorDemo> createState() => _CustomScrollIndicatorDemoState();
}

class _CustomScrollIndicatorDemoState extends State<CustomScrollIndicatorDemo> {
  final ScrollController _scrollController = ScrollController();
  double _progress = 0.0;

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

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        // 1. 列表部分
        SizedBox(
          height: 100,
          child: NotificationListener<ScrollNotification>(
            onNotification: (notification) {
              if (notification is ScrollUpdateNotification) {
                setState(() {
                  // 计算滚动比例
                  if (_scrollController.hasClients) {
                    _progress = _scrollController.offset / 
                               _scrollController.position.maxScrollExtent;
                    _progress = _progress.clamp(0.0, 1.0);
                  }
                });
              }
              return true;
            },
            child: SingleChildScrollView(
              controller: _scrollController,
              scrollDirection: Axis.horizontal,
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: Row(
                children: List.generate(10, (index) => _buildItem(index)),
              ),
            ),
          ),
        ),
        const SizedBox(height: 12),
        // 2. 指示器部分
        _buildIndicator(),
      ],
    );
  }

  // 模拟列表项
  Widget _buildItem(int index) {
    return Container(
      width: 60,
      margin: const EdgeInsets.only(right: 20),
      child: Column(
        children: [
          Container(
            width: 50,
            height: 50,
            decoration: BoxDecoration(
              color: Colors.blue[50],
              borderRadius: BorderRadius.circular(12),
            ),
            child: Icon(Icons.category, color: Colors.blue[400]),
          ),
          const SizedBox(height: 8),
          Text('分类 $index', style: const TextStyle(fontSize: 12)),
        ],
      ),
    );
  }

  // 构建指示器
  Widget _buildIndicator() {
    return Container(
      width: 40,
      height: 4,
      decoration: BoxDecoration(
        color: Colors.black12,
        borderRadius: BorderRadius.circular(2),
      ),
      alignment: Alignment.centerLeft,
      child: FractionallySizedBox(
        widthFactor: 1.0, // 占满父容器,配合下面的布局
        child: Stack(
          children: [
            Positioned(
              left: _progress * (40 - 20), // 40是总宽,20是滑块宽
              child: Container(
                width: 20,
                height: 4,
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

5. 进阶优化建议

  1. 动态计算滑块宽度 :如果你的列表项数量是动态的,你可以根据 viewportDimension / contentDimension 的比例来动态设置滑块宽度,这样指示器的体验会更接近原生滚动条。
  2. 缓动动画 :如果你希望指示器移动更丝滑,可以考虑使用 AnimatedPositioned 配合较短的动画时间,或者直接使用 CustomPainter 来绘制。
  3. 封装组件 :将这个逻辑封装成一个 CustomScrollbar 组件,方便在不同页面复用。

总结

通过 NotificationListener 结合 ScrollController,我们可以轻松获取滚动的实时进度。利用这个进度来驱动 StackPositioned 的位移,就能实现任何你想要的自定义指示器效果。

相关推荐
忆江南1 天前
iOS 深度解析
flutter·ios
明君879971 天前
Flutter 实现 AI 聊天页面 —— 记一次 Markdown 数学公式显示的踩坑之旅
前端·flutter
恋猫de小郭1 天前
移动端开发稳了?AI 目前还无法取代客户端开发,小红书的论文告诉你数据
前端·flutter·ai编程
MakeZero1 天前
Flutter那些事-交互式组件
flutter
shankss1 天前
pull_to_refresh_simple
flutter
shankss1 天前
Flutter 下拉刷新库新特性:智能预加载 (enableSmartPreload) 详解
flutter
SoaringHeart3 天前
Flutter调试组件:打印任意组件尺寸位置信息 NRenderBox
前端·flutter
九狼3 天前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
_squirrel3 天前
记录一次 Flutter 升级遇到的问题
flutter
Haha_bj3 天前
Flutter——状态管理 Provider 详解
flutter·app