VUE移动端音乐APP学习【十五】:播放器模式切换功能实现

播放模式一共有3种:列表播放,循环播放,随机播放

这个播放模式对应到vuex的data里面是mode字段,getters有这个state的映射,在player.vue中的mapGetters里添加mode,这样就可以通过this.mode访问当前的播放模式

    ...mapGetters([
      'fullScreen',
      'playlist',
      'currentSong',
      'playing',
      'currentIndex',
      'mode',
    ]),

有了播放模式这个概念以后,关于播放模式的icon就不用写死了 :class="iconMode"

<i class="iconfont" :class="iconMode"></i>

通过计算属性显示相应的图标,需要从config.js引入表示3种播放模式的playMode

import { playMode } from '../../common/js/config';

    iconMode() {
      return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === playMode.loop ? 'icon-loop' : 'icon-random';
    },

在按钮上面添加点击事件changeMode,定义该方法:有3种播放模式,每点击一次就改变它的mode

<div class="icon i-left" @click="changeMode">
       <i class="iconfont" :class="iconMode"></i>
</div>

    changeMode() {
      const mode = (this.mode + 1) % 3;
    },

然后这个mode 通过vuexmutation把它设置到state上,需要在mapMutations映射mutation,有了映射后,就可以调用this.setPlayMode去改变它的mode

    ...mapMutations({
      setFullScreen: 'SET_FULL_SCREEN',
      setPlayingState: 'SET_PLAYING_STATE',
      setCurrentIndex: 'SET_CURRENT_INDEX',
      setPlayMode: 'SET_PLAY_MODE',
    }),

    changeMode() {
      // 有3种播放模式,每点击一次就改变它的mode
      const mode = (this.mode + 1) % 3;
      this.setPlayMode(mode);
    },

运行效果:

 vuex的state里面的playlist表示的是当前播放列表,当我们改变播放模式的时候,实际上是去修改这个播放列表,所以在playerchangeMode方法中,需要去修改这个列表。

①首先定义1个list,当播放模式为随机播放时,列表就要改变

changeMode() {
      // 有3种播放模式,每点击一次就改变它的mode
      const mode = (this.mode + 1) % 3;
      this.setPlayMode(mode);
      let list = null;
      if (this.mode === playMode.random) {
      
      } else {
        // 如果是顺序播放或者循环播放
       
      }
      
    },

②改变列表前需要拿到原始列表sequenceList,在getters里有这个state的映射,所以通过mapGetters获取这个sequenceList

...mapGetters([
      'fullScreen',
      'playlist',
      'currentSong',
      'playing',
      'currentIndex',
      'mode',
      'sequenceList',
    ]),

③对这个列表进行洗牌,把它变成一个随机的列表。将数组打乱这个功能是经常使用的,可以在common->js文件夹下建立util.js(工具方法集合),在里面实现一个洗牌函数shuffle

思路:遍历array,从0-i之间随机取个数(索引),把这个索引对应的元素和当前元素arr[i]做交换,这样就会把这个数组洗的很乱

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}
export function shuffle(arr) {
  for (let i = 0; i < arr.length; i++) {
    let j = getRandomInt(0, i);
    let t = arr[i];
    arr[i] = arr[j];
    arr[j] = t;
  }
  return arr;
}

辅助函数getRandomInt返回min和max之间的一个随机数包括min和max。Math.random()是返回0和1之间不包括1的随机数,所以就用Math.random() * (max - min + 1),+1是为了确保能取到上限值,返回的是max-min中间的数,把数落在max和min之间还要+min。因为Math.random是个小数,所以还需要对它进行一个向下取整。这样的话就能获取到min到max中间的数且包括min和max。

④有了这样一个工具方法后,就可以调用它去给sequenceList进行洗牌然后赋值给list

if (this.mode === playMode.random) {
        list = shuffle(this.sequenceList);
      } else {
        // 如果是顺序播放或者循环播放
        list = this.sequenceList;
      }

⑤有了这个list以后,需要修改当前的playList为list,不过需要去对set_playlist映射,就可以调用方法进行修改了

