如何在Flutter音频播放器应用中实现音乐进度条
实现Flutter音频播放器的进度条(适配just_audio)
结合你正在使用的just_audio包,我们可以利用它提供的进度流来实现类似示例的音乐进度条,核心是监听音频的播放位置、缓冲位置和总时长,再通过Flutter组件构建UI。
核心思路
- 利用
just_audio的positionStream(当前播放位置)、bufferedPositionStream(缓冲进度)和durationStream(音频总时长)三个流,实时获取进度数据。 - 通过
StreamBuilder监听这些流的变化,动态更新UI。 - 基础版用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
相关产品推荐
相关产品推荐

