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

相关推荐
PWRJOY5 小时前
【flutter】项目配置文件 pubspec.yaml
flutter
徐安安ye6 小时前
Flutter 与 Rust 混合开发:打造毫秒级响应的高性能计算引擎
开发语言·flutter·rust
xianjixiance_15 小时前
Flutter跨平台三方库鸿蒙化适配指南
flutter·华为·harmonyos
SoaringHeart18 小时前
Flutter组件封装:视频播放组件全局封装
前端·flutter
程序员老刘21 小时前
Kotlin vs Dart:当“优雅”变成心智负担,我选择了更简单的 Dart
flutter·kotlin·dart
徐安安ye1 天前
Flutter 车载系统开发:打造符合 Automotive Grade Linux 标准的 HMI 应用
linux·flutter·车载系统
恋猫de小郭1 天前
2025 年终醒悟,AI 让我误以为自己很强,未来程序员的转型之路
android·前端·flutter
_大学牲1 天前
Flutter 勇闯2D像素游戏之路(五):像元气骑士一样的设计随机地牢
flutter·游戏·游戏开发
音浪豆豆_Rachel1 天前
Flutter鸿蒙化之深入解析Pigeon非空字段设计:non_null_fields.dart全解
flutter·harmonyos