Flutter如何实现卡拉OK式逐词依次变色的文字动画效果
结论
animated_text_kit等现有Flutter文字动画库没有内置卡拉OK式逐词高亮的现成能力。你当前使用的ColorizeAnimatedText逻辑是对整段文本做像素级扫色渐变,不是按单词维度切换高亮状态,完全不匹配需求。
这个效果不需要找额外插件,既可以直接用Flutter原生组件无依赖实现,也可以通过给animated_text_kit编写自定义AnimatedText子类实现,两种方案都能完美支持按单词长度动态分配高亮时长。
核心实现逻辑
- 先将完整文本按空格拆分为独立单词列表,统计每个单词的字符长度
- 按单词长度占全文总字符数的比例,给每个单词分配对应的高亮持续时长,可额外设置最小时长阈值,避免过短的单词一闪而过
- 动画推进过程中实时记录当前已完成高亮的单词索引:索引位置之前的单词统一使用高亮色,当前及之后的单词使用默认文本色,用
RichText拼接TextSpan渲染即可 - 单词切换时不需要做复杂过渡动画,直接切换颜色就能还原标准卡拉OK字幕效果,如果需要丝滑过渡,也可以给单个单词加100ms左右的颜色补间
可直接运行的无依赖实现(推荐)
不需要引入第三方依赖,逻辑更可控:
class KaraokeText extends StatefulWidget { final String text; final TextStyle defaultStyle; final TextStyle highlightStyle; final Duration totalDuration; final Duration minWordDuration; const KaraokeText({ super.key, required this.text, required this.defaultStyle, required this.highlightStyle, this.totalDuration = const Duration(seconds: 5), this.minWordDuration = const Duration(milliseconds: 300), }); @override State<KaraokeText> createState() => _KaraokeTextState(); } class _KaraokeTextState extends State<KaraokeText> { late List<String> _words; late List<Duration> _wordDurations; int _currentHighlightIndex = -1; @override void initState() { super.initState(); // 拆分单词 _words = widget.text.split(' '); // 按单词长度动态计算每个单词的高亮时长 int totalCharLen = _words.fold(0, (prev, word) => prev + word.length); _wordDurations = _words.map((word) { final ratio = word.length / totalCharLen; final duration = Duration( milliseconds: (widget.totalDuration.inMilliseconds * ratio).round() ); // 短单词不小于最小时长,避免一闪而过 return duration > widget.minWordDuration ? duration : widget.minWordDuration; }).toList(); _startAnimation(); } Future<void> _startAnimation() async { for (int i = 0; i < _words.length; i++) { if (!mounted) return; setState(() => _currentHighlightIndex = i); await Future.delayed(_wordDurations[i]); } // 需要循环播放可在此处重置索引后重新调用_startAnimation if (mounted) setState(() => _currentHighlightIndex = _words.length - 1); } @override Widget build(BuildContext context) { return RichText( text: TextSpan( children: List.generate(_words.length, (index) { final isHighlighted = index <= _currentHighlightIndex; return TextSpan( text: "${_words[index]} ", style: isHighlighted ? widget.highlightStyle : widget.defaultStyle, ); }) ), ); } }
调用示例
替换你原有textAnimated方法即可,注意修复你原代码中TextStyle未闭合的语法错误:
Widget textAnimated() { const baseStyle = TextStyle( fontSize: 18, height: 3.0, color: Colors.grey, // 未高亮状态文字颜色 ); return Padding( padding: const EdgeInsets.only(left: 10, right: 10, top: 140), child: KaraokeText( "The quick brown fox jumps over the lazy dog.", defaultStyle: baseStyle, highlightStyle: baseStyle.copyWith(color: Colors.black), // 高亮状态文字颜色 totalDuration: const Duration(seconds: 4), // 整段文本总播放时长 ), ); }
基于animated_text_kit的自定义实现
如果你需要复用animated_text_kit的播放回调、多段文本轮播等能力,可以继承AnimatedText抽象类实现,核心逻辑和上述无依赖方案完全一致:拆分单词、按长度分配时长、在动画推进到对应时间点时更新当前高亮的单词索引,最后在build方法中返回拼接好的RichText即可,不需要修改插件源码。
内容的提问来源于stack exchange,提问作者quickBrownFox
相关产品推荐
相关产品推荐

