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

Flutter中如何实现和弦与歌词成对行自动换行并保持对齐?

Flutter中如何实现和弦与歌词成对行自动换行并保持对齐?

这个需求我太熟了!之前帮朋友做吉他谱App的时候就遇到过一模一样的问题——用固定字符数拆分确实省事,但碰到和弦长度和歌词不匹配的情况,分分钟就错位,巨头疼。

要解决这个问题,核心是放弃按固定字符数拆分,转而基于「和弦-对应歌词」的绑定单元来计算换行。毕竟像Ultimate Guitar那样的效果,每个和弦都是和特定的歌词绑定的,换行时必须把绑定的一对内容一起移动,不能拆开。

下面是具体的实现步骤,都是我实际用过的可行方案:

1. 先把和弦与歌词转换成结构化的绑定单元

首先得把原始的和弦行、歌词行,转换成「每个和弦对应一段歌词」的结构化数据。比如你输入的是:

和弦行:C G Am
歌词行:I'm singing in the rain just singing in the rain

我们需要把它拆成一个个ChordLyricPair单元,比如(C, "I'm ")、(G, "singing ")、(Am, "in the rain ")这样的结构。

先定义一个数据类来存这些单元:

class ChordLyricPair {
  final String chord;
  final String lyric;

  ChordLyricPair(this.chord, this.lyric);
}

然后写一个解析函数,把原始的行转换成这些单元。这里要注意,解析逻辑得根据你实际的输入格式调整——如果你的和弦是用方括号标记的(比如[C]I'm [G]singing),就改解析逻辑识别方括号;如果是靠空格对齐的,就按空格拆分匹配:

List<ChordLyricPair> parseChordLyricLines(String chordLine, String lyricLine) {
  // 按空格拆分歌词和和弦,对应位置配对
  final lyricWords = lyricLine.split(RegExp(r'\s+')).map((word) => '$word ').toList();
  final chordParts = chordLine.split(RegExp(r'\s+'));
  final pairs = <ChordLyricPair>[];

  for (int i = 0; i < lyricWords.length; i++) {
    // 没有对应和弦的位置填空字符串
    final chord = i < chordParts.length ? chordParts[i] : '';
    pairs.add(ChordLyricPair(chord, lyricWords[i]));
  }

  return pairs;
}

2. 根据屏幕宽度,把单元分成合适的行

接下来用LayoutBuilder获取当前可用的屏幕宽度,再用TextPainter测量每个单元的宽度,逐个往当前行里加,直到加下一个单元就超宽了,就换行。

这个方法的好处是完全适配屏幕宽度,不管是手机还是平板,都能自动调整:

List<List<ChordLyricPair>> splitIntoLines(
  List<ChordLyricPair> pairs,
  double maxWidth,
  TextStyle lyricStyle,
) {
  final lines = <List<ChordLyricPair>>[];
  var currentLine = <List<ChordLyricPair>>[];
  var currentLineWidth = 0.0;

  for (final pair in pairs) {
    // 用TextPainter测量这段歌词的宽度(因为和弦要放在歌词上方,宽度以歌词为准)
    final textPainter = TextPainter(
      text: TextSpan(text: pair.lyric, style: lyricStyle),
      textDirection: TextDirection.ltr,
    )..layout();
    final pairWidth = textPainter.width;

    // 如果加当前单元会超宽,就把当前行存入,新建一行
    if (currentLineWidth + pairWidth > maxWidth && currentLine.isNotEmpty) {
      lines.add(currentLine);
      currentLine = [];
      currentLineWidth = 0.0;
    }

    currentLine.add(pair);
    currentLineWidth += pairWidth;
  }

  // 把最后一行加进去
  if (currentLine.isNotEmpty) lines.add(currentLine);

  return lines;
}

3. 把分好的行渲染成和弦+歌词的组合

最后就是把每一行的单元转换成对应的和弦文本和歌词文本,因为用的是等宽字体,直接用padRight按歌词的字符数填充和弦,就能完美对齐:

LayoutBuilder(
  builder: (context, constraints) {
    // 定义等宽字体的样式
    final chordStyle = TextStyle(
      fontFamily: 'Courier',
      fontSize: 16,
      color: Colors.redAccent,
    );
    final lyricStyle = TextStyle(
      fontFamily: 'Courier',
      fontSize: 14,
      color: Colors.black87,
    );

    // 解析成绑定单元
    final chordLine = 'C        G    Am';
    final lyricLine = "I'm singing in the rain just singing in the rain";
    final pairs = parseChordLyricLines(chordLine, lyricLine);

    // 按屏幕宽度拆分行
    final lines = splitIntoLines(pairs, constraints.maxWidth, lyricStyle);

    // 渲染每一行的和弦和歌词
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: lines.map((line) {
        // 把行内的和弦按歌词长度填充,保证对齐
        final chordText = line.map((pair) => pair.chord.padRight(pair.lyric.length)).join();
        final lyricText = line.map((pair) => pair.lyric).join();

        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(chordText, style: chordStyle),
            Text(lyricText, style: lyricStyle),
            SizedBox(height: 12),
          ],
        );
      }).toList(),
    );
  },
)

几个关键的注意点

  • 解析逻辑要适配你的实际格式:如果你的和弦不是靠空格对齐的,比如用[C]这种标记,一定要修改parseChordLyricLines函数,准确提取每个和弦对应的歌词段,这是对齐的基础。
  • 必须用等宽字体:非等宽字体的话,字符宽度不一样,用padRight按字符数填充就会错位,所以一定要指定等宽字体(比如Courier、Consolas这类)。
  • 用TextPainter测量宽度更准确:不要自己算字符数×单个字符宽度,因为不同字体的字距可能有差异,TextPainter是Flutter官方推荐的测量文本宽度的方法,结果最准确。

备注:内容来源于stack exchange,提问作者Phoenyx

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.13 17:59:39