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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 23:06:08