播放器基础样式
player.vue
<template> <div class="player" v-show="playlist.length>0"> <div class="normal-player" v-show="fullScreen"> <div class="background"> <img width="100%" height="100%" :src="currentSong.image"> </div> <div class="top"> <div class="back" @click="back"> <i class="iconfont icon-back"></i> </div> <h1 class="title" v-html="currentSong.name"></h1> <h2 class="subtitle" v-html="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> </div> <div class="bottom"> <div class="operators"> <div class="icon i-left"> <i class="iconfont icon-sequence"></i> </div> <div class="icon i-left"> <i class="iconfont icon-prev"></i> </div> <div class="icon i-center"> <i class="iconfont icon-play"></i> </div> <div class="icon i-right"> <i class="iconfont icon-next"></i> </div> <div class="icon i-right"> <i class="iconfont icon-not-favorite"></i> </div> </div> </div> </div> <div class="mini-player" v-show="!fullScreen"> <div class="icon"> <img width="40" height="40" :src="currentSong.image"> </div> <div class="text"> <h2 class="name" v-html="currentSong.name"></h2> <p class="desc" v-html="currentSong.singer"></p> </div> <div class="control"> <i class="iconfont icon-playlist"></i> </div> </div> </div> </template> <script> import { mapGetters, mapMutations } from ‘vuex‘; export default { name: ‘player‘, computed: { ...mapGetters([ ‘fullScreen‘, ‘playlist‘, ‘currentSong‘, ]), }, }; </script> <style lang="scss" scoped> .player { .normal-player { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 150; background-color: $color-background; .background { position: absolute; left: 0; top: 0; width: 100%; height: 100%; z-index: -1; opacity: 0.6; filter: blur(20px); } .top { position: relative; margin-bottom: 25px; .back { position: absolute; top: 0; left: 6px; z-index: 50; .icon-back { display: block; padding: 9px; font-size: $font-size-large-x; color: $color-theme; transform: rotate(-90deg); } } .title { width: 70%; margin: 0 auto; line-height: 40px; text-align: center; @include no-wrap(); font-size: $font-size-large; color: $color-text; } .subtitle { line-height: 20px; text-align: center; font-size: $font-size-medium; color: $color-text; } } .middle { position: fixed; width: 100%; top: 80px; bottom: 170px; // 规定段落中的文本不进行换行 white-space: nowrap; font-size: 0; .middle-l { display: inline-block; vertical-align: top; position: relative; width: 100%; height: 0; padding-top: 80%; .cd-wrapper { position: absolute; left: 10%; top: 0; width: 80%; height: 100%; .cd { width: 100%; height: 100%; box-sizing: border-box; border: 10px solid rgba(255, 255, 255, 0.1); border-radius: 50%; &.play { animation: rotate 20s linear infinite; } &.pause { animation-play-state: paused; } .image { position: absolute; left: 0; top: 0; width: 100%; height: 100%; border-radius: 50%; } } } .playing-lyric-wrapper { width: 80%; margin: 30px auto 0 auto; overflow: hidden; text-align: center; .playing-lyric { height: 20px; line-height: 20px; font-size: $font-size-medium; color: $color-text-l; } } } .middle-r { display: inline-block; vertical-align: top; width: 100%; height: 100%; overflow: hidden; .lyric-wrapper { width: 80%; margin: 0 auto; overflow: hidden; text-align: center; .text { line-height: 32px; color: $color-text-l; font-size: $font-size-medium; &.current { color: $color-text; } } } } } .bottom { position: absolute; bottom: 50px; width: 100%; .dot-wrapper { text-align: center; font-size: 0; .dot { display: inline-block; vertical-align: middle; margin: 0 4px; width: 8px; height: 8px; border-radius: 50%; background: $color-text-l; &.active { width: 20px; border-radius: 5px; background: $color-text-ll; } } } .progress-wrapper { display: flex; align-items: center; width: 80%; margin: 0 auto; padding: 10px 0; .time { color: $color-text; font-size: $font-size-small; flex: 0 0 30px; line-height: 30px; width: 30px; &.time-l { text-align: left; } &.time-r { text-align: right; } } .progress-bar-wrapper { flex: 1; } } .operators { display: flex; align-items: center; .icon { flex: 1; color: $color-theme; &.disable { color: $color-theme-d; } i { font-size: 30px; } } .i-left { text-align: right; } .i-center { padding: 0 20px; text-align: center; i { font-size: 40px; } } .i-right { text-align: left; } .icon-favorite { color: $color-sub-theme; } } } &.normal-enter-active, &.normal-leave-ative { 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-player { display: flex; align-items: center; position: fixed; left: 0; bottom: 0; z-index: 180; width: 100%; height: 60px; background: $color-highlight-background; &.mini-enter-active, &.mini-leave-active { transition: all 0.4s; } &.mini-enter, &.mini-leave-to { opacity: 0; } .icon { flex: 0 0 40px; width: 40px; padding: 0 10px 0 20px; img { border-radius: 50%; &.play { animation: rotate 10s linear infinite; } &.pause { animation-play-state: paused; } } } .text { display: flex; flex-direction: column; justify-content: center; flex: 1; line-height: 20px; overflow: hidden; .name { margin-bottom: 2px; @include no-wrap(); font-size: $font-size-medium; color: $color-text; } .desc { @include no-wrap(); font-size: $font-size-small; color: $color-text-d; } } .control { flex: 0 0 30px; width: 30px; padding: 0 10px; .icon-play-mini, .icon-pause-mini, .icon-playlist { font-size: 30px; color: $color-theme-d; } .icon-mini { font-size: 32px; position: absolute; left: 0; top: 0; } } } } @keyframes rotate { 0% { transform: rotate(0); } 100% { transform: rotate(360deg); } } </style>
播放器展开收起效果
点击歌曲时,通过mapGetters获取到currentSong数据填入到DOM中;同时在收起展开图标添加点击事件。点击切换播放器实现展开收起效果就需要修改fullScreen,可以通过vuex提供的mapMutations映射修改fullScreen。
//返回图标(即收起) <div class="back" @click="back"> <i class="iconfont icon-back"></i> </div> //展开图标 <div class="mini-player" v-show="!fullScreen" @click="open"> <script> import { mapGetters, mapMutations } from ‘vuex‘; export default { name: ‘player‘, computed: { ...mapGetters([ ‘fullScreen‘, ‘playlist‘, ‘currentSong‘, ]), }, methods: { back() { // 不能直接写this.fullScreen = false; 会弹出警告 // 有了映射之后可以调用 this.setFullScreen(false); }, open() { this.setFullScreen(true); }, ...mapMutations({ setFullScreen: ‘SET_FULL_SCREEN‘, }), }, }; </script>
播放器展开收起动画
1.展开收起的过程如果没有动画看起来比较生硬,可以实现动画:背景图片是渐隐渐现动画,展开时头部标题从顶部下落,底部操作区从底部往上,收起时相反。
- 在enter-active和leave-active定义它的transition属性,定义一个缓动函数使用贝塞尔曲线实现回弹效果;
&.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-player设置动画
&.mini-enter-active, &.mini-leave-active { transition: all 0.4s; } &.mini-enter, &.mini-leave-to { opacity: 0; }
2.展开收起播放器的时候,还可以添加这样一个动画效果:mini-player的专辑图片往返到normal-player对应的位置上,在这个过程中还会有个放大缩小的效果。可以利用vuex提供的javascript钩子,在相关的钩子中定义CSS3动画。
- 首先给normal添加几个事件
<transition name="normal" @enter="enter" @after-enter="afterEnter" @leave="leave" @after-leave="afterLeave">
- 在methods中定义这几个钩子函数,在这几个钩子函数里写CSS3动画代码
(JS怎么创建CSS动画呢,需要用到第三方库create-keyframe-animation 安装:cnpm install create-keyframe-animation@0.1.0 --save )
import animations from ‘create-keyframe-animation‘; enter(el, done) { }, afterEnter() { }, leave(el, done) { }, afterLeave() { },
- 在设置动画时,需要先获取mini-player专辑图片的位置以及mini-player专辑图片中心点到normal-player专辑图片中心点的偏移,在methods中封装函数getPosAndScale获取初始位置及缩放尺寸
getPosAndScale() { const targetWidth = 40; // mini-player专辑图片中心点偏移x 40像素 const paddingLeft = 40; // mini-player专辑图片中心点离底部有30像素 const paddingBottom = 30; // 大专辑图片离顶部有80像素 const paddingTop = 80; // 大专辑图片宽度是窗口的80% const width = window.innerWidth * 0.8; // 初始的缩放比例 const scale = targetWidth / width; // 初始的x坐标 const x = -(window.innerWidth / 2 - paddingLeft); const y = window.innerHeight - paddingTop - width / 2 - paddingBottom; return { x, y, scale, }; },
- 给cd-wrapper添加引用,在几个钩子函数中创建CSS3动画
import { prefixStyle } from ‘../../common/js/dom‘;
const transform = prefixStyle(‘transform‘);
enter(el, done) { const { x, y, scale } = this.getPosAndScale(); 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)‘, }, }; animations.registerAnimation({ name: ‘move‘, animation, // 预设 可以设置动画的间隔以及缓动 presets: { duration: 400, easing: ‘linear‘, }, }); animations.runAnimation(this.$refs.cdWrapper, ‘move‘, done); }, afterEnter() { animations.unregisterAnimation(‘move‘); this.$refs.cdWrapper.style.animation = ‘‘; }, leave(el, done) { this.$refs.cdWrapper.style.transition = ‘all 0.4s‘; const { x, y, scale } = this.getPosAndScale(); this.$refs.cdWrapper.style[transform] = `translate3d(${x}px,${y}px,0) scale(${scale})`; this.$refs.cdWrapper.addEventListener(‘transitionend‘, done); }, afterLeave() { this.$refs.cdWrapper.style.transition = ‘‘; this.$refs.cdWrapper.style.transform = ‘‘; },