1. 需求文档 && 解决思路

qq 音乐:

  1. 用 rem 布局解决移动端适配问题
  2. 播放音频(audio);页面加载后,自动播放;通过音符按钮来手动控制音频的播放和暂停;点击音符按钮到底是播放还是暂停,依赖于音频的播放状态;
  3. 随着播放要做以下处理:

    3.1 高亮当前进度匹配的歌词,歌词还会向上滑
    3.2 更新当前的播放进度
    3.3 更新进度条

zepto: 轻量级用于移动端的库,和 jq 使用起来一样的;但是有一些方法它没有;zepto 提供专门的移动端事件;
常用的方法,如获取元素、addClass、removeClass … 用法和 jq 一样;
在页面中引入的时候,要在咱们自己的写的 js 文件之前引入;

2. html 部分

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <link rel="stylesheet" href="css/reset.min.css">
  8. <link rel="stylesheet" href="css/index.css">
  9. <script src="js/rem.js"></script>
  10. </head>
  11. <body>
  12. <section class="container" id="container">
  13. <!--audio h5 新增的标签,用于播放音频;-->
  14. <audio src="img/myDream.m4a" id="audio"></audio>
  15. <!--背景图-->
  16. <div class="backgroundImg"></div>
  17. <!--背景蒙层-->
  18. <div class="bg"></div>
  19. <header class="header" id="header">
  20. <img src="img/myDream.jpg" alt="">
  21. <div class="song">
  22. <p>我的梦</p>
  23. <p>张靓颖</p>
  24. </div>
  25. <a href="javascript:;" class="musicBtn" id="musicBtn"></a>
  26. </header>
  27. <main class="main" id="main">
  28. <!--main 一个溢出隐藏的盒子;main 这个盒子的高度应该是动态计算的;为了保证在页面充满一屏的情况下
  29. ,尾部在页面底部 -->
  30. <div class="wrapper">
  31. <!--wrapper 里面放的都是歌词,歌词应该是动态绑定的-->
  32. <p class="select">一直地一直地往前走</p>
  33. <p>一直地一直地往前走</p>
  34. <p>一直地一直地往前走</p>
  35. <p>一直地一直地往前走</p>
  36. <p>一直地一直地往前走</p>
  37. <p>一直地一直地往前走</p>
  38. <p>一直地一直地往前走</p>
  39. <p>一直地一直地往前走</p>
  40. <p>一直地一直地往前走</p>
  41. </div>
  42. </main>
  43. <footer class="footer" id="footer">
  44. <div class="progress" id="progress">
  45. <span class="current">00:00</span>
  46. <span class="duration">00:00</span>
  47. <div class="probg" id="probg">
  48. <div class="already"></div>
  49. </div>
  50. </div>
  51. <a href="javascript:;" class="down" id="down">下载这首音乐</a>
  52. </footer>
  53. </section>
  54. <script src="js/zepto.min.js"></script>
  55. <script src="js/index.js"></script>
  56. </body>
  57. </html>

3. css 部分

  1. html {
  2. font-size: 100px;
  3. height: 100%; /*如果你的应用要充满一屏幕,html 、body 标签的 height 设为 100%*/
  4. }
  5. body {
  6. height: 100%;
  7. }
  8. .container {
  9. width: 100%;
  10. height: 100%;
  11. position: relative;
  12. }
  13. .container .backgroundImg, .container .bg {
  14. width: 100%;
  15. height: 100%;
  16. position: absolute;
  17. top: 0;
  18. left: 0;
  19. z-index: -2;
  20. }
  21. .container .backgroundImg {
  22. background: url("../img/myDream.jpg");
  23. background-size: cover;
  24. filter: blur(6px); /*filter 是滤镜属性;blur 是模糊程度*/
  25. }
  26. .container .bg {
  27. background: rgba(0, 0, 0, .3);
  28. }
  29. /*HEADER*/
  30. .container .header {
  31. position: relative;
  32. padding: .3rem;
  33. background: rgba(0, 0, 0, .4);
  34. }
  35. .container .header img {
  36. width: 1.2rem;
  37. height: 1.2rem;
  38. }
  39. .container .header .song {
  40. display: inline-block;
  41. vertical-align: top;
  42. }
  43. .header .song p {
  44. height: .6rem;
  45. line-height: .6rem;
  46. color: #fff;
  47. }
  48. .header .song p:nth-child(1) {
  49. font-size: .45rem;
  50. }
  51. .header .musicBtn {
  52. display: block;
  53. position: absolute;
  54. width: .6rem;
  55. height: .6rem;
  56. right: .3rem;
  57. top: 50%;
  58. margin-top: -0.3rem; /*top 50% margin-top 负的高度的一半 让元素在垂直方向上居中*/
  59. background: url("../img/music.svg") no-repeat;
  60. border-radius: 50%;
  61. background-size: 100%;
  62. }
  63. /*定义关键帧动画,实现旋转*/
  64. @keyframes move {
  65. from {
  66. transform: rotate(0deg);
  67. }
  68. to {
  69. transform: rotate(360deg);
  70. }
  71. }
  72. .header .musicBtn.select {
  73. animation: move 1s linear 0s infinite;
  74. }
  75. .container .main {
  76. position: relative;
  77. height: 8rem; /* main 部分的高度应该是在 js 中动态计算的*/
  78. padding: .3rem;
  79. overflow: hidden;
  80. }
  81. .container .main .wrapper {
  82. position: absolute;
  83. top: 0;
  84. left: 0;
  85. width: 100%;
  86. transition: all .5s linear 0s; /* 为了保证歌词移动的时候有过渡效果 */
  87. }
  88. .main .wrapper p {
  89. height: .84rem;
  90. line-height: .84rem;
  91. text-align: center;
  92. color: rgba(255, 255, 255, .5);
  93. font-size: .4rem;
  94. }
  95. .main .wrapper p.select {
  96. color: #31c27c;
  97. }
  98. /*FOOTER*/
  99. .footer .progress {
  100. width: 100%;
  101. position: relative;
  102. overflow: hidden;
  103. }
  104. .footer .progress span {
  105. position: absolute;
  106. color: #fff;
  107. }
  108. .footer .progress span.current {
  109. left: .3rem;
  110. }
  111. .footer .progress span.duration {
  112. right: .3rem;
  113. }
  114. .footer .progress .probg {
  115. width: 65%;
  116. margin: .15rem auto;
  117. background: rgba(255, 255, 255, .5);
  118. height: .04rem;
  119. }
  120. .progress .probg .already {
  121. width: 0; /*这个进度条的宽也不是写死的,而是随着歌曲的播放进度更新的*/
  122. height: 100%;
  123. background: #31c27c;
  124. }
  125. .footer .down {
  126. display: block;
  127. width: 60%;
  128. height: 1rem;
  129. line-height: 1rem;
  130. text-align: center;
  131. color: #fff;
  132. font-size: .4rem;
  133. background: url("../img/sprite_play.png") no-repeat .2rem -5.86rem #31c27c;
  134. background-size: .8rem 7rem;
  135. border-radius: .5rem;
  136. margin: auto;
  137. }

