超长篇幅,这一章让人想要放弃,但是坚持下来更加巩固自己的知识了,谢谢我的老师对我的鼓励。

包括播放器 Vuex 数据设计和相关应用、播放器基础样式及歌曲数据应用播放器展开收起动画的实现、播放器前进后退功能实现、播放器播放时间获取和更新、progress-bar 进度条组件开发、progress-circle 圆形进度条组件开发、播放器模式切换功能实现、播放器歌词数据抓取和解析、播放器歌词左右滑动的实现、播放器底部播…

7-1 播放器页面设计详解

ios 如何获取APP启动后的基地址_数据


ios 如何获取APP启动后的基地址_ios 如何获取APP启动后的基地址_02


播放器可以通过歌手详情列表、歌单详情列表、排行榜、搜索结果多种组件打开,因此播放器数据一定是全局的。因为在每个页面都可以调起播放器,所以播放器并不是一个路由组件,而是一个公共的组件。每个页面都可能会用到,我们把它放在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个文件

  1. 首先先定义state,我们先想清楚我们的原始的数据是什么,这些数据最好都是组件或者模块中的一些最底层的数据
  2. 接着是getters,getters的数据其实是对state数据的一层映射,在getters写的都是一些函数,这类似我们写的计算属性,它可以根据state中不用的值来计算一个新的值,比如currentSong。在getters里也可以写一些复杂的逻辑
  3. 数据有了,那么怎么修改这些数据呢,我们在mutations写对这些数据修改的逻辑,那么在写mutation之前我们先要写mutation-type文件的常量。 mutation-types文件,定义的常量通常是一个动词,它供actions和mutations使用
  4. 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啦


ios 如何获取APP启动后的基地址_ios 如何获取APP启动后的基地址_03



ios 如何获取APP启动后的基地址_ico_04


用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本质是文本文体,属于对图像形状的描述,体积较小。

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);
        }
    })
}

进行转码之后,我们得到的数据格式是这样的

ios 如何获取APP启动后的基地址_播放列表_05


对数据进行再一步解析成我们比较容易操作的数据

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);
    })    
}

等到的数据格式如下

ios 如何获取APP启动后的基地址_vue_06

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

当歌播放完成的时候

github chapter7