【開門見山】 實現目標: 需要讓歌詞列表隨著播放的時間更新而滾動,即實時的跟隨歌曲的進度而滾動 效果: 編輯 需要事先準備的東西: 1.音頻(mp3格式): 編輯 2.歌詞(詳細): 編輯 先展示html和css的實現(不重要,自己想怎樣調都行,重點在js的邏輯實現) 1.html: 小tip ...
【開門見山】
實現目標:
需要讓歌詞列表隨著播放的時間更新而滾動,即實時的跟隨歌曲的進度而滾動
效果:
需要事先準備的東西:
1.音頻(mp3格式):
2.歌詞(詳細):
先展示html和css的實現(不重要,自己想怎樣調都行,重點在js的邏輯實現)
1.html:
小tips:
這其中的歌詞列表ul里的li,可以用亂序假文(lorem)先去進行佈局或樣式的調整,後續再傳入歌詞。
如:li*30>lorem3 // 生成三十個li,且每個li中隨機生成三個詞語。
<!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>Document</title> <link rel="shortcut icon" href="./assets/favicon.ico" type="image/x-icon"> <link rel="stylesheet" href="./css/index.css"> </head> <body> <audio src="./assets/music.mp3" controls></audio> <div class="container"> <ul class="wordList"> </ul> </div> </body> <script src="./js/data.js"></script> <script src="./js/index.js"></script> </html>
其中data.js為歌詞的文件,index.js為主要邏輯文件,在下邊js的文件中會展示
2.css:
* { margin: 0; padding: 0; } ul { list-style: none; } body { background-color: black; } audio { display: block; width: 500px; margin: 30px auto; } .container { height: 420px; /* border: 1px solid white; */ overflow: hidden; } .container ul { transition: all 0.6s; text-align: center; } .container li { color: #666; height: 30px; line-height: 30px; } .container li.active{ transform: scale(1.3); // 歌詞放大效果 color: #fff; }
css實現的幾個細節:
1. 實際上整個歌詞列表ul是很長的,而歌詞的滾動需要依靠ul的上下偏移來實現,會在container 中溢出,所以給container 設置overflow:hidden。
2. 實現寫好要給當前播放的歌詞的樣式,封裝到active類下,為後續js的實現鋪好路。
3.當前播放的歌詞的樣式無非兩個,放大和變色,這裡為什麼放大不用font-size,其實有些講究:
font-size的改變就導致了元素幾何信息的變化,幾何信息的變化就意味著會導致reflow,會使頁面重新佈局,影響效率。
而transform 變形,並不是在渲染主線程中執行,不占用主線程,而是在合成線程中執行,最終的實現也是交給cpu,所以不會導致頁面重新佈局,不影響效率。
重點!JS的實現
1.data.js 歌詞文件:
var lrc = `[00:01.06]難念的經
[00:03.95]演唱:周華健
[00:06.78]
[00:30.96]笑你我枉花光心計
[00:34.15]愛競逐鏡花那美麗
[00:36.75]怕幸運會轉眼遠逝
[00:39.32]為貪嗔喜惡怒著迷
[00:41.99]責你我太貪功戀勢
[00:44.48]怪大地眾生太美麗
[00:47.00]悔舊日太執信約誓
[00:49.66]為悲歡哀怨妒著迷
[00:52.56]啊 捨不得璀燦俗世
[00:57.66]啊 躲不開痴戀的欣慰
[01:02.86]啊 找不到色相代替
[01:08.09]啊 參一生參不透這條難題
[01:13.15]吞風吻雨葬落日未曾彷徨
[01:15.73]欺山趕海踐雪徑也未絕望
[01:18.23]拈花把酒偏折煞世人情狂
[01:20.90]憑這兩眼與百臂或千手不能防
[01:23.76]天闊闊雪漫漫共誰同航
[01:26.09]這沙滾滾水皺皺笑著浪蕩
[01:28.68]貪歡一刻偏教那女兒情長埋葬
[01:32.38]
[01:34.09]吞風吻雨葬落日未曾彷徨
[01:36.50]欺山趕海踐雪徑也未絕望
[01:39.07]拈花把酒偏折煞世人情狂
[01:41.69]憑這兩眼與百臂或千手不能防
[01:44.68]天闊闊雪漫漫共誰同航
[01:46.93]這沙滾滾水皺皺笑著浪蕩
[01:49.54]貪歡一刻偏教那女兒情長埋葬
[01:53.41]
[02:15.45]笑你我枉花光心計
[02:18.53]愛競逐鏡花那美麗
[02:21.14]怕幸運會轉眼遠逝
[02:23.76]為貪嗔喜惡怒著迷
[02:26.43]責你我太貪功戀勢
[02:28.98]怪大地眾生太美麗
[02:31.60]悔舊日太執信約誓
[02:34.26]為悲歡哀怨妒著迷
[02:36.90]啊 捨不得璀燦俗世
[02:42.04]啊 躲不開痴戀的欣慰
[02:47.34]啊 找不到色相代替
[02:52.52]啊 參一生參不透這條難題
[02:57.47]吞風吻雨葬落日未曾彷徨
[03:00.05]欺山趕海踐雪徑也未絕望
[03:02.64]拈花把酒偏折煞世人情狂
[03:05.27]憑這兩眼與百臂或千手不能防
[03:08.22]天闊闊雪漫漫共誰同航
[03:10.49]這沙滾滾水皺皺笑著浪蕩
[03:13.06]貪歡一刻偏教那女兒情長埋葬
[03:18.45]吞風吻雨葬落日未曾彷徨
[03:20.90]欺山趕海踐雪徑也未絕望
[03:23.54]拈花把酒偏折煞世人情狂
[03:26.21]憑這兩眼與百臂或千手不能防
[03:29.07]天闊闊雪漫漫共誰同航
[03:31.32]這沙滾滾水皺皺笑著浪蕩
[03:33.92]貪歡一刻偏教那女兒情長埋葬
[03:39.32]吞風吻雨葬落日未曾彷徨
[03:41.84]欺山趕海踐雪徑也未絕望
[03:44.38]拈花把酒偏折煞世人情狂
[03:47.04]憑這兩眼與百臂或千手不能防
[03:49.99]天闊闊雪漫漫共誰同航
[03:52.20]這沙滾滾水皺皺笑著浪蕩
[03:54.89]貪歡一刻偏教那女兒情長埋葬
[04:00.28]吞風吻雨葬落日未曾彷徨
[04:02.68]欺山趕海踐雪徑也未絕望
[04:05.25]拈花把酒偏折煞世人情狂
[04:07.90]憑這兩眼與百臂或千手不能防
[04:10.85]天闊闊雪漫漫共誰同航
[04:13.08]這沙滾滾水皺皺笑著浪蕩
[04:15.75]貪歡一刻偏教那女兒情長埋葬
[04:19.48]`;
2.index.js 主文件:
// 最開始獲取到的歌詞列表是字元串類型(不好操作)
let lrcArr = lrc.split('\n');
// 接收修正後的歌詞數組
let result = [];
// 獲取所要用到的dom列表
doms = {
audio: document.querySelector("audio"),
ul: document.querySelector("ul"),
container: document.querySelector(".container")
}
// 將歌詞數組轉成由對象組成的數組,對象有time和word兩個屬性(為了方便操作)
for (let i = 0; i < lrcArr.length; i++) {
var lrcData = lrcArr[i].split(']');
var lrcTime = lrcData[0].substring(1);
var obj = {
time: parseTime(lrcTime),
word: lrcData[1]
}
result.push(obj);
}
// 將tiem轉換為秒的形式
function parseTime(lrcTime) {
lrcTimeArr = lrcTime.split(":")
return +lrcTimeArr[0] * 60 + +lrcTimeArr[1];
}
// 獲取當前播放到的歌詞的下標
function getIndex() {
let Time = doms.audio.currentTime;
for (let i = 0; i < result.length; i++) {
if (result[i].time > Time) {
return i - 1;
}
}
}
// 創建歌詞列表
function createElements() {
let frag = document.createDocumentFragment(); // 文檔片段
for (let i = 0; i < result.length; i++) {
let li = document.createElement("li");
li.innerText = result[i].word;
frag.appendChild(li);
}
doms.ul.appendChild(frag);
}
createElements();
// 獲取顯示視窗的可視高度
let containerHeight = doms.container.clientHeight;
// 獲取歌詞列表的可視高度
let liHeight = doms.ul.children[0].clientHeight;
// 設置最大最小偏移量,防止顯示效果不佳
let minOffset = 0;
let maxOffset = doms.ul.clientHeight - containerHeight;
// 控制歌詞滾動移動的函數
function setOffset() {
let index = getIndex();
// 計算滾動距離
let offset = liHeight * index - containerHeight / 2 + liHeight / 2;
if (offset < minOffset) {
offset = minOffset;
};
if (offset > maxOffset) {
offset = maxOffset;
};
// 滾動
doms.ul.style.transform = `translateY(-${offset}px)`;
// 清除之前的active
let li = doms.ul.querySelector(".active")
if (li) {
li.classList.remove("active");
}
// 為當前所唱到的歌詞添加active
li = doms.ul.children[index];
if (li) {
li.classList.add("active");
}
};
// 當audio的播放時間更新時,觸發該事件
doms.audio.addEventListener("timeupdate", setOffset);
思路與解析:
首先,在寫js主文件之前,我們需要構思一下這個功能大概要怎樣去實現。
我看到這個功能的想法是 既然是要讓歌詞隨著播放進度而滾動,肯定最終是要監聽到當前播放的時間,去匹配當前播放時間所要播放的對應的歌詞,然後讓匹配到的歌詞移動到可視視窗的中央,再給它高亮啊什麼的。
有了怎麼實現,再去細分其中的細節步驟。
細節步驟:
<1>(1). 歌詞文件中的歌詞都是字元串類型,不好操作,我們需要將其轉換成數組,同時因為文件中的歌詞其實包含著兩個信息,‘每個歌詞的內容’ 和 ‘其對應的播放時間’,所以最好把每個歌詞又轉換成對象的形式加入數組,即:
這種形式。
<1>(2). 歌詞文件中的歌詞是 [分:秒] 的格式,而最後要與audio的播放時間匹配的話,即audio.currentTime 這個時間是以秒為單位的,所以在<1>實現時,可以先封裝一個parseTime(轉換時間)的函數(這裡將轉換時間獨立封裝成函數,是為了代碼看起來更加簡潔清晰)。
<2>. 在把歌詞轉換為自己想要的格式後,有了每個歌詞的內容和其對應的時間,下一步要實現如何去匹配播放時間,然後獲取對應歌詞的下標(因為歌詞被轉換為數組),既然是獨立的功能,也可獨立封裝為getIndex函數。
<3>. 現在有了歌詞格式,有了獲取當前播放的對應歌詞下標的方法,我們可以將歌詞加入頁面了(不要忘記html中只寫了ul,為了能夠更靈活,所以html中並沒有直接寫死分配的歌詞),這個功能也可以封裝成一個createElements函數。
<4>. 之後就是封裝控制歌詞滾動的setOffset函數,當前播放的歌詞的下標,滾動就很容易實現了,因為每個歌詞li的高度都是一樣的,所以只需要用 每個li的下標*index + li高度的一半 - 可視視窗高度的一般即是ul需要移動的距離。
<5>. 最後就是給audio綁定一個監聽播放時間改變的事件就好了,回調函數直接用setOffset。
具體實現步驟:
<1>(1) 將歌詞轉換為所需的格式
// 最開始獲取到的歌詞列表是字元串類型(不好操作)
let lrcArr = lrc.split('\n');
// 接收修正後的歌詞數組
let result = [];
// 獲取所要用到的dom列表
doms = {
audio: document.querySelector("audio"),
ul: document.querySelector("ul"),
container: document.querySelector(".container")
}
// 獲取所要用到的dom列表
doms = {
audio: document.querySelector("audio"),
ul: document.querySelector("ul"),
container: document.querySelector(".container")
}
// 將歌詞數組轉成由對象組成的數組,對象有time和word兩個屬性(為了方便操作)
for (let i = 0; i < lrcArr.length; i++) {
var lrcData = lrcArr[i].split(']');
var lrcTime = lrcData[0].substring(1);
var obj = {
time: parseTime(lrcTime),
word: lrcData[1]
}
result.push(obj);
}
【這裡將這次所要用到的dom元素,都放在了doms對象中,這樣更清晰】
用到的知識點:
1. split() 方法用於把一個
成字元串數組。// 目的是為了把歌詞文件中,時間前後的[ ] 給去掉
2.string.substring(start, end) 截取字元串方法從 start 位置截取到 end 位置,end 可選
<1>(2). 轉換時間parseTime函數
// 將tiem轉換為秒的形式
function parseTime(lrcTime) {
lrcTimeArr = lrcTime.split(":")
return +lrcTimeArr[0] * 60 + +lrcTimeArr[1];
}
用到的知識點:
1.在字元串前加上+,可以將其轉換為數字
<2>. 獲取對應歌詞的下標:
// 獲取當前播放到的歌詞的下標
function getIndex() {
let Time = doms.audio.currentTime;
for (let i = 0; i < result.length; i++) {
if (result[i].time > Time) {
return i - 1;
}
}
}
這裡當匹配到歌詞數組中,匹配到的第一個播放時間大於當前播放時間的歌詞,它的前一個歌詞即為當前播放的歌詞,因為既然還沒到這一句,那就是前一句。
<3>. 創建歌詞li
// 創建歌詞列表
function createElements() {
let frag = document.createDocumentFragment(); // 文檔片段
for (let i = 0; i < result.length; i++) {
let li = document.createElement("li");
li.innerText = result[i].word;
frag.appendChild(li);
}
doms.ul.appendChild(frag);
}
createElements();
用到的知識點:
1.文檔片段 document.createDocumentFragment() 【但其實七十個不多】
為了不頻繁的改動頁面的佈局導致reflow而影響效率,原先需要加入七十多次li,
這裡可以統一先將li加入到文檔片段frag中,最後只需ul加入一次frag,即可完成。
<4>. 滾動函數
// 獲取顯示視窗的可視高度
let containerHeight = doms.container.clientHeight;
// 獲取歌詞列表的可視高度
let liHeight = doms.ul.children[0].clientHeight;
// 設置最大最小偏移量,防止顯示效果不佳
let minOffset = 0;
let maxOffset = doms.ul.clientHeight - containerHeight;
// 控制歌詞滾動移動的函數
function setOffset() {
let index = getIndex();
// 計算滾動距離
let offset = liHeight * index - containerHeight / 2 + liHeight / 2;
if (offset < minOffset) {
offset = minOffset;
};
if (offset > maxOffset) {
offset = maxOffset;
};
// 滾動
doms.ul.style.transform = `translateY(-${offset}px)`;
// 清除之前的active
let li = doms.ul.querySelector(".active")
if (li) {
li.classList.remove("active");
}
// 為當前所唱到的歌詞添加active
li = doms.ul.children[index];
if (li) {
li.classList.add("active");
}
};
要用到的知識點:
1. clientHeight 獲取可視高度
2.模板字元串(``)
3.利用transform:translateY();來進行滾動,不用margin-top原因也是因為會影響佈局導致reflow,影響效率。
<5>.給audio綁定播放時間更新事件:
// 當audio的播放時間更新時,觸發該事件 doms.audio.addEventListener("timeupdate", setOffset);
結語:
感謝觀看,文章主為記錄個人筆記方便以後重溫,希望能為各位解開一些疑惑