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'; },
<div class="icon i-left" @click="changeMode"> <i class="iconfont" :class="iconMode"></i> </div> changeMode() { const mode = (this.mode + 1) % 3; },
然后这个mode 通过vuex的mutation把它设置到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表示的是当前播放列表,当我们改变播放模式的时候,实际上是去修改这个播放列表,所以在player的changeMode方法中,需要去修改这个列表。
①首先定义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; }
④有了这样一个工具方法后,就可以调用它去给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是根据playlist和currentIndex计算而来的,我们修改了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也改变了currentIndex,currentSong还是会触发这个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; }
至此,播放模式的相关功能都已开发出来啦