...mapMutations({
      setFullScreen: 'SET_FULL_SCREEN',
      setPlayingState: 'SET_PLAYING_STATE',
      setCurrentIndex: 'SET_CURRENT_INDEX',
      setPlayMode: 'SET_PLAY_MODE',
      setPlaylist: 'SET_PLAYLIST',
    }),

    changeMode() {
      // 有3种播放模式,每点击一次就改变它的mode
      const mode = (this.mode + 1) % 3;
      this.setPlayMode(mode);
      let list = null;
      if (this.mode === playMode.random) {
        list = shuffle(this.sequenceList);
      } else {
        // 如果是顺序播放或者循环播放
        list = this.sequenceList;
      }
      this.setPlaylist(list);
    },

currentSong是根据playlistcurrentIndex计算而来的,我们修改了state.playlist之后,currentSong也势必发生改变。但是我们希望在切换播放模式的时候,这个currentSong并不改变,这个新的currentSong的id跟之前是一样的,所以需要去设置currentIndex。这里定义成方法resetCurrentIndex, 这个方法接收参数list,从list找到currentSong对应的索引,再调用mutation的setCurrentIndex去设置index。这样的话,当我们的playlist改变的时候,currentIndex也发生改变来保证currentSong的id不变,切换模式的时候当前歌曲始终不会发生变化。

    changeMode() {
      // 有3种播放模式,每点击一次就改变它的mode
      const mode = (this.mode + 1) % 3;
      this.setPlayMode(mode);
      let list = null;
      if (this.mode === playMode.random) {
        list = shuffle(this.sequenceList);
      } else {
        // 如果是顺序播放或者循环播放
        list = this.sequenceList;
      }
      this.resetCurrentIndex(list);
      this.setPlaylist(list);
    },
    resetCurrentIndex(list) {
      let index = list.findIndex((item) => {
        return item.id === this.currentSong.id;
      });
      this.setCurrentIndex(index);
    },

这里的findIndex()是es6的语法,使用箭头函数来遍历list所有的item,查找目标元素,找到就返回元素的位置,找不到就返回-1

 

⑦运行后会发现一个问题:当暂停的时候切换模式,可以发现歌曲在播放,但是图标为播放图标

原因:因为改变了playlist也改变了currentIndexcurrentSong还是会触发这个watch里的currentSong(),也就是currentSong在代码上还是改变了,只不过它的两次改变其id都是没变化的。因为这个回调一旦执行,它就会调用audio.play(),当暂停的时候切换模式它还是会播放。所以这个逻辑是有问题的。

解决:添加两个参数 newSong和oldSong,当改变的时候,如果newSong的id和oldSong的id一致,则return. 歌曲没变,所有根据currentSong改变而引发的回调的逻辑都不应该执行

 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('播放出错,暂无该歌曲资源');

        });
      });
    },

在暂停播放时不断切换模式,它也不会播放啦

 继续完善其他功能:

①当进度条到最后时,发现这个歌曲没有继续播放了,这是因为audio本身没有切换到下一首歌的功能,这个逻辑需要我们去实现。

audio标签给我们提供一个事件,当歌曲播放的时候会派发一个ended事件,我们可以利用这个事件做逻辑。我们在method里定义这个end,歌曲播放完了就切换下一首 this.next();但是如果考虑到播放模式为单曲循环的话,它也跳到下一首显示是不合理的。所以需要做个逻辑判断 如果当前模式为循环播放的话,就调用this.loop() 如果为剩下两种模式,才会调用this.next()。

实现loop()方法:把audio的currentTime切为0再调用audio.play()即可实现循环播放的功能。

<audio ref="audio" :src="currentSong.url" @canplay="ready" @error="error" @timeupdate="updateTime" @ended="end"></audio>

end() {
if (this.mode === playMode.loop) { this.loop(); } else { this.next(); } }, loop() { this.$refs.audio.currentTime = 0; this.$refs.audio.play(); },

达到预期期望,但是在当我们点击progressBtn的时候有个小问题,e.offsetX获取不对,它并不是真正的偏移量。

