前言

歌词显示是音乐播放器中非常受欢迎的功能,用户可以边听歌边看歌词,跟着一起唱。本篇我们来实现一个完整的歌词显示页面,包含歌词滚动列表、当前行高亮、点击跳转、以及底部播放控制。整个页面采用渐变背景设计,营造沉浸式的听歌体验。

功能分析

歌词页面需要实现以下功能:顶部显示歌曲名和歌手名,中间是可滚动的歌词列表,当前播放的歌词行高亮显示,用户可以点击任意歌词行跳转播放,底部提供进度条和播放控制按钮。

页面框架搭建

歌词页面需要管理当前播放行和滚动控制器,使用 StatefulWidget 实现:

class LyricsPage extends StatefulWidget {
  const LyricsPage({super.key});

  
  State<LyricsPage> createState() => _LyricsPageState();
}

class _LyricsPageState extends State<LyricsPage> {
  int currentLine = 5;
  final ScrollController _scrollController = ScrollController();

currentLine 记录当前播放到第几行歌词,默认值为5表示从第6行开始(索引从0开始)。_scrollController 用于控制歌词列表的滚动位置,实现歌词自动滚动到当前行。

歌词数据结构

歌词数据使用 List<Map> 存储,每条歌词包含时间戳和文本内容:

  final List<Map<String, dynamic>> lyrics = [
    {'time': 0, 'text': '作词: 音乐人'},
    {'time': 1, 'text': '作曲: 音乐人'},
    {'time': 5, 'text': ''},
    {'time': 10, 'text': '夜空中最亮的星'},
    {'time': 15, 'text': '能否听清'},
    {'time': 20, 'text': '那仰望的人'},
    {'time': 25, 'text': '心底的孤独和叹息'},
    {'time': 30, 'text': ''},
    {'time': 35, 'text': '夜空中最亮的星'},
    {'time': 40, 'text': '能否记起'},
    {'time': 45, 'text': '曾与我同行'},
    {'time': 50, 'text': '消失在风里的身影'},
    {'time': 55, 'text': ''},
    {'time': 60, 'text': '我祈祷拥有一颗透明的心灵'},
    {'time': 65, 'text': '和会流泪的眼睛'},
    {'time': 70, 'text': '给我再去相信的勇气'},
    {'time': 75, 'text': '越过谎言去拥抱你'},
  ];

time 字段表示这行歌词对应的播放时间(秒),text 字段是歌词内容。空字符串表示歌词间的空行,用于视觉上的分隔。实际项目中这些数据通常从LRC文件解析而来。

页面主体结构

页面使用 Stack 叠加渐变背景和内容区域:

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        children: [
          Container(
            decoration: BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topCenter, 
                end: Alignment.bottomCenter, 
                colors: [
                  const Color(0xFFE91E63).withOpacity(0.3), 
                  Colors.black
                ]
              )
            )
          ),
          SafeArea(
            child: Column(
              children: [
                _buildHeader(),
                Expanded(child: _buildLyrics()),
                _buildControls(),
              ],
            ),
          ),
        ],
      ),
    );
  }

Stack 的第一层是渐变背景,从顶部的粉色渐变到底部的黑色。第二层是实际内容,使用 Column 布局分为三部分:顶部标题栏、中间歌词列表、底部播放控制。SafeArea 确保内容不会被状态栏遮挡。

顶部标题栏

标题栏显示歌曲名、歌手名,以及返回和分享按钮:

  Widget _buildHeader() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [
          IconButton(
            icon: const Icon(Icons.keyboard_arrow_down, size: 30), 
            onPressed: () => Get.back()
          ),
          const Expanded(
            child: Column(
              children: [
                Text('夜空中最亮的星', 
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                SizedBox(height: 4),
                Text('逃跑计划', style: TextStyle(color: Colors.grey)),
              ],
            ),
          ),
          IconButton(icon: const Icon(Icons.share), onPressed: () {}),
        ],
      ),
    );
  }

左侧是向下箭头按钮,点击返回上一页。中间使用 Expanded 占据剩余空间,显示歌曲名和歌手名,歌曲名使用粗体突出显示。右侧是分享按钮。这种布局是音乐App中常见的设计模式。

歌词列表实现

