likes
comments
collection
share

2023-05-01 原生JS实现歌词滚动

作者站长头像
站长
· 阅读数 13

用原生JS去实现歌词滚动这个效果,从下面三个方面去开展思考:css布局、数据处理、界面事件

CSS布局

首先要把JS不能实现的地方用CSS来开展,先把CSS布局的功能做好,剩下的交给JS

页面如下:

2023-05-01 原生JS实现歌词滚动

很简单的一个页面,有h5音频元素audio和歌词滚动面板div,两个元素都是水平居中

HTML文件

<audio controls src="./assets/林俊杰-江南.mp3"></audio>
<div class="container">
  <ul class="lrc-list"></ul>
</div>

CSS文件

* {
  margin: 0;
  padding: 0;
}

body {
  background-color: #000;
  color: #666;
  text-align: center;
}

audio {
  width: 450px;
  margin: 30px 0;
}

.container {
  height: 420px;
  overflow: hidden;
}

.container ul {
  transition: 0.2s;
}

.container li {
  height: 30px;
  line-height: 30px;
  transition: 0.6s;
}

.container li.active {
  color: #fff;
  transform: scale(1.2); /* 放大,不影响集合属性,没有reflow */
}

1、音频元素audio原本是不显示的,加了属性controls可以显示在页面上

2、text-align属性是针对行盒文本元素的,只不过li ul等都继承了父元素属性

3、transition是动画效果,只针对元素的几何属性如宽高、偏移等

4、用transform变形是因为其不改变layout,是在合成线程中进行的,不涉及到渲染主线程,效率高

页面CSS布局弄好之后,接下来就是数据部分了,也是最关键的部分

数据处理

歌词原数据其实就是一个字符串,如下:

const lrc = `[00:00.01]歌曲名 江南(经典风行版)
[00:01.02]歌手名 林俊杰
[00:05.94]作词:李瑞洵
[00:10.04]作曲:林俊杰
[00:15.74]编曲:蔡政勋/陈建玮
[00:20.74]
[00:36.26]风到这里就是黏
[00:39.71]黏住过客的思念
[00:44.00]雨到了这里缠成线
[00:47.69]缠着我们留恋人世间
[00:52.18]你在身边就是缘
[00:55.62]缘分写在三生石上面
[01:00.26]爱有万分之一甜
[01:03.65]宁愿我就葬在这一点
[01:07.94]圈圈圆圆圈圈
[01:09.84]天天年年天天的我
[01:12.33]深深看你的脸
[01:15.03]生气的温柔
[01:16.92]埋怨的温柔的脸
[01:23.41]不懂爱恨情愁煎熬的我们
[01:26.75]都以为相爱就像风云的善变
[01:30.94]相信爱一天抵过永远
[01:35.18]在这一刹那冻结了时间
[01:38.97]不懂怎么表现温柔的我们
[01:42.81]还以为殉情只是古老的传言
[01:46.70]离愁能有多痛痛有多浓
[01:51.29]当梦被埋在江南烟雨中
[01:54.58]心碎了才懂
[02:20.14]圈圈圆圆圈圈
[02:21.78]天天年年天天的我
[02:24.33]深深看你的脸
[02:27.07]生气的温柔
[02:28.97]埋怨的温柔的脸
[02:34.95]不懂爱恨情愁煎熬的我们
[02:38.79]都以为相爱就像风云的善变
[02:42.88]相信爱一天抵过永远
[02:47.37]在这一刹那冻结了时间
[02:50.97]不懂怎么表现温柔的我们
[02:54.81]还以为殉情只是古老的传言
[02:58.80]离愁能有多痛痛有多浓
[03:03.24]当梦被埋在江南烟雨中
[03:06.58]心碎了才懂
[03:19.05]相信爱一天抵过永远
[03:23.24]在这一刹那冻结了时间
[03:26.93]不懂怎么表现温柔的我们
[03:30.78]还以为殉情只是古老的传言
[03:34.72]离愁能有多痛痛有多浓
[03:39.26]当梦被埋在江南烟雨中
[03:44.64]心碎了才懂`;

首先这样的数据肯定是不能直接用的,需要做一些处理,比如将其转为数组对象,每个对象里有两个变量,time表示时间与播放时间呼应,words表示歌词用来展示页面中的ul li元素的内容

