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

如何在Flutter音频播放器应用中实现音乐进度条

实现Flutter音频播放器的进度条(适配just_audio)

结合你正在使用的just_audio包,我们可以利用它提供的进度流来实现类似示例的音乐进度条,核心是监听音频的播放位置、缓冲位置和总时长,再通过Flutter组件构建UI。

核心思路

  1. 利用just_audio的positionStream(当前播放位置)、bufferedPositionStream(缓冲进度)和durationStream(音频总时长)三个流,实时获取进度数据。
  2. 通过StreamBuilder监听这些流的变化,动态更新UI。
  3. 基础版用Flutter自带的Slider组件快速实现,进阶版可以自定义带缓冲进度的样式(匹配你示例图的效果)。

1. 基础版:使用原生Slider实现

先实现一个简单可用的进度条,包含进度滑块和时长显示:

代码实现

// 先定义你的AudioPlayer实例
final AudioPlayer _player = AudioPlayer();

// 格式化时长为 mm:ss 或 hh:mm:ss
String _formatDuration(Duration duration) {
  String twoDigits(int n) => n.toString().padLeft(2, '0');
  final hours = twoDigits(duration.inHours);
  final minutes = twoDigits(duration.inMinutes.remainder(60));
  final seconds = twoDigits(duration.inSeconds.remainder(60));
  return duration.inHours > 0 ? '$hours:$minutes:$seconds' : '$minutes:$seconds';
}

// 进度条UI组件
Widget buildProgressBar() {
  return StreamBuilder<Duration?>(
    stream: _player.durationStream,
    builder: (context, durationSnapshot) {
      final Duration? totalDuration = durationSnapshot.data;
      
      return StreamBuilder<Duration>(
        stream: _player.positionStream,
        builder: (context, positionSnapshot) {
          final Duration currentPosition = positionSnapshot.data ?? Duration.zero;
          double progress = 0.0;
          
          // 避免除以0的情况
          if (totalDuration != null && totalDuration.inMilliseconds > 0) {
            progress = currentPosition.inMilliseconds / totalDuration.inMilliseconds;
          }
          
          return Column(
            children: [
              // 进度滑块
              Slider(
                value: progress,
                onChanged: (value) {
                  if (totalDuration != null) {
                    final Duration newPosition = Duration(
                      milliseconds: (value * totalDuration.inMilliseconds).round(),
                    );
                    _player.seek(newPosition); // 跳转播放位置
                  }
                },
                min: 0.0,
                max: 1.0,
                activeColor: Colors.blue,
                inactiveColor: Colors.grey[300],
              ),
              // 时长显示
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text(_formatDuration(currentPosition)),
                    Text(_formatDuration(totalDuration ?? Duration.zero)),
                  ],
                ),
              ),
            ],
          );
        },
      );
    },
  );
}

2. 进阶版:自定义带缓冲进度的进度条

如果要实现类似示例图的分层进度(总背景、缓冲进度、已播放进度),可以用Stack组件自定义:

步骤1:添加依赖

需要用rxdart合并多个流,在pubspec.yaml中添加:

dependencies:
  rxdart: ^0.27.7

步骤2:定义数据类

用于统一存储进度相关数据:

class PositionData {
  final Duration position;
  final Duration bufferedPosition;
  final Duration duration;

  PositionData(this.position, this.bufferedPosition, this.duration);
}

步骤3:实现自定义进度条

Widget buildCustomProgressBar() {
  return StreamBuilder<Duration?>(
    stream: _player.durationStream,
    builder: (context, durationSnapshot) {
      final Duration? totalDuration = durationSnapshot.data;
      
      return StreamBuilder<PositionData>(
        // 合并三个流的数据
        stream: Rx.combineLatest3<Duration, Duration, Duration?, PositionData>(
          _player.positionStream,
          _player.bufferedPositionStream,
          _player.durationStream,
          (position, bufferedPosition, duration) => PositionData(
            position,
            bufferedPosition,
            duration ?? Duration.zero,
          ),
        ),
        builder: (context, snapshot) {
          final positionData = snapshot.data;
          final Duration currentPosition = positionData?.position ?? Duration.zero;
          final Duration bufferedPosition = positionData?.bufferedPosition ?? Duration.zero;
          final Duration duration = positionData?.duration ?? Duration.zero;

          return Column(
            children: [
              // 自定义进度条(支持点击跳转)
              GestureDetector(
                onTapDown: (details) {
                  if (duration.inMilliseconds == 0) return;
                  final RenderBox box = context.findRenderObject() as RenderBox;
                  final double tapX = details.localPosition.dx;
                  final double progress = tapX / box.size.width;
                  _player.seek(Duration(
                    milliseconds: (progress * duration.inMilliseconds).round(),
                  ));
                },
                child: Container(
                  height: 6,
                  margin: const EdgeInsets.symmetric(horizontal: 16),
                  child: Stack(
                    children: [
                      // 总进度背景
                      Container(
                        width: double.infinity,
                        height: 6,
                        decoration: BoxDecoration(
                          color: Colors.grey[200],
                          borderRadius: BorderRadius.circular(3),
                        ),
                      ),
                      // 缓冲进度
                      Container(
                        width: duration.inMilliseconds == 0
                            ? 0
                            : (bufferedPosition.inMilliseconds / duration.inMilliseconds) * double.infinity,
                        height: 6,
                        decoration: BoxDecoration(
                          color: Colors.grey[400],
                          borderRadius: BorderRadius.circular(3),
                        ),
                      ),
                      // 已播放进度
                      Container(
                        width: duration.inMilliseconds == 0
                            ? 0
                            : (currentPosition.inMilliseconds / duration.inMilliseconds) * double.infinity,
                        height: 6,
                        decoration: BoxDecoration(
                          color: Colors.blue,
                          borderRadius: BorderRadius.circular(3),
                        ),
                      ),
                      // 进度滑块圆点
                      if (duration.inMilliseconds > 0)
                        Positioned(
                          left: (currentPosition.inMilliseconds / duration.inMilliseconds) * MediaQuery.of(context).size.width - 28,
                          top: -8,
                          child: Container(
                            width: 20,
                            height: 20,
                            decoration: const BoxDecoration(
                              color: Colors.blue,
                              shape: BoxShape.circle,
                              boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4)],
                            ),
                          ),
                        ),
                    ],
                  ),
                ),
              ),
              // 时长显示
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text(_formatDuration(currentPosition)),
                    Text(_formatDuration(duration)),
                  ],
                ),
              ),
            ],
          );
        },
      );
    },
  );
}

关键注意点

  • 始终处理duration为null的情况:音频未加载完成时,总时长为空,需要避免计算错误。
  • 用StreamBuilder确保UI跟随音频进度实时更新,无需手动 setState。
  • 自定义进度条的点击跳转逻辑,需要通过RenderBox获取点击位置的相对比例,再转换为播放位置。

内容的提问来源于stack exchange,提问作者Mohamed Gamal Mohamed

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 01:45:32