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 的位移,就能实现任何你想要的自定义指示器效果。

相关推荐
程序员老刘·6 小时前
Android Studio Otter 3 发布:日常开发选AS还是Cursor?
flutter·android studio·ai编程·跨平台开发·客户端开发
浩辉_6 小时前
Dart - 内存管理与垃圾回收(GC)深度解析
flutter·dart
一起养小猫8 小时前
Flutter for OpenHarmony 实战:记忆棋游戏完整开发指南
flutter·游戏·harmonyos
Betelgeuse769 小时前
【Flutter For OpenHarmony】TechHub技术资讯界面开发
flutter·ui·华为·交互·harmonyos
铅笔侠_小龙虾10 小时前
Flutter 安装&配置
flutter
mocoding11 小时前
使用已经完成鸿蒙化适配的Flutter本地持久化存储三方库shared_preferences让你的应用能够保存用户偏好设置、缓存数据等
flutter·华为·harmonyos·鸿蒙
无熵~12 小时前
Flutter入门
flutter
hudawei99613 小时前
要控制动画的widget为什么要with SingleTickerProviderStateMixin
flutter·mixin·with·ticker·动画控制
jian1105814 小时前
flutter dio 依赖,dependencies 和 dev_dependencies的区别
flutter
王码码203515 小时前
Flutter for OpenHarmony 实战之基础组件:第十七篇 滚动进阶 ScrollController 与 Scrollbar
flutter·harmonyos