function parseLrc() {
  const lines = lrc.split("\n");
  const result = []; // 歌词对象数组

  for (let i = 0; i < lines.length; i++) {
    const str = lines[i];
    const parts = str.split("]");
    const timeStr = parts[0].substring(1);
    const obj = {
      time: parseTime(timeStr),
      words: parts[1]
    };
    result.push(obj);
  }

  return result;
}

其中parseTime是对时间处理的函数,单独抽取出来

function parseTime(timeStr) {
  const parts = timeStr.split(":");
  return +parts[0] * 60 + +parts[1]; // +一元运算符转数字
}

可以对每一个写的函数进行单独的测试,在控制台里输入

2023-05-01 原生JS实现歌词滚动

验证完一个函数没问题后就不用去管了,写好注释

接下来应该计算出播放器播放到第几秒时,歌词数组对象中应该显示的歌词下标

// 获取需要的dom
const doms = {
  audio: document.querySelector("audio"),
  ul: document.querySelector(".lrc-list"),
  container: document.querySelector(".container")
};

用对象去获取需要的dom集合,减少全局变量

function findIndex() {
  const curTime = doms.audio.currentTime;
  for (let i = 0; i < lrcData.length; i++) {
    if (curTime < lrcData[i].time) {
      return lrcData[i - 1].words ? i - 1 : i - 2;
    }
  }
  // 找遍了没有找到(说明播放到歌词的最后一句)
  return lrcData.length - 1;
}

函数findIndex的作用就是查找歌词高亮显示的下标,根据的是获取到h5元素audio的当前时间curTime,然后遍历歌词数组,找到数组对象中time时间第一个大于curTime的下标,要注意对象的words为空字符串情况,找到则返回前一个下标,没找到表示歌词播放结束,返回最后一个下标

然后控制台测试函数findIndex

2023-05-01 原生JS实现歌词滚动

界面&事件

先看html文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>歌词滚动</title>
    <link rel="shortcut icon" href="./assets/favicon.ico" type="image/x-icon" />
    <link rel="stylesheet" href="./css/index.css" />
  </head>
  <body>
    <audio controls src="./assets/林俊杰-江南.mp3"></audio>
    <div class="container">
      <ul class="lrc-list"></ul>
    </div>

    <script src="./js/data.js"></script>
    <script src="./js/index.js"></script>
  </body>
</html>

歌词数组对象是要放到ul元素li里的textContent中,这部分需要JS去实现

function createElements() {
  const frag = document.createDocumentFragment(); // 文档片段
  for (let i = 0; i < lrcData.length; i++) {
    const li = document.createElement("li");
    li.textContent = lrcData[i].words;
    frag.appendChild(li);
  }
  doms.ul.appendChild(frag);
}

这里使用文档片段fragment,一次性添加完li后再添加到ul里,这样减少回流次数,因为文档片段不占据空间

然后是设置ul元素的偏移量,这里是根据audio元素的播放事件触发的,封装成函数

// 容器高度
const containerHight = doms.container.clientHeight;
// li的高度
const liHight = doms.ul.children[0].clientHeight;
// 偏移最大值
const maxOffset = doms.ul.clientHeight - containerHight;

/**
 * 设置ul元素的便宜量
 */
function setOffset() {
  const index = findIndex();
  let offset = liHight * index + liHight / 2 - containerHight / 2;
  if (offset < 0) {
    offset = 0;
  }
  if (offset > maxOffset) {
    offset = maxOffset;
  }
  // 设置偏移量给ul
  doms.ul.style.transform = `translateY(-${offset}px)`;
  // 去掉之前的样式
  let li = doms.ul.querySelector(".active");
  if (li) {
    li.classList.remove("active");
  }
  // 高亮
  li = doms.ul.children[index];
  if (li) {
    li.classList.add("active");
  }
}

偏移量有三种情况,小于0、大于0&小于最大偏移量、最大偏移量,如下图

2023-05-01 原生JS实现歌词滚动

偏移使用transform变形函数

新增&移除样式使用dom.classList.add&remove方法

最后,audio元素的事件监听

doms.audio.addEventListener("timeupdate", setOffset);

至此,JS实现歌词滚动完成,全部代码链接:gitee.com/xumaozeng/m…