点击时除了可以获取到e.offsetX外还可以获取到e.pageX。pageX是获取的点到屏幕最左边的距离,但是超出了我们想要的距离。可以通过progressBar这个元素的getBoundingClientRect()返回的left值得到progressBar元素左边到视窗左边的距离,通过e.pageX - rect.left得到真正的偏移量offsetWidth

progress-bar.vue

progressClick(e) {
      const rect = this.$refs.progressBar.getBoundingClientRect();
      const offsetWidth = e.pageX - rect.left;
      this._offset(offsetWidth);
      // 当我们点击progressBtn的时候,e.offsetX获取不对
      // this._offset(e.offsetX);
      // 去通知外层改变了多少percent
      this._triggerPercent();
    },

②当点击上方的随机播放全部按钮时,也应该能播放歌曲并且播放模式为随机播放

music-list组件里给随机播放全部的按钮添加点击事件random,实现该方法。这个randomPlay也要和之前的selectPlay一样也是要去创建Actions

music-list.vue

<div class="play-wrapper">
        <div class="play" v-show="songs.length > 0" ref="playBtn" @click="random">
          <i class="icon-play"></i>
          <span class="text">随机播放全部</span>
        </div>
</div>

random() {
      this.randomPlay({
        list: this.songs,
      });
    },
    ...mapActions([
      'selectPlay',
      'randomPlay',
    ]),

actions.js

// randomPlay这里是没有索引的
export const randomPlay = function ({ commit }, { list }) {
  commit(types.SET_PLAY_MODE, playMode.random);
  commit(types.SET_SEQUENCE_LIST, list);
  // 洗牌歌曲
  let randomlist = shuffle(list);
  commit(types.SET_PLAYLIST, randomlist);
  commit(types.SET_CURRENT_INDEX, 0);
  commit(types.SET_FULL_SCREEN, true);
  commit(types.SET_PLAYING_STATE, true);
};

③需要修改一个地方:假设它已经在随机播放了,然后再点击列表中的歌曲,可以看到播放的并不是被点击的歌。

原因:在点击列表的时候,它会调用selectPlay,selectPlay的逻辑是按照播放模式为顺序播放搞的。但现在播放模式是random了,再将index设置为currentIndex就不合适了。以及playlist也不能设置为没有打乱的list

解决:sequence_list设置完了之后加个判断mode,参数里的state可以获取到当前的mode,当播放模式为随机播放时同样需要洗牌,然后提交随机歌曲列表。修改索引之前需要先找到顺序列表的这首歌对应到随机列表的索引

function findIndex(list, song) {
  return list.findIndex((item) => {
    return item.id === song.id;
  });
}

export const selectPlay = function ({ commit, state }, { list, index }) {
  commit(types.SET_SEQUENCE_LIST, list);
  if (state.mode === playMode.random) {
    let randomlist = shuffle(list);
    commit(types.SET_PLAYLIST, randomlist);
    index = findIndex(randomlist, list[index]);
  } else {
    commit(types.SET_PLAYLIST, list);
  }

  commit(types.SET_CURRENT_INDEX, index);
  commit(types.SET_FULL_SCREEN, true);
  commit(types.SET_PLAYING_STATE, true);
};

运行后发现还是有错误:播放模式为随机播放时,点击相同的歌播放的却是另一首。这是因为在shuffle函数中,传入arr,对这个arr做了修改,导致arr发生了变化。点击播放时selectPlay调用了shuffle以后呢,这个list发生了变化,已经不是原来的sequencelist了。

解决:不要让这个函数对list产生副作用,定义变量_arr为原来arr的副本。这样调用以后,list本身就不会发生改变

export function shuffle(arr) {
  let _arr = arr.slice();
  for (let i = 0; i < _arr.length; i++) {
    let j = getRandomInt(0, i);
    let t = _arr[i];
    _arr[i] = _arr[j];
    _arr[j] = t;
  }
  return _arr;
}

至此,播放模式的相关功能都已开发出来啦

 

posted @ 2021-04-27 13:52  小风车吱呀转  阅读(691)  评论(0编辑  收藏  举报