歌词列表是页面的核心部分,使用 ListView.builder 构建:

  Widget _buildLyrics() {
    return ListView.builder(
      controller: _scrollController,
      padding: const EdgeInsets.symmetric(vertical: 100),
      itemCount: lyrics.length,
      itemBuilder: (context, index) {
        final isCurrentLine = index == currentLine;
        return GestureDetector(
          onTap: () => setState(() => currentLine = index),
          child: Container(
            padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 32),
            child: Text(
              lyrics[index]['text'],
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: isCurrentLine ? 20 : 16,
                fontWeight: isCurrentLine ? FontWeight.bold : FontWeight.normal,
                color: isCurrentLine 
                  ? const Color(0xFFE91E63) 
                  : Colors.white.withOpacity(0.5),
              ),
            ),
          ),
        );
      },
    );
  }

padding: const EdgeInsets.symmetric(vertical: 100) 在列表顶部和底部添加100像素的空白,让歌词可以滚动到屏幕中央位置。

每行歌词用 GestureDetector 包裹,点击后更新 currentLine 实现跳转功能。当前行使用更大的字号(20px vs 16px)、粗体和粉色高亮显示,其他行使用半透明白色,形成明显的视觉对比。

底部播放控制

底部控制区包含进度条和播放按钮:

  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(20),
      child: Column(
        children: [
          Row(
            children: [
              const Text('2:35', 
                style: TextStyle(color: Colors.grey, fontSize: 12)),
              Expanded(
                child: Slider(
                  value: 0.4, 
                  activeColor: const Color(0xFFE91E63), 
                  inactiveColor: Colors.grey.withOpacity(0.3), 
                  onChanged: (v) {}
                )
              ),
              const Text('4:28', 
                style: TextStyle(color: Colors.grey, fontSize: 12)),
            ],
          ),

进度条使用 Slider 组件,左右两侧分别显示当前播放时间和总时长。activeColor 设置已播放部分的颜色为粉色,inactiveColor 设置未播放部分为灰色。

          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              IconButton(icon: const Icon(Icons.shuffle), onPressed: () {}),
              IconButton(
                icon: const Icon(Icons.skip_previous, size: 36), 
                onPressed: () {}
              ),
              Container(
                width: 64, height: 64,
                decoration: const BoxDecoration(
                  color: Color(0xFFE91E63), 
                  shape: BoxShape.circle
                ),
                child: IconButton(
                  icon: const Icon(Icons.pause, size: 32, color: Colors.white), 
                  onPressed: () {}
                ),
              ),
              IconButton(
                icon: const Icon(Icons.skip_next, size: 36), 
                onPressed: () {}
              ),
              IconButton(icon: const Icon(Icons.repeat), onPressed: () {}),
            ],
          ),
        ],
      ),
    );
  }

播放控制按钮使用 Row 水平排列,spaceEvenly 让按钮均匀分布。中间的播放/暂停按钮使用粉色圆形背景突出显示,是整个控制区的视觉焦点。上一首和下一首按钮使用较大的图标(36px),随机播放和循环播放按钮使用默认大小。

页面入口

歌词页面的入口在播放器页面,用户点击"点击查看歌词"文字即可进入:

GestureDetector(
  onTap: () => Get.to(() => const LyricsPage()), 
  child: const Text('点击查看歌词', style: TextStyle(color: Colors.white54))
),

也可以在播放器页面的进度条区域添加这个入口,让用户更容易发现歌词功能。

扩展思路

实际项目中,歌词同步需要与播放器的播放进度联动。可以监听播放器的进度变化,根据当前播放时间找到对应的歌词行,然后更新 currentLine 并调用 _scrollController.animateTo() 平滑滚动到该行。

点击歌词跳转功能需要调用播放器的 seek 方法,将播放位置跳转到对应歌词的时间点。

还可以添加歌词翻译功能,在原歌词下方显示翻译文本,或者添加歌词字体大小调节、歌词背景模糊等个性化设置。

小结

本篇我们实现了一个功能完整的歌词显示页面,包含渐变背景、歌词列表、当前行高亮、点击跳转和播放控制。通过 ListView.builder 构建歌词列表,使用条件样式实现当前行高亮效果。渐变背景和统一的粉色主题色让页面视觉效果更加协调。在实际项目中,还需要与播放器进度联动,实现真正的歌词同步滚动功能。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