4. js 部分

  1. // 1. 获取元素对象
  2. let audio = document.getElementById('audio'); // 获取 audio 标签
  3. let $header = $('.header'); // 获取头
  4. let $footer = $('.footer'); // 获取尾部
  5. let $musicBtn = $('.musicBtn'); // 音符按钮
  6. let $main = $('.main'); // 包裹 wrapper 的容器
  7. let $wrapper = $('.wrapper'); // 获取包裹歌词的容器,歌词滚动的效果就是操作 $wrapper 相对于 $main 的 top 值
  8. let $current = $('.current'); // 音频当前播放的时间
  9. let $duration = $('.duration'); // 音频的总时长
  10. let $already = $('.already'); // 进度条
  11. let autoTimer = null; // 定义一个变量存储定时器 id
  12. // 2. 动态计算 main 区域的高度
  13. // main 的高度 = 视口的高度 - header的高度 - footer 的高度 - 0.6 rem的 padding (height 是指内容区域的高度)
  14. function computeMain () {
  15. let winH = document.documentElement.clientHeight; // 获取视口的高度
  16. let headerH = $header[0].offsetHeight; // 获取 header 的高度
  17. let footerH = $footer[0].offsetHeight; // 获取 footer 的高度
  18. // winH、headerH、footerH 都是以 px 为单位的,需要转成 rem 才能计算;
  19. // 如何把 winH、headerH、footerH 转成 rem?用 px 除以 HTML 的 fontSize 就可以转成 rem
  20. let fontSize = parseFloat(document.documentElement.style.fontSize); // 这个 fontSize 是一个带单位的字符串,需要转成数字
  21. let curH = (winH - headerH - footerH) / fontSize - 0.6 - 0.3; // 多减去的 0.3 是为了让下载按钮距离底部有一点距离
  22. $main.css({
  23. height: curH + 'rem'
  24. })
  25. }
  26. computeMain();
  27. window.addEventListener('resize', computeMain); // 当屏幕尺寸发生变化时,重新计算 main 区域的高度
  28. // 2. 通过 ajax 获取歌词
  29. $.ajax({
  30. url: 'json/lyric.json', // 接口
  31. method: 'GET', // HTTP METHOD: GET POST
  32. async: false, // async 是否异步,默认值是 true
  33. error (err) {
  34. console.log(err)
  35. },
  36. success ({ lyric }) {
  37. // console.log(lyric)
  38. // lyric 就是我们需要的歌词数据,我们需要把歌词绑定到页面中
  39. bindHTML(lyric)
  40. }
  41. });
  42. // 绑定数据
  43. function bindHTML(data) {
  44. // 处理第一句:
  45. // 我的梦 (华为手机主题曲) - 张靓颖
  46. // 我的梦&#32;&#40;华为手机主题曲&#41;&#32;&#45;&#32;张靓颖&#10;
  47. // &#32; -> ' '
  48. // &#40; -> '('
  49. // &#41; -> ')'
  50. // &#45; -> '-'
  51. let d1 = data.replace(/&#(\d+);/g, (wholeMatch, group) => {
  52. // replace 方法使用回调函数进行替换时,用回调函数的返回值替换正则捕获的内容;
  53. switch (parseFloat(group)) {
  54. case 32:
  55. return ' ';
  56. case 40:
  57. return '(';
  58. case 41:
  59. return ')';
  60. case 45:
  61. return '-'
  62. }
  63. return wholeMatch // 本次替换我们只想处理 32 40 41 45;其他的情况我们不做替换,所以我们原样
  64. // 返回大正则捕获到的内容
  65. });
  66. // console.log(d1)
  67. // 处理每一秒的歌词
  68. // [00&#58;08&#46;73]一直地一直地往前走&#10;
  69. // &#58; 前面的数字是 分钟数
  70. // &#46; 前面的是 秒数
  71. // 方括号以后 &#10; 之前的是 歌词
  72. let reg = /\[(\d+)&#58;(\d+)&#46;(?:\d+)\]([^&#]+)&#10/g;
  73. let ary = [];
  74. // 使用正则 + replace 遍历歌词,把需要的数据保存到数组中
  75. d1.replace(reg, (wholeMatch, minute, seconds, value) => {
  76. // 正则能够匹配到多少次,这个回调函数就会执行多少次
  77. ary.push({
  78. minute,
  79. seconds,
  80. value
  81. })
  82. });
  83. // console.log(ary)
  84. // 绑定数据
  85. let str = ``;
  86. ary.forEach(({ minute, seconds, value }) => {
  87. str += `<p data-min="${minute}" data-sec="${seconds}">${value}</p>`
  88. });
  89. $wrapper.html(str);
  90. // 歌词就绪后,让音乐自动播放
  91. // audio.play(); // audio 元素上自带播放和暂停的方法;
  92. // play 播放;pause 暂停
  93. // $musicBtn.addClass('select')
  94. }
  95. // 处理音符按钮的点击事件: zepto 提供了一个 tap 方法用于给元素绑定触摸事件;
  96. $musicBtn.tap(function () {
  97. // 如果音频是暂停的,需要让音频播放;如果是播放的,就让音频暂停;
  98. // audio 有一个 paused 属性,是一个布尔值,true 表示处于暂停,false 表示正在播放
  99. if (audio.paused) {
  100. // 暂停的
  101. audio.play();
  102. $(this).addClass('select')
  103. // 每隔一秒钟计算一下进度
  104. autoTimer = setInterval(computeTime, 1000)
  105. } else {
  106. // 播放中
  107. audio.pause();
  108. $(this).removeClass('select');
  109. clearInterval(autoTimer); // 当音频停止播放的时候,清除定时器;
  110. }
  111. });
  112. let step = 0; // 前 5 行,歌词不会向上划动,所以需要记录当前匹配到多少行
  113. let curTop = 0; // 记录 wrapper 相对于 main 的 top 值;初始值是 0;
  114. function computeTime() {
  115. // 获取当前音频的播放进度,audio 标签身上有两个属性:
  116. // currentTime 表示当前音频已经播放的时间,单位:秒
  117. // duration 表示当前音频的总时长,单位:秒
  118. let { currentTime, duration } = audio;
  119. let curTime = formatTime(currentTime);
  120. let durTime = formatTime(duration);
  121. // 把时间进度回填到页面中
  122. $current.html(curTime);
  123. $duration.html(durTime);
  124. // 更新进度条: 进度条的宽度就是 当前时间 / 总时长 的百分比
  125. $already.css({
  126. width: currentTime / duration * 100 + '%'
  127. });
  128. // 高亮歌词:就是从 wrapper 下找到和当前播放进度匹配的歌词的 p 标签,然后给他增加 select 类名,同时移除其兄弟们的 select 类名
  129. // curTime 02:34
  130. let [min, sec] = curTime.split(':');
  131. // console.log(min, sec)
  132. let highLight = $('.wrapper p').filter(`[data-min="${min}"]`).filter(`[data-sec="${sec}"]`);
  133. // 是用 filter 方法把和当前播放进度匹配的歌词 p 标签找到;
  134. if (highLight.length) {
  135. highLight.addClass('select').siblings().removeClass('select')
  136. // 匹配到一次给 step 累加1
  137. step++;
  138. if (step >= 5) {
  139. curTop -= .84; // 让歌词向上移动一行,就是让 wrapper 的 top 值减小一个 p 标签的高度(0.84 就是 p 的高度)
  140. $wrapper.css({
  141. top: curTop + 'rem'
  142. })
  143. }
  144. }
  145. // 如果当前的播放时长大于等于总时长的时候,把定时器清除掉
  146. if (currentTime >= duration) {
  147. clearInterval(autoTimer);
  148. $musicBtn.removeClass('select')
  149. }
  150. }
  151. function formatTime(time) {
  152. // time 157.12345 s
  153. let min = Math.floor(time / 60);
  154. let sec = Math.floor(time - min * 60);
  155. if (min < 10) {
  156. min = '0' + min
  157. }
  158. if (sec < 10) {
  159. sec = '0' + sec
  160. }
  161. return `${min}:${sec}` // => 03:15
  162. }

5. 完整资源包

q-music.zip