超长篇幅,这一章让人想要放弃,但是坚持下来更加巩固自己的知识了,谢谢我的老师对我的鼓励。
包括播放器 Vuex 数据设计和相关应用、播放器基础样式及歌曲数据应用播放器展开收起动画的实现、播放器前进后退功能实现、播放器播放时间获取和更新、progress-bar 进度条组件开发、progress-circle 圆形进度条组件开发、播放器模式切换功能实现、播放器歌词数据抓取和解析、播放器歌词左右滑动的实现、播放器底部播…
7-1 播放器页面设计详解
播放器可以通过歌手详情列表、歌单详情列表、排行榜、搜索结果多种组件打开,因此播放器数据一定是全局的。因为在每个页面都可以调起播放器,所以播放器并不是一个路由组件,而是一个公共的组件。每个页面都可能会用到,我们把它放在app.vue中。播放器是一个业务组件,并不是基础组件,我们把它放在components目录下。player/player.vue
在app.vue中引入player组件
7-2 播放器Vuex数据设计
在多个页面都可以打开播放器的,那么这个播放器是全局的,我们可以通过vuex来管理播放器,首先我们需要想清楚,播放器相关的数据有哪些
state | 描述 |
playing | 暂停,播放 |
fullScreen | 全屏和迷你 |
mode | 播放的模式默认是顺序播放 |
playlist | 播放的列表(当顺序播放的时候是一个playlist列表,当点随机或者循环时候sequenceList和playlist列表不一样) |
sequenceList | 默认的播放列表(默认是顺序播放的列表) |
currentIndex | 当前播放的是哪一首歌(currentSong可以被playList和currentIndex计算出来) |
播放模式常量配置 common\js\config.js
export const playMode = {
sequence:0,//顺序播放
loop: 1,//循环播放
random: 2//随机播放
}
components\store\state.js
import {playMode} from 'common/js/config'
const state={
singer:{},//歌手
playing:true,//是否在播放
fullScreen:true,//展开和隐藏
mode:playMode.sequence,//播放的模式默认是顺序播放
currentIndex:0,//当前播放的是哪一首歌
playlist:[], //当前播放的列表。(根据模式变化而变化)
sequenceList: [], //顺序播放的列表
}
export default state
src\store\getters.js
//歌手
export const singer=state=>state.singer
//是否在播放
export const playing=state=>state.playing
//展开和隐藏
export const fullScreen=state=>state.fullScreen
//播放的模式
export const mode=state=>state.mode
//当前播放的是哪一首歌
export const currentIndex=state=>state.currentIndex
//当前的播放列表,当mode改变时,playlist也要改变
export const playlist=state=>state.playlist
//存初始的播放列表(默认是顺序播放)
export const sequenceList=state=>state.sequenceList
//当前播放的哪一首歌
export const currentSong=state=>state.playlist[state.currentIndex]
store\mutation-types.js
//歌手
export const SET_SINGER='SET_SINGER'
//是否在播放
export const SET_PLAYING='SET_PLAYING'
//是全屏还是迷你
export const SET_FULLSCREEN='SET_FULLSCREEN'
//当前的播放模式
export const SET_MODE='SET_MODE'
//当前播放的是那一项
export const SET_CURRENTINDEX='SET_CURRENTINDEX'
//当前的播放列表
export const SET_PLAYLIST='SET_PLAYLIST'
//初始播放列表
export const SET_SEQUENCELIST='SET_SEQUENCELIST'
store\mutations.js
import * as types from './mutation-types'
const mutations={
//歌手
[types.SET_SINGER](state,singer){
state.singer=singer
},
//改变播放的状态
[types.SET_PLAYING](state,playing){
state.playing=playing
},
//是否全屏
[types.SET_FULLSCREEN](state,flag){
state.fullScreen=flag
},
//播放模式
[types.SET_MODE](state,mode){
state.mode=mode
},
//播放的是哪一项
[types.SET_CURRENTINDEX](state,index){
state.currentIndex=index
},
//改变播放列表
[types.SET_PLAYLIST](state,list){
state.playlist=list
},
//初始播放列表
[types.SET_SEQUENCELIST](state,list){
state.sequenceList=list
}
}
export default mutations
所以大家通常给vuex定义数据的时候,一般先改4个文件
- 首先先定义state,我们先想清楚我们的原始的数据是什么,这些数据最好都是组件或者模块中的一些最底层的数据
- 接着是getters,getters的数据其实是对state数据的一层映射,在getters写的都是一些函数,这类似我们写的计算属性,它可以根据state中不用的值来计算一个新的值,比如currentSong。在getters里也可以写一些复杂的逻辑
- 数据有了,那么怎么修改这些数据呢,我们在mutations写对这些数据修改的逻辑,那么在写mutation之前我们先要写mutation-type文件的常量。 mutation-types文件,定义的常量通常是一个动词,它供actions和mutations使用
- mutations文件中的每个mutation本质就是一个函数,函数的名字就是mutations-type的对应的常量,参数就是state和传入参数
在这里我们先不写actions部分,actions的使用通常有以下两个场景。
一种情况是当需要进行异步操作的时候,
另一种情况是对mutations进行封装,比如当某个操作需要出发多个mutation,我们可以把这些mutation都用一个actions去封装,从达到调用一个action同时改变多个mutation
7-4 播放器基础结构及歌曲数据的应用
当我们点击歌手页面的的时候
src\components\base\song-list\song-list.vue
//只负责emit,不需要理会外部怎么使用数据,外部需要什么样的数据,只需将自己能够派发出去的派发出去
selectItem(song,index){
this.$emit('select',song,index)
}
当music-list\收到emit的时候,立即改变vuex中的数据
- 需要改变当前的playing
- 需要改变当前的fullScreen
- 需要改变当前的currentIndex
- 需要改变当前的playlist
- 需要改变当前的sequenceList
所以我们需要派发一个action。让action改变多个mutation
src\store\actions.js
import * as types from './mutation-types'
export const selectPlay=function({commit,state},{list,index}){
commit(types.SET_PLAYING,true)
commit(types.SET_FULLSCREEN,true)
commit(types.SET_CURRENTINDEX,index)
commit(types.SET_PLAYLIST,list)
commit(types.SET_SEQUENCELIST,list)
}
src\components\music-list\music-list.vue
import {mapActions} from 'vuex'
selectItem(song,index){
//获取到当前的列表
this.selectPlay({
list:this.songs,
index:index
})
},
...mapActions(['selectPlay']),
我们一旦点击列表的时候就可以派发action啦
前
后
用mapGetters 语法糖得到要渲染到组件上的数据
import {mapGetters} from 'vuex'
computed: {
...mapGetters([
'fullScreen',
'playlist',
'currentSong'
])
},
<template>
<!-- 当有列表的时候才展示播放组件 -->
<div class="player" v-show="playlist.length">
<!-- 大播放器 -->
<div class="normal-player" v-show="fullScreen" >
<!-- 背景高斯模糊 -->
<div class="background">
<img width="100%" height="100%" :src="currentSong.image">
</div>
<!--top标题部分 -->
<div class="top">
<div class="back"><i class="icon-back"></i></div>
<h1 class="title">{{currentSong.name}}</h1>
<h2 class="subtitle">{{currentSong.singer}}</h2>
</div>
<!-- 中间唱片转圈 -->
<div class="middle">
<!-- 唱片下方带歌词 -->
<div class="middle-l">
<div class="cd-wrapper">
<div class="cd">
<img class="image" :src="currentSong.image">
</div>
</div>
<div class="playing-lyric-wrapper">
<div class="playing-lyric">制作人:周杰伦</div>
</div>
</div>
<!-- 展开全部歌词 -->
<div class="middle-r" style="display:none"></div>
</div>
<!-- 底部控制部分 -->
<div class="bottom">
<div class="operators">
<div class="icon i-left">
<i class="icon-sequence"></i>
</div>
<div class="icon i-left">
<i class="icon-prev"></i>
</div>
<div class="icon i-center">
<i class="needsclick icon-play"></i>
</div>
<div class="icon i-right">
<i class="icon-next"></i>
</div>
<div class="icon i-right">
<i class="icon icon-not-favorite"></i>
</div>
</div>
</div>
</div>
<!--小播放器 -->
<div class="mini-player" v-show="!fullScreen">
<!--可以转动的icon -->
<div class="icon">
<div class="imgWrapper">
<img width="40" height="40" class="" :src="currentSong.image">
</div>
</div>
<!-- 歌曲信息部分 -->
<!-- <div class="text">
<h2 class="name">布拉格广场</h2>
<p class="desc">蔡依林/周杰伦</p>
</div> -->
<!-- 播放暂停歌曲 -->
<div class="control">
<div class="progress-circle">
<i class="icon-mini icon-play-mini"></i>
</div>
</div>
<div class="control">
<div class="progress-circle">
<i class="icon-playlist"></i>
</div>
</div>
</div>
</div>
</template>
点击向下按钮展示和点击底部收起
我们直接在组件上commit一个mutation
import {mapGetters,mapMutations} from 'vuex'
在method里面
...mapMutations({
setFullScreen:'SET_FULLSCREEN'
}),
展开与隐藏的函数定义
open(){
this.setFullScreen(true)
},
close(){
this.setFullScreen(false)
}
在向下按钮中加上点击事件
<div class="back" @click="close"><i class="icon-back"></i></div>
当点击mini播放器时加上open事件事件
<div class="mini-player" v-show="!fullScreen" @click="open">
但是这样的展开和隐藏是非常生硬的,我们希望有一个过渡的效果
7-5 播放器展开收起动画 (过渡动画)
控制页面的展开模式,展开和迷你
给normal这个播放器加上normal过渡动画,top部分从上方滑动下来,bottom部分从底部滑上,整体的normal是淡入淡出效果
在normal-player外套上transition(回弹效果使用贝塞尔曲线)
<transition name="normal">
&.normal-enter-active, &.normal-leave-active
transition: all 0.4s
.top, .bottom
transition: all 0.4s cubic-bezier(0.86, 0.18, 0.82, 1.32)
&.normal-enter, &.normal-leave-to
opacity: 0
.top
transform: translate3d(0, -100px, 0)
.bottom
transform: translate3d(0, 100px, 0)
给mini播放器加上name为minii的transition ,效果展示为 慢慢淡入淡出
<transition name="mini">
&.mini-enter-active, &.mini-leave-active
transition: all 0.4s
&.mini-enter, &.mini-leave-to
opacity: 0
7-6 播放器展开收起动画(animation动画)
需求:
展开时,mini-player的专辑图片从原始位置飞入CD图片位置,同时有一个放大缩小效果, 对应顶部和底部的回弹;收起时,normal-player的CD图片从原始位置直接落入mini-player的专辑图片位置
外部库的使用
create-keyframe-animation 使用JavaScript在浏览器中动态生成CSS关键帧动画。
1、利用vue给我们提供的javascript钩子我们在上面写css3动画即可
<transition
name="normal"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
@after-leave="afterLeave"
>
2、封装_getPosAndScale()函数获取到初始位置(以大小cd的圆心计算圆心与圆心之间的距离),和初始比例
//首先需要获取到初始距离和比例
_getPosAndScale(){
const miniWidth=40;//mini-player的小cd的宽度
const normalWidth=window.innerWidth * 0.8;//大的cd的宽度
const normalpaddingTop=80;//大的cd顶部距离最上边界
//小cd的中心点的坐标
const minipaddingLeft=40;//小的cd的中心距离左边
const minipaddingBottom=30;//小的cd的中心点距离底部的距离
//小的cd到大的cs的中心点的距离
const x=window.innerWidth*0.5 - minipaddingLeft;
const y=window.innerHeight-normalpaddingTop-minipaddingBottom-0.5*normalWidth
//小的cd和大的cd的比例
const scale=miniWidth/normalWidth | 0;//比例取整
return {
x,
y,
scale
}
},
3、给cd-wrapper添加引用
<div class="cd-wrapper" ref="cdWrapper">
4、在时间钩子方法中利用create-keyframe-animation 实现动画效果
create-keyframe-animation
cnpm install create-keyframe-animation --save
import animations from 'create-keyframe-animation'
import {prefixStyle} from '@/common/js/dom'
//一进入的时候
enter(el,done){
let {x,y,scale}=this._getPosAndScale();
console.log(x,y,scale);
let animation={
0:{
transform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`
},
60:{
transform: `translate3d(0, 0, 0) scale(1.1)`
},
100:{
transform: `translate3d(0, 0, 0) scale(1)`
}
}
// this creates the animation above
animations.registerAnimation({
name: 'move',
// the actual array of animation changes
animation,
// optional presets for when actually running the animation
presets: {
duration: 400,
easing: 'linear'
}
})
//then run it 当执行done函数的时候自动到afterEnter钩子
animations.runAnimation(this.$refs.cdWrapper, 'move', done)
},
//进入之后
afterEnter(){
animations.unregisterAnimation('move')
//当动画结束之后清除cd-wrapper的style属性。因为当runAnimation的时候默认加上style=“transform: xxxx”行内样式
this.$refs.cdWrapper.style.animation=''
},
//离开的时机,从大的cd到小的cs的重点
leave(el, done){
let transform=prefixStyle('transform');
const {x, y, scale} = this._getPosAndScale()
this.$refs.cdWrapper.style.transition = 'all 0.4s'
this.$refs.cdWrapper.style[transform] = `translate3d(${x}px, ${y}px, 0)`
this.$refs.cdWrapper.addEventListener('transitionend', done)
},
//离开之后
afterLeave(){
let transform=prefixStyle('transform');
this.$refs.cdWrapper.style.transition = ''
this.$refs.cdWrapper.style[transform] = ''
},
7-7 播放器歌曲播放功能实现(歌曲的播放和暂停)
加上audio播放器
<!-- 加audio播放 -->
<audio :src="currentSong.url" ref="audio"></audio>
watch currentSongde变化,currentSong一变化,代表着已经切换一首歌了,当新打开一首歌的时候让它自动播放
watch: {
//监听currentSong的变化
currentSong(newSong){
this.$nextTick(()=>{
this.$refs.audio.play();
})
}
},
点击播放按钮,让歌曲的playing切换暂停和播放
<i class="needsclick icon-play" @click="togglePlaying"></i>
改变vuex的playing,所以我们需要发起一个mutation
...mapMutations({
setFullScreen:'SET_FULLSCREEN',
setPlaying:'SET_PLAYING'
}),
//播放和暂停
togglePlaying(){
//发起一个playing 的 mutation
this.setPlaying(!this.playing)
},
监听到playing的变化,然后根据playing控制播放歌曲和在暂停歌曲
playing(isplay){
this.$nextTick(()=>{
isplay ? this.$refs.audio.play() : this.$refs.audio.pause()
})
}
播放图标处理
<i class="needsclick" :class="playIcon" @click="togglePlaying"></i>
playIcon(){
return this.playing ? 'icon-pause' : 'icon-play'
}
现在可以播放歌曲了,也可以暂停歌曲了,
那么我们来做一个动画的效果,当播放歌曲的时候,normal和mini中的cd唱片在转圈,当暂停歌曲的时候,唱片停止
&.play
animation: rotate 20s linear infinite
&.pause
animation-play-state: paused
<div class="cd" style="transform: matrix(0.720933, 0.693005, -0.693005, 0.720933, 0, 0);" :class="cdCls">
cdCls(){
return this.playing ? 'play' : 'pause'
}
同时,我们也应该给mini的唱片部分加上转动的样式,给mini的播放器机上点击事件
<!--可以转动的icon -->
<div class="icon">
<div class="imgWrapper">
<img width="40" height="40" class="" :src="currentSong.image" :class="cdCls">
</div>
</div>
记得加上@click.stop ,icon-mini的父级本身有点击事件,这里阻止事件冒泡
<i class="icon-mini icon-play-mini" @click.stop="togglePlaying" :class="playIcon"></i>
7-8播放器歌曲播放功能实现(前进和后退)
给前进和后退按钮绑定prev个next方法
//上一首歌
prev(){
let index=this.currentIndex;
index--;
if(index==-1){
index=this.playlist.length;
}
this.setCurrentIndex(index)
},
//下一首歌
next(){
let index=this.currentIndex;
index++;
if(index==this.playlist.length){
index=0
}
this.setCurrentIndex(index);
},
这样可以前进和后退了,但是当我们暂停歌曲的时候,播放按钮的图标并不正确
所以我们切换上一首歌和下一首歌的时候,需要判断当前的playing状态是false的时候,我们才playing设置成true
if(!this.playing){
this.setPlaying(true);
}
问题:当前进和后退点击太快的话,会出现以下报错
Uncaught (in promise) DOMException
那是因为切换太快的时候,audio的数据还没准备好
解决。利用audio的canplay和error事件来解决
aduio事件 | 触发时机 |
canplay | 歌曲请求到的时候触发 |
error 1 | 歌曲请求失败的时候触发 |
<!-- 加audio播放 -->
<audio :src="currentSong.url" ref="audio" @canplay="ready" @error="error"></audio>
data() {
return {
songReady: false//标识歌曲是否准备好了
}
},
ready(){
this.songReady=true
}
在点击播放,前进后退的时候加上判断 this.songReady=true时才起作用,当数据请求失败的时候return掉。不播放。
if(!this.songReady){
return
}
其中prev()和next()中歌曲发生改变了之后,重置songReady为false,便于下一次ready()
this.songReady=false 便于下次播放
问题:当歌曲错误的时候,或者网络异常,songReady一直为false导致播放,前进后退功能不可用。
解决:在error被触发的时候,将songReady的功能变成true。这样就不影响其他功能的使用了
error(){
this.songReady=true
},
优化:
当歌曲请求错误的时候,让操作按钮变灰
<div class="icon i-left" :class="disableCls">
<i class="icon-prev" @click="prev"></i>
</div>
<div class="icon i-center" :class="disableCls">
<!-- 根据播放的状态来控制图标 -->
<i class="needsclick" :class="playIcon" @click="togglePlaying" ></i>
</div>
<div class="icon i-right" :class="disableCls">
<i class="icon-next" @click="next"></i>
</div>
计算属性disableCls
disableCls(){
return this.songReady ? '' : 'disable'
}
7-9 播放器播放时间获取
我们在data上维护一个currentTime ,用来存audio的时间
在audio中我们通过timeupdata可以派发一个timeupdata事件
timeupdate(e){
this.currentTime=e.target.currentTime
},
但是e.target.currentTime 是一个时间戳,我们需要转换一下
//将时间戳转换
formate(interval){
interval = interval | 0 //向下取整
const minute = interval / 60 | 0
const second = this._pad(interval % 60)
return `${minute}:${second}`
},
//用0补位,补2位字符串长度
_pad(num, n = 2){
let len = num.toString().length
while(len < n){
num = '0' + num
len++
}
return num
},
显示当前的播放的时间,总时长
<span class="time time-l">{{format(currentTime)}}</span>
<span class="time time-r">{{format(currentSong.duration)}}</span>
7-10 播放器progress-bar进度条组件实现
我们来创建一个基础组件,进度条组件 progress-bar.vue
base\progress-bar\progress-bar.vue
<!-- 进度条bar -->
<div class="progress-bar-wrapper">
<div class="progress-bar">
<!-- bar-inner 滚动条的黑色的背景部分 -->
<div class="bar-inner">
<!-- 黄色的滚动条
-->
<div class="progress">
</div>
<!--progress-btn-wrapper 按钮部分 -->
<div class="progress-btn-wrapper">
<div class="progress-btn">
</div>
</div>
</div>
</div>
</div>
<!-- 进度条bar end -->
需求:当歌曲播放的时候,黄色的进度条和按钮,跟着播放的时间变化
实现:根据传入的precent去得到黄色进度条的长度,和按钮的的向右的偏移
watch: {
precent(newPrecent){
if(newPrecent>=0){
//当前的进度条的宽度= 进度条背景的宽度-按钮的宽度
const barWidth =this.$refs.progressBar.clientWidth-progressBtnWidth;
//偏移的宽度
const offsetWidth=newPrecent*barWidth;
console.log(offsetWidth);
// //进度条偏移
this.$refs.progress.style.width = `${offsetWidth}px`;
// //小球偏移
let transform=prefixStyle('transform');
this.$refs.progressBtn.style[transform] = `translate3d(${offsetWidth}px, 0, 0)`
}
}
},
至此小球可以根据播放时间的滚动而滚动了
需求,当我们点击小球,或者拖动小球的时候,让进度条改变
监听touchstart、touchmove、touchend事件,阻止浏览器默认行为;
<div class="progress-bar-wrapper"
@touchstart.prevent="progressTouchStart"
@touchmove.prevent="progressTouchMove"
@touchend="progressTouchEnd"
>
创建一个 touch ,用来共享数据,当不需要在组件上面渲染。我们在create一开始的时候,创建touch空对象,用来挂载共享数据
created() {
this.touch = {}
},
touchstart,在手指触碰到屏幕的那一刻,记录下初始的值(pageX,距离屏幕左边的偏移)。
progressTouchStart(e){
this.touch.initiated=true;//表示已经开始初始化,标记当前以及开始触摸了
this.touch.x1=e.touches[0].pageX//获取到手指的距离屏幕左边的距离,当前拖动点X轴位置
this.touch.progressWidth=this.$refs.progress.clientWidth //获取到当前进度条的宽度
},
touchmove,当手指移动的时候,我们需要计算移动的距离,得到变化后的进度条的宽度
const progressBtnWidth = 16 //通过样式设置得到
progressTouchMove(e){
if(this.touch.initiated){//如果为true的话
//计算移动的距离
let moveX=e.touches[0].pageX-this.touch.x1;
//计算进度条的宽度
let progressWidth=this.touch.progressWidth+moveX;
//做边界处理,进度条的宽度,最小不能笑于0,最大不能大于progress-bar的宽度
//首先先获取到progress-bar的宽度
let barWidth=this.$refs.progressBar.clientWidth-progressBtnWidth;
//边界处理。
let offsetWidth=Math.min(Math.max(0,progressWidth),barWidth)
//把offsetWidth传入_offset方法里面做效果处理
this._offset(offsetWidth);
}
},
//改变小球的位置和进度条的宽度
_offset(offsetWidth){
this.$refs.progress.style.width=offsetWidth+'px';
this.$refs.progressBtn.style[transform]=`translate3d(${offsetWidth}px,0, 0)`
},
这时候,进度条可以移动了。但是,当前的播放时间并没有跟着变化
在touchend事件触发时计算当前进度条的占比,将百分比它派发给外部组件player,让它实时改变当前的播放时长,并且当手指松开的时候,将this.touch.initiated设置为false,方便下一次滑动进度条
progressTouchEnd(e){//当移动完成的时候
this.touch.initiated = false
this._triggerPercent()
},
//改变百分比
_triggerPercent(){
const barWidth=this.$refs.progressBar.clientWidth-progressBtnWidth;//进度条的总长度
const percent=this.$refs.progress.clientWidth/barWidth;//百分比=进度条总长度/进度条的长度
his.$emit('percentChange', percent)
}
在player\player.vue接收percent
<progressBar :percent="percent" @percentChange='percentChange'></progressBar>
注意是改变audio的currentTime。而不是this.currentTime
//当改变的时候,改变当前的播放的时间
percentChange(percent){
//当前的播放时长=总时长*百分比
this.$refs.audio.currentTime = this.currentSong.duration * percent
//如果当前不是播放状态的,让播放器播放
if(!this.playing){
this.togglePlaying()
}
}
这样进度条可以滑动了,并且也能正确显示时长了
当歌曲在播放的时候,我们希望进度条能够根据播放时长的变化而变化,这个时候我们可以在watch percent中实时做,进度条的宽度变化,和按钮的偏移
watch: {
percent(newPercent){
//当百分比大于等于0 并且 当前不是滑动的状态的时候
if(newPercent >= 0 && !this.touch.initiated){
//当前的进度条的宽度= 进度条背景的宽度-按钮的宽度
const barWidth =this.$refs.progressBar.clientWidth-progressBtnWidth;
//偏移的宽度
const offsetWidth=newPercent*barWidth;
this._offset(offsetWidth)
}
}
},
当点击进度条时,改变当前的播放的进度
//点击进度条改变进度
//思路。点击位置/进度条的总长度=百分比,改变进度条的宽度,emit一个percent
progressClick(e){
//进度条的总长度
const barWidth=this.$refs.progressBar.clientWidth-progressBtnWidth;
//progressBar距离屏幕左边的距离
let barLeft=this.$refs.progressBar.getBoundingClientRect().left;
//点击的位置,距离左边
let clickX=e.clientX-barLeft
//做边界处理
let progressWidth=Math.max(Math.min(clickX,barWidth),0)
//改变进度条的位置
this._offset(progressWidth);
//emit派发percent
this._triggerPercent(progressWidth)
},
7-11播放器progress-circle 圆形进度条组件实现
当播放器在播播放时,mini播放器的播放按钮的也呈现一个圆环的进度条
svg 的图片不会失真,区别于其他的图像格式,其他的图像格式是局域像素处理的,而svg本质是文本文体,属于对图像形状的描述,体积较小。
我们在基础组件创建圆环进度 src\components\base\progress-circle\progress-circle.vue
<div class="progress-circle">
<!-- viewBox 视口位置 与半径、宽高相关 stroke-dasharray 描边虚线 周长2πr stroke-dashoffset 描边偏移 未描边部分-->
<svg :width="radius" :height="radius" viewBox="0 0 100 100" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<circle class="progress-backgroud" r="50" cx="50" cy="50" fill="transparent"/>
<circle class="progress-bar" r="50" cx="50" cy="50" fill="transparent"
:stroke-dasharray="dashArray" :stroke-dashoffset="dashOffset"/>
</svg>
<slot></slot>
</div>
需要从外部组传入 radius 和 percent
src\components\base\progress-circle\progress-circle.vue
props:{
radius:{//圆的半径
type: Number,
default: 100
},
percent: {//百分比
type: Number,
default: 0
}
},
在播放暂停按钮外面用进度条包裹,并且传入radius 和percent
components\player\player.vue
<!-- 播放暂停歌曲 -->
<div class="control">
<!--加上圆环进度条 -->
<progressCircle :radius="radius" :percent="percent">
<i class="icon-mini icon-play-mini" @click.stop="togglePlaying" :class="playIcon"></i>
</progressCircle>
</div>
维护一个dashArray等于圆的周长,在计算属性中设置,dashOffset等于周长的剩余占比。得到当前的进度的偏移
base\progress-circle\progress-circle.vue
data() {
return {
dashArray: Math.PI * 100
}
},
computed: {
dashOffset() {
return (1 - this.percent) * this.dashArray
}
}
7-12 播放器模式切换功能实现
用计算属性计算mode的图标,切换的时候mutation改变切换的模式
<div class="icon i-left">
<i :class="modeIcon" @click="changeMode"></i>
</div>
//引入模式的配置文件
import {playMode} from '../../common/js/config'
modeIcon(){//计算模式的icon
return this.mode==playMode.sequence ? `icon-sequence` : this.mode==playMode.loop ? `icon-loop` : `icon-random`
}
...mapMutations({
setMode: 'SET_MODE'
}),
changeMode(){
let mode=(this.mode+1)%3 //只有三个求模
this.setMode(mode);
},
至此我们可以点击切换模式的按钮切换模式了,但是虽然图标的样式改变了,当前的播放列表还是没有改变的,我们需要在改变播放模式的同时也改变当前的播放列表,this.playlist
创建工具函数src\common\js\util.js
function getRandomInt(min, max){
return Math.floor(Math.random() * (max - min + 1) + min)
}
//洗牌: 遍历arr, 从0-i 之间随机取一个数j,使arr[i]与arr[j]互换
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
}
当点击切换模式,派发一个mutation去改变当前的播放列表(当是顺序播放的时候不要打乱播放列表,当是随机的时候才打乱播放列表)
//改变歌曲的模式
changeMode(){
const mode = (this.mode + 1) % 3
this.setMode(mode)
let list = null
if(mode === playMode.random){
list = shuffle(this.sequenceList)
}else{
list = this.sequenceList//sequenceList存的是顺序的列表,即初始时的播放列表
}
this.setPlayList(list)
},
我们已经可以通过切换模式去改变播放列表的顺序了,页面上也展示正常,但是当我们点击上一首下一首的时候。切换的歌曲和playlist的不一致。
findIndex找到当前歌曲id值index,通过mapMutations改变currentIndex,保证当前歌曲的id不变。当改变mode的时候,要改变当前的播放歌曲的索引
在changeMode中
//改变歌曲的模式
changeMode(){
//...
this.resetCurrentIndex(list)
this.setPlayList(list)
},
重置当前的索引,用findIndex
//重置当前的索引
resetCurrentIndex(list){
let index = list.findIndex((item) => { //es6语法 findIndex
return item.id === this.currentSong.id
})
this.setCurrentIndex(index)
},
当播放到歌曲结尾时,需要根据歌曲的模式来切换歌曲
当歌曲播放完毕时,audio派发 ended事件
<audio @ended="end"></audio>
//当歌曲播放完成的时候
end(){
//判断当前的播放模式
if(this.mode==playMode.loop){
this.loop()
}else{
this.next();
}
},
loop(){
//设置当前的currentTime
this.currentTime=0;
//设置自动播放
this.$refs.audio.play()
},
歌手详情页面,随机播放全部按钮点击实现
components\music-list\music-list.vue
<div class="play" @click="random">
<i class="icon-play"></i>
<span class="text">随机播放全部</span>
</div>
当点击随机播放按钮的时候,需要派发多个mutation所以我们可以在action里面定义一个setRandomPlay
store\actions.js
import {playMode} from 'common/js/config.js'
import {shuffle} from 'common/js/util.js'
export const setRandomPlay=function({commit},{list}){
//将当前的播放状态设置为播放
commit(types.SET_PLAYING,true)
//设置当前的索引为0.默认播放第一个
commit(types.SET_CURRENTINDEX,0)
//设置当前的播放的列表
commit(types.SET_PLAYLIST,shuffle(list.slice()))
//设置当前的顺序列表
commit(types.SET_SEQUENCELIST,list)
//设置当前的播放的模式
commit(types.SET_MODE,playMode.random)
//设置为全屏
commit(types.SET_FULLSCREEN, true)
}
...mapActions(['selectPlay','setRandomPlay'])
random(){
//因为这里需要改变多个状态
//改变当前的播放状态
//改变当前的currentIndex
//改变当前的playList
//所以在这里我们需要定义一个randomPlay 的mutation
this.setRandomPlay({list:this.songs});
},
7-13 播放器歌词数据抓取
src\api\song.js 中封装得到歌词的方法
import {commonParams} from './config'
import axios from 'axios'
//根据id去获取到歌词信息
export function getLyric(mid){
const url = '/api/lyric'
return new Promise((resolve,reject)=>{
let data=Object.assign({},commonParams,{
songmid: mid,
pcachetime: +new Date(),
platform: 'yqq',
hostUin: 0,
needNewCode: 0,
g_tk: 5381, //会变化,以实时数据为准
format: 'json' //规定为json请求
})
axios.get(url,{params: data}).then(res=>{
resolve(res.data)
}).catch(err=>{
reject(err)
})
})
}
config\index.js设置代理
//获取到歌词的信息
'/api/lyric':{
target:'https://szc.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg',
secure:false,
changeOrigin:true,
bypass:function(req,res,proxyOptions){
req.headers.referer='https://y.qq.com/'
req.headers.host='c.y.qq.com'
},
pathRewrite:{
'^/api/lyric': ''
}
}
将获取到歌词的方法封装到 class 类中 src\common\js\song.js
//获取到歌词的函数
getLyric(){
alert(this.mid);
getLyric(this.mid).then(res=>{
if(res.retcode ==ERR_OK){
this.lyric = res.lyric
}
})
}
在组件上获取到歌词 components\player\player.vue
this.$nextTick(()=>{
this.$refs.audio.play();
this.currentSong.getLyric() //获取的奥当前歌曲的歌词
})
7-14 播放器歌词数据解析
如图所示我们得到的歌词是base-64编码格式的,我们需要将歌词进行转换,在这里我们用到一个库
https://github.com/dankogai/js-base64
cnpm install --save js-base64
src\common\js\song.js
import { Base64 } from 'js-base64';
//获取到歌词的函数
getLyric(){
getLyric(this.mid).then(res=>{
if(res.retcode ==ERR_OK){
this.lyric = Base64.decode(res.lyric);
}
})
}
进行转码之后,我们得到的数据格式是这样的
对数据进行再一步解析成我们比较容易操作的数据
https://github.com/ustbhuangyi/lyric-parser 安装第三方库
cnpm install lyric-parser --save
src\common\js\song.js优化getLyric。如果已经有歌词就不要请求了
//获取到歌词的函数
getLyric(){
if(this.lyric){
Promise.resolve(this.lyric);
}else{
return new Promise((resolve,reject)=>{
getLyric(this.mid).then(res=>{
if(res.retcode ==ERR_OK){
this.lyric = Base64.decode(res.lyric);
resolve(this.lyric);
}else{
reject('error lyric');
}
})
})
}
}
src\components\player\player.vue
//引入歌词解析库
import Lyric from 'lyric-parser'
//监听currentSong的变化
currentSong(newSong, oldSong){ //确保DOM已存在
if(newSong.id === oldSong.id) {
return
}
this.$nextTick(()=>{
this.$refs.audio.play();
this.getLyric()
})
},
//获取到当前的歌词
getLyric(){
this.currentSong.getLyric().then(lyricStr=>{
//对当前的歌词进行解析
let lyric= new Lyric(lyricStr)
console.log(lyric);
})
}
等到的数据格式如下
7-15 播放器歌词滚动列表实现
在data中维护一个currentLyric。在watch currentSong 时得将得到的歌词传给currentLyric
this.currentSong.getLyric().then(lyricStr=>{
//对当前的歌词进行解析并且赋值给currentLyric
this.currentLyric= new Lyric(lyricStr)
})
歌词html结构
<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.txt}}
</p>
</div>
</div>
</div>
当前播放的歌曲高亮显示
在data中维护一个currentLineNum: 0,并且给歌词添加高亮效果
:class="{'current': currentLineNum === index}"
初始化lyric对象时传入handleLyric方法,得到当前currentLingNum值,判断如果歌曲播放,调用Lyric的play()
this.currentSong.getLyric().then(lyricStr=>{
//对当前的歌词进行解析
this.currentLyric= new Lyric(lyricStr,this.handleLyric)
//当歌曲播放的时候,调用Lyric实例 的 play()方法
if(this.playing){
this.currentLyric.play()
}
})
handleLyric({lineNum, txt}){
this.currentLineNum = lineNum
}
让歌词可以滚动,当前的播放的歌词居中展示
引入并且注册scroll组件
//引入scroll组件
import Scroll from 'components/base/scroll/scroll'
<Scroll class="middle-r" ref="lyricList" :data="currentLyric && currentLyric.lines">
当lineNum(当前播放的歌词索引)>5时,让scrol滚动到指定的位置减去5行,保证当前的歌词展示在页面的中间,否则滚动到顶部
handleLyric({lineNum, txt}){
this.currentLineNum = lineNum
if(lineNum>5){
let lineeEl = this.$refs.lyricLine[lineNum - 5] //保证歌词在中间位置滚动
//让滚动滚动到指定的位置减去5行
this.$refs.lyricList.scrollToElement(lineeEl)
}else{
//滚动回到顶部
this.$refs.lyricList.scrollTo(0,0,1000)
}
}
7-16 播放器歌词左右滑动实现
在data中刚维护一个 currentShow。默认值是 ‘cd’
currentShow:'cd'
<!-- dot部分 -->
<div class="dot-wrapper" >
<span class="dot" :class="{active:currentShow=='cd'}"></span>
<span class="dot" :class="{active:currentShow=='lyric'}"></span>
</div>
切换唱片和歌词
当当前是唱片展示 ,向右滑动的时候。当大于10%,歌词出现,唱片渐隐
当当前是歌词展示,向左滑动的时候,当滑动的距离大于10%的时候,歌词隐,唱片渐现
绑定touch事件:一定记得阻止浏览器默认事件
<div class="middle"
@touchstart.prevent="middleTouchStart"
@touchmove.prevent="middleTouchMove"
@touchend.prevent="middleTouchEnd"
>
created()中创建touch空对象:因为touch只存取数据,不需要添加gettter和setter监听
created() {
this.touch={}
},
把歌词的页面作为参考物,
当向右滑动的时候,歌词页面的偏移是-window.innerWidth 唱片页面的的opacity的值是0
当歌词页面没有展示时,页面偏移为0 唱片页面的的opacity的值是1
//切换歌词和唱片
middleTouchStart(e){
//是否开始滑动
this.touch.initiated=true//初始化标志
this.touch.x1=e.touches[0].pageX;//初始化位置
this.touch.y1=e.touches[0].pageY;//初始化的y的位置
},
middleTouchMove(e){
if(!this.touch.initiated){
return
}
let deltaX =e.touches[0].pageX- this.touch.x1;//x的横向的滑动的距离
let deltaY =e.touches[0].pageY- this.touch.y1;//x的横向的滑动的距离
//判断两个距离,如果是纵向滑动的时候,不做任何处理
if(Math.abs(deltaY)-Math.abs(deltaX) >0){
return
}else{
//参考位置 歌词的左边相对页面的偏移
let offsetLeft= this.currentShow==='cd' ? 0 : -window.innerWidth
//向左边滑动不能小于 -window.innerWidth 向右边滑动不能大于 window.innerWidth
//滑入歌词offsetWidth = 0 + deltaX(负值) 歌词滑出offsetWidth = -innerWidth + delta(正值)
const offsetWidth = Math.min(0, Math.max(-window.innerWidth,offsetLeft+deltaX))
this.touch.percent=Math.abs(offsetWidth/window.innerWidth);
console.log(offsetWidth, this.touch.percent);
//记住这里 this.$refs.lyricList得到的是一个component组件,当操作这个组件的元素需要用this.$refs.lyricList.$el
this.$refs.lyricList.$el.style[transform]=`translate3d(${offsetWidth}px,0,0)`
//唱片部分渐隐
this.$refs.middleL.style['opacity']=1-this.touch.percent
this.$refs.middleL.style[transitionDuration] = 0
this.$refs.lyricList.$el.style[transitionDuration] = 0
}
},
middleTouchEnd(){
this.touch.initiated=false//初始化标志
let offsetWidth;
let opacity;
//当当前的是cd唱片
if(this.currentShow === 'cd'){
if(this.touch.percent>0.1){
offsetWidth=-window.innerWidth;
opacity=0
this.currentShow = 'lyric'
}else{
offsetWidth = 0
opacity = 1
}
//当当前的是歌词
}else if(this.currentShow === 'lyric'){
if(this.touch.percent<0.9){
offsetWidth=0;
opacity=1
this.currentShow = 'cd'
}else{
offsetWidth=-window.innerWidth;
opacity=0
}
}
this.$refs.middleL.style['opacity']=opacity
this.$refs.lyricList.$el.style[transform]=`translate3d(${offsetWidth}px,0,0)`
this.$refs.middleL.style[transitionDuration] = 1000
this.$refs.lyricList.$el.style[transitionDuration] =1000
},
现在我们为页面的唱片部分添加上歌词
<div class="playing-lyric-wrapper">
<div class="playing-lyric">{{playingLyric}}</div>
</div>
在data中维护一个playingLyric,在handleLyric中得到当前的播放的歌词
handleLyric({lineNum, txt}){
//...
this.playingLyric=txt
}
},
7-17 播放器歌词剩余功能实现
bug1:当切换歌曲时,歌词出现闪动,因为每次都会重新实例化Layric,但前一首的Layric中的定时器还在,造成干扰
解决:在监听currentSong 的变化时,清空其中的定时器
if(this.currentLyric){
this.currentLyric.stop() //切换歌曲后,清空前一首歌歌词Layric实例中的定时器
}
bug2:当歌曲暂停的时候,歌词还在跳动的处理
解决,currentLyric实例的togglePlaying()方法
//播放和暂停,
togglePlaying(){
if(!this.songReady){
return
}
//解决当歌曲暂停的时候,歌词还在跳动
if(this.currentLyric){
this.currentLyric.togglePlay()//歌词切换播放暂停
}
//发起一个playing 的 mutation
this.setPlaying(!this.playing)
},
bug3:当歌曲的模式是,单曲循环的时候,当唱完一首歌曲,歌词并没有回到当前歌曲的开头
解决,当是单曲循环时,当播放完完整的一首歌时,判断是否有currentLyric实例,如果有,让歌词偏移到到刚开始的位置
loop(){
//设置当前的currentTime
this.currentTime=0;
//设置自动播放下一首歌
this.$refs.audio.play()
//解决当是单曲循环的时候,播放完成之后歌曲并没有回到开头
if(this.currentLyric){
this.currentLyric.seek(0) //歌词偏移到一开始
}
},
bug4:当滑动进度条时,页面的歌词并没有跟着变化
解决,在监听到滚动条的percent变化时,判断当前是否有currentLyric 实例,当有的时候,让歌词偏移到当前的播放时间的位置
//当改变的时候,改变当前的播放的时间
percentChange(percent){
//当前的播放时长=总时长*百分比
this.$refs.audio.currentTime = this.currentSong.duration * percent
//如果当前不是播放状态的,让播放器播放
if(!this.playing){
this.togglePlaying()
}
if(this.currentLyric){
//让歌曲进行偏移
let currentTime=this.currentSong.duration * percent
this.currentLyric.seek(currentTime * 1000)
}
},
优化1:当获取歌曲异常的时候,做处理
this.currentSong.getLyric().then(lyricStr=>{
//对当前的歌词进行解析
this.currentLyric= new Lyric(lyricStr,this.handleLyric)
//当歌曲播放的时候,调用Lyric实例 的 play()方法
if(this.playing){
this.currentLyric.play()
}
}).catch(err=>{//当请求失败时处理
this.currentLyric = null
this.playingLyric = ''
this.currentLineNum = 0
})
优化2:
当只有一首歌曲的时候,当点击下一首或者上一首时,让歌曲循环播放
在prev() 和next方法中都加上
if(this.playlist.length==1){
this.loop()
return
}
优化3:因为手机微信运行时从后台切换到前台时不执行js,要保证歌曲重新播放,使用
setTimeOut去代替nextTick
setTimeout(()=>{
this.$refs.audio.play();
this.getLyric()
},1000)
7-18播放器底部播放器适配+mixin的应用
mixin就是采取一定规则将一个功能(组件)的属性混合到另一个组件或者全局当中,优点就是灵活度高,耦合度低,便于维护
src\common\js\mixin.js
import {mapGetters} from 'vuex'
export const playlistMixin={
computed: {
...mapGetters([`playlist`])
},
mounted() {
this.handlePlayList(this.playlist)
},
activated() {//keep-alive 组件切换的时候会触发activated
this.handlePlayList(this.playlist)
},
watch: {
playlist(newval){
this.handlePlayList(newval)
}
},
methods: {//组件中定义handlePlaylist,就会覆盖这个,否则没有定义的,就会抛出异常
handlePlayList(){
throw new Error("component must implement handlePlaylist method")
}
},
}
歌曲详情页music-list适配
music-list\music-list.vue
//引入mixin
import {playlistMixin} from "common/js/mixin";
export default {
//注册minxins
mixins:[playlistMixin],
//...
}
在methods中定义适配函数
//mixins部分
handlePlayList(playlist){
let bottom= playlist.length ? `60px` : `0px`;
this.$refs.list.$el.style['bottom']=`${bottom}`//底部播放器适配
this.$refs.list.refresh();//强制刷新滚动条
}
歌手页面singer.vue适配
components\base\listview\listview.vue
派发一个refresh
refresh(){
this.$refs.listview.refresh()
},
singer\singer.vue
引入并且注册mixisn,定义handlePlayList函数
//mixins部分
handlePlayList(playlist){
let bottom=playlist.length ? `60`: `0`
this.$refs.list.style['bottom']=`${bottom}px`;
this.$refs.scroll.refresh();//重刷新
},
推荐页面
引入并且注册mixisn,定义handlePlayList函数
handlePlayList(playlist){
let bottom=this.playlist.length ? `60` :`0`
this.$refs.recommend.style['bottom']=`${bottom}px`
this.$refs.scroll.refresh();
},
项目中用到的audio事件总结:
事件名称 | 触发时机 |
play | 播放 |
pause | 暂停 |
canplay | 请求到歌曲的时候 |
error | 歌曲没法请求到 |
timeupdate | 当前的播放时间(可读写) |
ended | 当歌播放完成的时候 |