zoukankan      html  css  js  c++  java
  • VUE移动端音乐APP学习【十六】:播放器歌词显示开发

    播放器歌词数据抓取

    在api文件夹下创建song.js 设置获取歌曲api

    import axios from 'axios';
    
    export function getLyric(id) {
      return axios.get(`/api/lyric?id=${id}`);
    }

    把这个方法封装到common->js->下的song类,歌词可以理解为song的一个属性。不能直接拿歌词需要调用这个接口,所以给song扩展一个方法getLyric

    import { getLyric } from '../../api/song';
    import { ERR_OK } from '../../api/config';
    
    export default class Song {
      // song的id,歌手,歌曲名name,专辑名album,歌曲长度duration,歌曲图片img,歌曲的真实路径url
      constructor({
        id, singer, name, album, duration, image, url,
      }) {
        this.id = id;
        this.singer = singer;
        this.name = name;
        this.album = album;
        this.duration = duration;
        this.image = image;
        this.url = url;
      }
    
      getLyric() {
        getLyric(this.id).then((res) => {
          if (res.data.code === ERR_OK) {
            this.lyric = res.data.lrc.lyric;
            console.log(this.lyric);
          }
        });
      }
    }

    在player组件的watch里调用看看能不能正确获取到歌词

     watch: {
        currentSong(newSong, oldSong) {
          if (newSong.id === oldSong.id) {
            return;
          }
          this.$nextTick(() => {
            this.$refs.audio.play().catch((error) =>  {
              this.togglePlaying();
              // eslint-disable-next-line no-alert
              alert('播放出错,暂无该歌曲资源');
            });
            this.currentSong.getLyric();
          });
        },

    播放器歌词数据解析

    可以看到歌词是非常长的字符串。接下来就是解析字符串。利用第三方库 lyric-parser ,它支持传入lyricStrhandler,歌词在不断播放的时候,每执行到一个时间点都会执行handler函数

    npm install lyric-parser@1.0.1

    在数据解析之前需要优化一个地方:currentSong每次变化的时候都会调用song的getLyric方法,则会有很多次请求,这样是不合理的。

    所以要加个逻辑判断并利用Promise进行改造

    getLyric() {
        if (this.lyric) {
          // getLyric本身返回的就是Promise
          return Promise.resolve(this.lyric);
        }
        // 封装Promise,只用于获取歌词
        return new Promise((resolve, reject) => {
          getLyric(this.id).then((res) => {
            if (res.data.code === ERR_OK) {
              this.lyric = res.data.lrc.lyric;
              resolve(this.lyric);
            } else {
              // 获取不到歌词
              // eslint-disable-next-line prefer-promise-reject-errors
              reject('no lyric');
            }
          });
        });
    
      }

    player组件里引入插件lyric-parser,并且修改watch里的currentSong(),不直接调用currentSong.getLyric(),在methods里封装getLyric()

    import Lyric from 'lyric-parser';
    
    
    currentSong(newSong, oldSong) {
         ...
            // 这里不直接调用currentSong.getLyric()
            this.getLyric();
          });
        },
     getLyric() {
          this.currentSong.getLyric().then((lyric) => {
            this.currentLyric = new Lyric(lyric);
            console.log(this.currentLyric);
          });
        },
    
    //在data里添加currentLyric
     data() {
        return {
          songReady: false,
          currentTime: 0,
          radius: 32,
          currentLyric: null,
        };
      },

    打印可以看到lyric对象的数据,数据里有一个lines,每个lines对象都有time和txt

    播放器歌词滚动列表实现

    在player组件添加以下dom结构:使用获取到currentLyric,并且遍历lines对象

     <div class="middle-r" ref="lyricList">
        <div class="lyric-wrapper">
            <div v-if="currentLyric">
                 <p ref="lyricLine" class="text" v-for="(line,index) in currentLyric.lines" :key="index">{{line.text}}</p>
            </div>
        </div>
    </div>

    在浏览器先将middle-l的dom结构删掉,可以看到歌词列表

    目前的歌词列表是无法滚动的,也不能实时根据歌曲的播放显示对应的歌词,需要去处理一下

    当执行到getLyric()时,获取到歌词的时候,调用this.currentLyric.play(),这样的话歌词就会播放了。还要在初始化的时候传一个回调函数handleLyric()。

    getLyric() {
          this.currentSong.getLyric().then((lyric) => {
            this.currentLyric = new Lyric(lyric, this.handleLyric);
            if (this.playing) {
              this.currentLyric.play();
            }
            console.log(this.currentLyric);
          });
        },

    定义这个方法handleLyric():当歌词每一行发生改变的时候,它就回调一下,让当前的歌词变高亮。

    • 首先需要先去data里定义一个currentLineNum,表示当前所在的行。
     data() {
        return {
          songReady: false,
          currentTime: 0,
          radius: 32,
          currentLyric: null,
          currentLineNum: 0,
        };
      },
    • 在刚才的dom结构绑定一个class:当currentLineNum等于index的时候就显示current样式,实现高亮效果
    <p ref="lyricLine" class="text" :class="{'current':currentLineNum === index}" v-for="(line,index) in currentLyric.lines" :key="index">{{line.txt}}</p>
    • handleLyric设置当前currentLine等于index,这样就可以看到当前播放的歌词
    handleLyric({ lineNum, txt }) {
          this.currentLineNum = lineNum;
        },

    歌词列表如果想要实现滚动,就需要用到scroll组件。向scroll传入data是为了currentLyric发生变化的时候,它可以自动调用它的refresh方法

    <scroll class="middle-r" ref="lyricList" :data="currentLyric&&currentLyric.lines">
          <div class="lyric-wrapper">
                  <div v-if="currentLyric">
                    <p ref="lyricLine" class="text" :class="{'current':currentLineNum === index}" v-for="(line,index) in currentLyric.lines" :key="index">{{line.txt}}</p>
                  </div>
           </div>
    </scroll>
    
    
    import Scroll from '../../base/scroll/scroll';
    components: {
        ProgressBar,
        ProgressCircle,
        Scroll,
      },

    当我们歌词播放到中间的时候(歌词的第5-6行开始,可以保证在屏幕的中间),会有上下滚动的效果。这个时候如果滚动到一个位置它也会再滚回去。

    handleLyric({ lineNum, txt }) {
          this.currentLineNum = lineNum;
          if (lineNum > 5) {
            let lineEl = this.$refs.lyricLine[lineNum - 5];
            this.$refs.lyricList.scrollToElement(lineEl, 1000);
          } else {
            this.$refs.lyricList.scrollTo(0, 0, 1000);
          }
        },

    播放器歌词左右滑动实现

    在播放器页面有个dot的dom结构

    <div class="bottom">
         <div class="dot-wrapper">
                <span class="dot"></span>
                <span class="dot"></span>
        </div>
        ...
    </div>

    当前哪个点应该是active,用一个变量维护这个状态。currentShow默认为cd,当切换右边页面的时候,currentShow就改为lyric

      data() {
        return {
          songReady: false,
          currentTime: 0,
          radius: 32,
          currentLyric: null,
          currentLineNum: 0,
          currentShow: 'cd',
        };
      },
    <span class="dot" :class="{'active':currentShow==='cd'}"></span>
    <span class="dot" :class="{'active':currentShow==='lyric'}"></span>

    接下来就是实现左右滑动。

    • created()下定义touch变量
    • middle绑定touch事件,touchStart,touchMove,touchEnd,并定义这三个方法(其中还添加了一些动画效果使得滑动画面不生硬)
    middleTouchStart(e) {
          this.touch.initiated = true;
          const touch = e.touches[0];
          // 记录X坐标和Y坐标
          this.touch.startX = touch.pageX;
          this.touch.startY = touch.pageY;
        },
        middleTouchMove(e) {
          if (!this.touch.initiated) {
            return;
          }
          const touch = e.touches[0];
          const deltaX = touch.pageX - this.touch.startX;
          // 为什么要维护纵坐标呢,因为歌词滚动是用scroll是一个上下滚动的过程,当纵轴偏移大于横轴偏移时,就不应该左右移动
          const deltaY = touch.pageY - this.touch.startY;
          if (Math.abs(deltaY) > Math.abs(deltaX)) {
            return;
          }
          // 在滚动时,需要知道歌词列表滚动的宽度是多少。首先要记录在滚动过程中,middle-r距离右侧的宽度
          const left = this.currentShow === 'cd' ? 0 : -window.innerWidth;
          // 最大不超过0
          const offsetWidth = Math.min(0, Math.max(-window.innerWidth, left + deltaX));
          // lyricList实际上是个scroll组件,即vue组件,是没法直接操作dom,需要访问它的element才能访问dom
          // 滑动的比例=列表向左宽度的宽度/整个屏幕的宽度
          this.touch.percent = Math.abs(offsetWidth / window.innerWidth);
          this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)`;
          this.$refs.lyricList.$el.style[transitionDuration] = 0;
          // percent越大,透明度就越小
          this.$refs.middleL.style.opacity = 1 - this.touch.percent;
          this.$refs.middleL.style[transitionDuration] = 0;
        },
        middleTouchEnd() {
          let offsetWidth;
          let opacity;
          // 歌词页面从右向左滑
          if (this.currentShow === 'cd') {
            if (this.touch.percent > 0.1) {
              offsetWidth = -window.innerWidth;
              opacity = 0;
              this.currentShow = 'lyric';
            } else {
              offsetWidth = 0;
              opacity = 1;
            }
          } else { // 从左向右滑
          // 如果滑超过10%就要偏移回去
            // eslint-disable-next-line no-lonely-if
            if (this.touch.percent < 0.9) {
              offsetWidth = 0;
              opacity = 1;
              this.currentShow = 'cd';
            } else {
              offsetWidth = -window.innerWidth;
              opacity = 0;
            }
          }
          const time = 300;
          this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)`;
          // 加点动画效果,使得滑动效果不再生硬
          this.$refs.lyricList.$el.style[transitionDuration] = `${time}ms`;
          this.$refs.middleL.style.opacity = opacity;
          this.$refs.middleL.style[transitionDuration] = `${time}ms`;
        },
    View Code

    播放器歌词剩余功能实现

    有以下问题需要去完善

    • 多次切换歌曲时,歌词会不停来回跳动

    原因:歌词是用currentLyric对象内部的一些功能实现跳跃,每个currentLyric内部是用了一个计时器实现歌曲的播放跳到相应的位置。每次currentSong改变的时候,都会重新new一个新的lyric-parser出来,但是之前的对象并没有做清理操作,也就是之前的currentLyric还是有计时器在里面,所以造成了歌词来回闪动的bug.

    解决方法:在切currentSong即重新getLyric之前,把当前的getLyricstop

     currentSong(newSong, oldSong) {
          if (newSong.id === oldSong.id) {
            return;
          }
          if (this.currentLyric) {
            this.currentLyric.stop();
          }
         ...
        },
    • 点击暂停时,歌词并没有停止滚动

    原因:在播放状态改变的时候,歌词的播放状态没有改变

    解决方法:在togglePlaying()中添加判断逻辑:播放状态改变时,歌词的播放状态也随之改变

     togglePlaying() {
          // 如果没有ready好的话就直接返回
          if (!this.songReady) {
            return;
          }
          this.setPlayingState(!this.playing);
          if (this.currentLyric) {
            this.currentLyric.togglePlay();
          }
        },
    • 在循环播放模式下,将进度条切到末尾让它重新回到初始位置时,歌词并没有回到最初的位置

    解决方法:在loop()中实现逻辑,使用歌词的seek方法将它偏移到初始位置。

    loop() {
          this.$refs.audio.currentTime = 0;
          this.$refs.audio.play();
          if (this.currentLyric) {
            this.currentLyric.seek(0);
          }
        },
    • 在拖动进度条的时候,歌词并没有随着进度条的改变而改变

    解决方法:在onProgressBarChange()中实现逻辑,也是调用歌词的seek方法

    onProgressBarChange(percent) {
          const currentTime = this.currentSong.duration * percent;
          this.$refs.audio.currentTime = currentTime;
          if (!this.playing) {
            this.togglePlaying();
          }
          if (this.currentLyric) {
            this.currentLyric.seek(currentTime * 1000);
          }
        },
    • 在cd下方显示歌词,这样用户就不用每次切换到歌词列表看歌词

    解决方法:在cd-wrapper下面加一个div,传入数据playingLyric。playingLyric在handleLyric执行的时候改变。

    <div class="middle-l" ref="middleL">
         <div class="cd-wrapper" ref="cdWrapper">
               <div class="cd" :class="cdCls">
                    <img class="image" :src="currentSong.image">
               </div>
         </div>
         <div class="playing-lyric-wrapper">
               <div class="playing-lyric">{{playingLyric}}</div>
         </div>
    </div>
    
    
    
    data() {
        return {
          ...
          playingLyric: '',
        };
      },
    
    
    handleLyric({ lineNum, txt }) {
          ...
          this.playingLyric = txt;
        },
    • 考虑getLyric异常情况:获取不到歌词的时候,要做清理操作
    getLyric() {
          this.currentSong.getLyric().then((lyric) => {
            this.currentLyric = new Lyric(lyric, this.handleLyric);
            if (this.playing) {
              this.currentLyric.play();
            }
          }).catch(() => {
            this.currentLyric = null;
            this.playingLyric = '';
            this.currentLineNum = 0;
          });
        },
    • 考虑边界条件:当歌曲列表只有一首歌,点击下一首或上一首的时候会有什么问题?源代码中index = currentIndex + 1,currentIndex = 0;此时index等于playlist的长度,重置为0,然后又执行下面的逻辑将currentIndex置为0,则playlist不会发生变化,currentSong的id也不会发生变化,之后的逻辑都不会执行。

    解决方法:在next()和prev()加个判断,如果playlist的长度为1时,就让它使用loop()进行单曲循环。

    prev() {
          // 如果没有ready好的话就直接返回 不能使用下面的逻辑实现功能
          if (!this.songReady) {
            return;
          }
          if (this.playlist.length === 1) {
            this.loop();
          } else {
            let index = this.currentIndex - 1;
            if (index === -1) {
              index = this.playlist.length - 1;
            }
            this.setCurrentIndex(index);
            if (!this.playing) {
              this.togglePlaying();
            }
          }
          this.songReady = false;
        },
    • 当我们在微信播放的时候,实际上js是不会执行的但是audio可以将当前歌曲播放完。一旦歌曲播放完就会触发end事件,但是end事件是js不会执行。如果end不执行,那么再次播放的时候,songReady就一直不会设置为true,我们就切换不了歌曲。

    解决方法:让audio的play方法延迟时间更长一点,保证在手机浏览器从后台切换到前台js执行的时候,播放器可以正常播放。

    currentSong(newSong, oldSong) {
          if (newSong.id === oldSong.id) {
            return;
          }
          if (this.currentLyric) {
            this.currentLyric.stop();
          }
          setTimeout(() => {
            this.$refs.audio.play().catch((error) =>  {
              this.togglePlaying();
              // eslint-disable-next-line no-alert
              alert('播放出错,暂无该歌曲资源');
            }, 1000);
            // 这里不直接调用currentSong.getLyric()
            this.getLyric();
          });
        },

    播放器底部播放器适配

    以前页面的滚动高度都是计算到底的,但是现在有了迷你播放器占了底部一定高度,scroll滚动的高度就出错了。

    监听playerlist,如果当有playerlist的时候,scoll组件的bottom值重新设置成mini-player的高度,让它重新计算scorll滚动的高度。

    因为这些组件都需要处理这个问题,处理这个问题的逻辑又非常类似,可以使用mixin

    创建mixin.js

    export const playlistMixin = {
      computed: {
        // 通过getters拿到playlist
        ...mapGetters([
          'playlist',
        ]),
      },
      mounted() {
        this.handlePlaylist(this.playlist);
      },
      activated() {
        this.handlePlaylist(this.playlist);
      },
      watch: {
        playlist(newVal) {
          this.handlePlaylist(newVal);
        },
      },
      methods: {
        handlePlaylist() {
          // 具体方法要到具体组件实现
          // 抛个异常,组件必须实现这个函数,一旦组件定义这个函数,它就会覆盖mixin里的这个函数。如果没有则调用mixin里的这个函数
          throw new Error('component must implement handlePlaylist method');
        },
      },
    };

    一个组件可以插入多个mixin,所以有个mixins属性使用。在music-list组件应用mixin,一旦组件使用了mixin,就必须定义handlePlaylist方法不然会报错.

    定义handlePlaylist方法,判断如果有playlist,改变改变list的bottom并强制scroll重新计算

    import { playlistMixin } from '../../common/js/mixin';
    
    
    mixins: [playlistMixin],
    
    handlePlaylist(playlist) {
          const bottom = playlist.length > 0 ? '60px' : '';
          this.$refs.list.$el.style.bottom = bottom;
          // 调用refresh()让scroll重新计算高度
          this.$refs.list.refresh();
        },

    singer组件同理,但是需要调用listview让它重新计算,在listview.vue中暴露一个refresh方法后,再在singer.vue中调用

    refresh() {
          this.$refs.listview.refresh();
        },
    <div class="singer" ref="singer">
        <list-view @select="selectSinger" :data="singers" ref="list"></list-view>
        <router-view></router-view>
    </div>
    
     handlePlaylist(playlist) {
          const bottom = playlist.length > 0 ? '60px' : '';
          this.$refs.list.$el.style.bottom = bottom;
          // 调用refresh()让scroll重新计算高度
          this.$refs.list.refresh();
        },

    最后修改推荐页面

    handlePlaylist(playlist) {
          const bottom = playlist.length > 0 ? '60px' : '';
          this.$refs.recommend.style.bottom = bottom;
          // 调用refresh()让scroll重新计算高度
          this.$refs.scroll.refresh();
        },
  • 相关阅读:
    php遍历目录
    PHP处理一个5G文件,使用内存512M的,数据为整形,从大到小排序,优化排序算法
    百钱买百鸡问题 php版本
    青蛙跳100级台阶算法,完整可运行,php版本
    网站如何整体换角度
    SOA架构设计(转发)
    一些稍微复杂点的sql语句
    php变量和数组大小限制
    uploadify中文开发文档,解决php多图上传
    mysql索引的一些知识
  • 原文地址:https://www.cnblogs.com/Small-Windmill/p/14725002.html
Copyright © 2011-2022 走看看