1. 需求文档 && 解决思路
qq 音乐:
- 用 rem 布局解决移动端适配问题
- 播放音频(audio);页面加载后,自动播放;通过音符按钮来手动控制音频的播放和暂停;点击音符按钮到底是播放还是暂停,依赖于音频的播放状态;
随着播放要做以下处理:
3.1 高亮当前进度匹配的歌词,歌词还会向上滑
3.2 更新当前的播放进度
3.3 更新进度条
zepto: 轻量级用于移动端的库,和 jq 使用起来一样的;但是有一些方法它没有;zepto 提供专门的移动端事件;
常用的方法,如获取元素、addClass、removeClass … 用法和 jq 一样;
在页面中引入的时候,要在咱们自己的写的 js 文件之前引入;
2. html 部分
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="stylesheet" href="css/reset.min.css"><link rel="stylesheet" href="css/index.css"><script src="js/rem.js"></script></head><body><section class="container" id="container"><!--audio 是 h5 新增的标签,用于播放音频;--><audio src="img/myDream.m4a" id="audio"></audio><!--背景图--><div class="backgroundImg"></div><!--背景蒙层--><div class="bg"></div><header class="header" id="header"><img src="img/myDream.jpg" alt=""><div class="song"><p>我的梦</p><p>张靓颖</p></div><a href="javascript:;" class="musicBtn" id="musicBtn"></a></header><main class="main" id="main"><!--main 一个溢出隐藏的盒子;main 这个盒子的高度应该是动态计算的;为了保证在页面充满一屏的情况下,尾部在页面底部 --><div class="wrapper"><!--wrapper 里面放的都是歌词,歌词应该是动态绑定的--><p class="select">一直地一直地往前走</p><p>一直地一直地往前走</p><p>一直地一直地往前走</p><p>一直地一直地往前走</p><p>一直地一直地往前走</p><p>一直地一直地往前走</p><p>一直地一直地往前走</p><p>一直地一直地往前走</p><p>一直地一直地往前走</p></div></main><footer class="footer" id="footer"><div class="progress" id="progress"><span class="current">00:00</span><span class="duration">00:00</span><div class="probg" id="probg"><div class="already"></div></div></div><a href="javascript:;" class="down" id="down">下载这首音乐</a></footer></section><script src="js/zepto.min.js"></script><script src="js/index.js"></script></body></html>
3. css 部分
html {font-size: 100px;height: 100%; /*如果你的应用要充满一屏幕,html 、body 标签的 height 设为 100%*/}body {height: 100%;}.container {width: 100%;height: 100%;position: relative;}.container .backgroundImg, .container .bg {width: 100%;height: 100%;position: absolute;top: 0;left: 0;z-index: -2;}.container .backgroundImg {background: url("../img/myDream.jpg");background-size: cover;filter: blur(6px); /*filter 是滤镜属性;blur 是模糊程度*/}.container .bg {background: rgba(0, 0, 0, .3);}/*HEADER*/.container .header {position: relative;padding: .3rem;background: rgba(0, 0, 0, .4);}.container .header img {width: 1.2rem;height: 1.2rem;}.container .header .song {display: inline-block;vertical-align: top;}.header .song p {height: .6rem;line-height: .6rem;color: #fff;}.header .song p:nth-child(1) {font-size: .45rem;}.header .musicBtn {display: block;position: absolute;width: .6rem;height: .6rem;right: .3rem;top: 50%;margin-top: -0.3rem; /*top 50% margin-top 负的高度的一半 让元素在垂直方向上居中*/background: url("../img/music.svg") no-repeat;border-radius: 50%;background-size: 100%;}/*定义关键帧动画,实现旋转*/@keyframes move {from {transform: rotate(0deg);}to {transform: rotate(360deg);}}.header .musicBtn.select {animation: move 1s linear 0s infinite;}.container .main {position: relative;height: 8rem; /* main 部分的高度应该是在 js 中动态计算的*/padding: .3rem;overflow: hidden;}.container .main .wrapper {position: absolute;top: 0;left: 0;width: 100%;transition: all .5s linear 0s; /* 为了保证歌词移动的时候有过渡效果 */}.main .wrapper p {height: .84rem;line-height: .84rem;text-align: center;color: rgba(255, 255, 255, .5);font-size: .4rem;}.main .wrapper p.select {color: #31c27c;}/*FOOTER*/.footer .progress {width: 100%;position: relative;overflow: hidden;}.footer .progress span {position: absolute;color: #fff;}.footer .progress span.current {left: .3rem;}.footer .progress span.duration {right: .3rem;}.footer .progress .probg {width: 65%;margin: .15rem auto;background: rgba(255, 255, 255, .5);height: .04rem;}.progress .probg .already {width: 0; /*这个进度条的宽也不是写死的,而是随着歌曲的播放进度更新的*/height: 100%;background: #31c27c;}.footer .down {display: block;width: 60%;height: 1rem;line-height: 1rem;text-align: center;color: #fff;font-size: .4rem;background: url("../img/sprite_play.png") no-repeat .2rem -5.86rem #31c27c;background-size: .8rem 7rem;border-radius: .5rem;margin: auto;}
4. js 部分
// 1. 获取元素对象let audio = document.getElementById('audio'); // 获取 audio 标签let $header = $('.header'); // 获取头let $footer = $('.footer'); // 获取尾部let $musicBtn = $('.musicBtn'); // 音符按钮let $main = $('.main'); // 包裹 wrapper 的容器let $wrapper = $('.wrapper'); // 获取包裹歌词的容器,歌词滚动的效果就是操作 $wrapper 相对于 $main 的 top 值let $current = $('.current'); // 音频当前播放的时间let $duration = $('.duration'); // 音频的总时长let $already = $('.already'); // 进度条let autoTimer = null; // 定义一个变量存储定时器 id// 2. 动态计算 main 区域的高度// main 的高度 = 视口的高度 - header的高度 - footer 的高度 - 0.6 rem的 padding (height 是指内容区域的高度)function computeMain () {let winH = document.documentElement.clientHeight; // 获取视口的高度let headerH = $header[0].offsetHeight; // 获取 header 的高度let footerH = $footer[0].offsetHeight; // 获取 footer 的高度// winH、headerH、footerH 都是以 px 为单位的,需要转成 rem 才能计算;// 如何把 winH、headerH、footerH 转成 rem?用 px 除以 HTML 的 fontSize 就可以转成 remlet fontSize = parseFloat(document.documentElement.style.fontSize); // 这个 fontSize 是一个带单位的字符串,需要转成数字let curH = (winH - headerH - footerH) / fontSize - 0.6 - 0.3; // 多减去的 0.3 是为了让下载按钮距离底部有一点距离$main.css({height: curH + 'rem'})}computeMain();window.addEventListener('resize', computeMain); // 当屏幕尺寸发生变化时,重新计算 main 区域的高度// 2. 通过 ajax 获取歌词$.ajax({url: 'json/lyric.json', // 接口method: 'GET', // HTTP METHOD: GET POSTasync: false, // async 是否异步,默认值是 trueerror (err) {console.log(err)},success ({ lyric }) {// console.log(lyric)// lyric 就是我们需要的歌词数据,我们需要把歌词绑定到页面中bindHTML(lyric)}});// 绑定数据function bindHTML(data) {// 处理第一句:// 我的梦 (华为手机主题曲) - 张靓颖// 我的梦 (华为手机主题曲) - 张靓颖 //   -> ' '// ( -> '('// ) -> ')'// - -> '-'let d1 = data.replace(/&#(\d+);/g, (wholeMatch, group) => {// replace 方法使用回调函数进行替换时,用回调函数的返回值替换正则捕获的内容;switch (parseFloat(group)) {case 32:return ' ';case 40:return '(';case 41:return ')';case 45:return '-'}return wholeMatch // 本次替换我们只想处理 32 40 41 45;其他的情况我们不做替换,所以我们原样// 返回大正则捕获到的内容});// console.log(d1)// 处理每一秒的歌词// [00:08.73]一直地一直地往前走 // : 前面的数字是 分钟数// . 前面的是 秒数// 方括号以后 之前的是 歌词let reg = /\[(\d+):(\d+).(?:\d+)\]([^&#]+)
/g;let ary = [];// 使用正则 + replace 遍历歌词,把需要的数据保存到数组中d1.replace(reg, (wholeMatch, minute, seconds, value) => {// 正则能够匹配到多少次,这个回调函数就会执行多少次ary.push({minute,seconds,value})});// console.log(ary)// 绑定数据let str = ``;ary.forEach(({ minute, seconds, value }) => {str += `<p data-min="${minute}" data-sec="${seconds}">${value}</p>`});$wrapper.html(str);// 歌词就绪后,让音乐自动播放// audio.play(); // audio 元素上自带播放和暂停的方法;// play 播放;pause 暂停// $musicBtn.addClass('select')}// 处理音符按钮的点击事件: zepto 提供了一个 tap 方法用于给元素绑定触摸事件;$musicBtn.tap(function () {// 如果音频是暂停的,需要让音频播放;如果是播放的,就让音频暂停;// audio 有一个 paused 属性,是一个布尔值,true 表示处于暂停,false 表示正在播放if (audio.paused) {// 暂停的audio.play();$(this).addClass('select')// 每隔一秒钟计算一下进度autoTimer = setInterval(computeTime, 1000)} else {// 播放中audio.pause();$(this).removeClass('select');clearInterval(autoTimer); // 当音频停止播放的时候,清除定时器;}});let step = 0; // 前 5 行,歌词不会向上划动,所以需要记录当前匹配到多少行let curTop = 0; // 记录 wrapper 相对于 main 的 top 值;初始值是 0;function computeTime() {// 获取当前音频的播放进度,audio 标签身上有两个属性:// currentTime 表示当前音频已经播放的时间,单位:秒// duration 表示当前音频的总时长,单位:秒let { currentTime, duration } = audio;let curTime = formatTime(currentTime);let durTime = formatTime(duration);// 把时间进度回填到页面中$current.html(curTime);$duration.html(durTime);// 更新进度条: 进度条的宽度就是 当前时间 / 总时长 的百分比$already.css({width: currentTime / duration * 100 + '%'});// 高亮歌词:就是从 wrapper 下找到和当前播放进度匹配的歌词的 p 标签,然后给他增加 select 类名,同时移除其兄弟们的 select 类名// curTime 02:34let [min, sec] = curTime.split(':');// console.log(min, sec)let highLight = $('.wrapper p').filter(`[data-min="${min}"]`).filter(`[data-sec="${sec}"]`);// 是用 filter 方法把和当前播放进度匹配的歌词 p 标签找到;if (highLight.length) {highLight.addClass('select').siblings().removeClass('select')// 匹配到一次给 step 累加1step++;if (step >= 5) {curTop -= .84; // 让歌词向上移动一行,就是让 wrapper 的 top 值减小一个 p 标签的高度(0.84 就是 p 的高度)$wrapper.css({top: curTop + 'rem'})}}// 如果当前的播放时长大于等于总时长的时候,把定时器清除掉if (currentTime >= duration) {clearInterval(autoTimer);$musicBtn.removeClass('select')}}function formatTime(time) {// time 157.12345 slet min = Math.floor(time / 60);let sec = Math.floor(time - min * 60);if (min < 10) {min = '0' + min}if (sec < 10) {sec = '0' + sec}return `${min}:${sec}` // => 03:15}
