基于just_audio与Stream实现Flutter音频字幕高亮及自动滚动
自行开发音频字幕同步功能方案(基于just_audio: ^0.9.34)
1. 定义字幕数据结构
先统一字幕数据模型,不管是解析LRC文件还是自定义格式,都能适配:
class LyricLine { final Duration startTime; // 该行字幕开始显示时间 final Duration endTime; // 该行字幕结束显示时间 final String text; // 字幕文本 LyricLine({ required this.startTime, required this.endTime, required this.text, }); }
如果用LRC格式,自己写个解析函数即可——把每行[00:12.34]歌词内容拆成时间戳和文本,转成LyricLine对象存入列表。
2. 监听音频进度匹配当前字幕
利用just_audio的player.position流监听实时播放进度,实时匹配对应字幕:
- 用
ValueNotifier托管当前高亮字幕的索引,避免频繁调用setState刷新UI - 进度更新时,找到
startTime ≤ 当前进度 ≤ endTime的字幕行,更新索引;字幕数量多的话,改用二分查找提升性能
示例代码:
final player = AudioPlayer(); final ValueNotifier<int> currentLyricIndex = ValueNotifier(-1); final List<LyricLine> lyricLines = []; // 提前解析好的字幕列表 // 监听播放进度,匹配对应字幕 player.position.listen((position) { // 这里用遍历示例,字幕多的话替换成二分查找 for (int i = 0; i < lyricLines.length; i++) { final line = lyricLines[i]; if (position >= line.startTime && position <= line.endTime) { if (currentLyricIndex.value != i) { currentLyricIndex.value = i; } break; } } });
3. 渲染字幕列表并高亮当前行
用ListView.builder渲染所有字幕,通过ValueListenableBuilder监听索引变化,高亮当前行:
- 给当前行设置差异化样式(字体大小、颜色、粗细)
- 加动画过渡,让高亮切换更自然
示例代码:
ValueListenableBuilder<int>( valueListenable: currentLyricIndex, builder: (context, activeIndex, child) { return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), itemCount: lyricLines.length, itemBuilder: (context, i) { final line = lyricLines[i]; final isCurrent = i == activeIndex; return AnimatedContainer( duration: const Duration(milliseconds: 300), padding: const EdgeInsets.symmetric(vertical: 8), child: Text( line.text, textAlign: TextAlign.center, style: TextStyle( fontSize: isCurrent ? 20 : 16, color: isCurrent ? Colors.blue : Colors.grey, fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal, ), ), ); }, ); }, )
4. 实现自动滚动到当前字幕
给ListView绑定ScrollController,当当前字幕索引变化时触发自动滚动:
- 计算当前行的滚动偏移量(可假设每行固定高度,或通过RenderObject获取实际高度)
- 用
animateTo实现平滑滚动,避免生硬跳转
示例代码:
final ScrollController scrollController = ScrollController(); // 监听索引变化触发滚动 currentLyricIndex.addListener(() { final activeIndex = currentLyricIndex.value; if (activeIndex == -1) return; // 假设每行高度40,计算偏移量;要精确高度可通过RenderBox获取 final offset = activeIndex * 40.0; scrollController.animateTo( offset, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, ); }); // 把controller绑定到ListView ListView.builder( controller: scrollController, // ...其他参数 )
5. 优化同步精度与体验
- 加100ms容错:判断
position >= line.startTime - const Duration(milliseconds: 100),避免音频进度抖动导致字幕频繁切换 - 处理拖动进度:音频seek后立即触发字幕匹配,无需等待position流更新
- 兼容异常LRC格式:没有结束时间的行,自动用下一行的开始时间作为当前行的结束时间
6. 自定义扩展(按需添加)
- 点击字幕跳转时间:给列表项加
GestureDetector,点击时调用player.seek(line.startTime) - 双语字幕展示:修改
LyricLine添加translation字段,列表项同时显示原文和翻译 - 可配置样式:把字体、颜色、行间距做成参数,方便组件复用
内容的提问来源于stack exchange,提问作者Jinto Joseph
相关产品推荐
相关产品推荐

