播放器内核的动画
2021-03-31 本文已影响0人
web_jianshu
image.png
先安装
cnpm install create-keyframe-animation -S
template部分
<template>
<!-- 当前播放列表(只有顺序一种排序)有数据时 播放器才显示 -->
<div class="player" v-show="playlist.length > 0">
<transition
name="normal"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
@after-leave="afterLeave"
>
<!-- 展开的播放器内核 fullScreen控制播放器内核展开和收起 -->
<div class="normal-player" v-show="fullScreen">
<!-- 播放器内核的模糊背景图 -->
<div class="background">
<img width="100%" height="100%" :src="currentSong.image" />
</div>
<div class="top" @click="back">
<!-- 播放器内核左上角返回按钮 -->
<div class="back">
<i class="icon-back"></i>
</div>
<!-- 播放器内核顶部歌曲名称 -->
<h1 class="title" v-html="currentSong.name"></h1>
<!-- 播放器内核顶部歌手名称 -->
<h2 class="subtitle" v-html="currentSong.singer"></h2>
</div>
<div
class="middle"
@touchstart.prevent="middleTouchStart"
@touchmove.prevent="middleTouchMove"
@touchend="middleTouchEnd"
>
<div class="middle-l" ref="middleL">
<!-- 播放器内核中间唱片图片 -->
<div
class="cd-wrapper"
ref="cdWrapper"
:data="currentLyric && currentLyric.lines"
>
<div class="cd" :class="cdCls">
<img class="image" :src="currentSong.image" />
</div>
</div>
<div class="playing-lyric-wrapper">
<div class="playing-lyric">{{ playingLyric }}</div>
</div>
</div>
<!-- 播放器内核中间滚动歌词 -->
<scroll class="middle-r" ref="lyricList">
<div class="lyric-wrapper">
<div v-if="currentLyric">
<p
ref="lyricLine"
class="text"
:class="{ current: currentLineNum === index }"
v-for="(line, index) in currentLyric.lines"
:key="index"
>
{{ line.txt }}
</p>
</div>
</div>
</scroll>
</div>
<!-- 播放器内核底部操作区 -->
<div class="bottom">
<!-- 播放器内核底部两个小圆圈 -->
<div class="dot-wrapper">
<span class="dot" :class="{ active: currentShow === 'cd' }"></span>
<span
class="dot"
:class="{ active: currentShow === 'lyric' }"
></span>
</div>
<!-- 播放器内核底部进度条区域 -->
<div class="progress-wrapper">
<!-- 播放器内核底部进度条左边歌曲正在播放进度时间 -->
<span class="time time-l">{{ format(currentTime) }}</span>
<!-- 播放器内核底部进度条 -->
<div class="progress-bar-wrapper">
<progress-bar
:percent="percent"
@percentChange="onProgressBarChange"
></progress-bar>
</div>
<!-- 播放器内核底部进度条右边歌曲总时间 -->
<span class="time time-r">{{ format(currentSong.duration) }}</span>
</div>
<!-- 播放器内核底部播放模式、上一曲、暂停播放、下一曲、喜欢区域 -->
<div class="operators">
<!-- 播放模式 -->
<div class="icon i-left" @click="changeMode">
<i :class="iconMode"></i>
</div>
<!-- 上一曲 -->
<div class="icon i-left" :class="disableCls">
<i @click="prev" class="icon-prev"></i>
</div>
<!-- 暂停播放 -->
<div class="icon i-center" :class="disableCls">
<i @click="togglePlaying" :class="playIcon"></i>
</div>
<!-- 下一曲 -->
<div class="icon i-right" :class="disableCls">
<i @click="next" class="icon-next"></i>
</div>
<!-- 喜欢 -->
<div class="icon i-right">
<i
class="icon"
@click="toggleFavorite(currentSong)"
:class="getFavoriteIcon(currentSong)"
></i>
</div>
</div>
</div>
</div>
<!-- // 展开的播放器内核 -->
</transition>
<transition name="mini">
<!-- 播放器内核收起后固定在底部的播放器 fullScreen为false时打开 -->
<div class="mini-player" v-show="!fullScreen" @click="open">
<!-- 播放器内核收起后固定在底部的播放器左边转动的图片 -->
<div class="icon">
<img :class="cdCls" 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">
<progress-circle :radius="radius" :percent="percent">
<i
@click.stop="togglePlaying"
class="icon-mini"
:class="miniIcon"
></i>
</progress-circle>
</div>
<!-- 播放器内核收起后固定在底部的播放器右边歌曲列表按钮 -->
<div class="control" @click.stop="showPlaylist">
<i class="icon-playlist"></i>
</div>
</div>
<!-- // 播放器内核收起后固定在底部的播放器 -->
</transition>
<playlist ref="playlist"></playlist>
<!-- 歌曲从加载到播放 播放时派发事件canplay 歌曲发生错误请求不到地址时 派发事件error 歌曲播放时派发事件timeupdate -->
<audio
ref="audio"
:src="currentSong.url"
@play="ready"
@error="error"
@timeupdate="updateTime"
@ended="end"
></audio>
</div>
</template>
js部分
import { mapGetters, mapMutations, mapActions } from "vuex";
// 使用js方式编写css3动画
import animations from "create-keyframe-animation";
import { prefixStyle } from "common/js/dom";
const transform = prefixStyle("transform");
const transitionDuration = prefixStyle('transitionDuration');
export default {
methods: {
back() {
// 注意: 修改state中一个数据时 通过调用一个mutations中方法
// 批量修改state中多个数据时 通过actions中封装多个mutations 调用多个mutations方法 达到修改多个state中数据
// 调用mutations字段里方法 点击返回关闭播放器全屏
this.setFullScreen(false);
},
open() {
// 点击播放器内核收起后的底部播放器mini-player打开播放器全屏
this.setFullScreen(true);
},
enter(el, done) {
// el是发生动画的元素 done执行是执行下一个钩子函数
const { x, y, scale } = this._getPosAndScale();
// 定义动画 为一个对象 前半场动画:从播放器内核图从隐藏为小图过渡到显示大图过程
let animation = {
0: {
// 播放器内核默认是隐藏的
// 这里是给播放器内核大图设置向左和向下偏移到相当于播放器内核收起后底部播放器左边小图 并且压缩大小
// 0表示是开始点击后打开播放器内核 先瞬间把内核大图隐藏成小图放在左下角位置 再过渡到内核大图原始位置
// 记忆技巧: 在前半场动画中 这里的x和y和scale表示该dom **已经事先提前 走的、缩放过了的值** 是已经发生了的事情
transform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`
},
60: {
// 60 内核大图已经到了正确位置之后 先放大1.1倍
transform: `translate3d(0, 0, 0) scale(1.1)`
},
100: {
// 60-100 内核大图一直处于正确位置 之后再缩小为1倍
transform: `translate3d(0, 0, 0) scale(1)`
}
};
// 注册动画
animations.registerAnimation({
name: "move", // 动画名称
animation,
presets: {
// 预设 设置动画间隔
// duration: 400,
duration: 1500,
easing: "linear"
}
});
// 运行动画 给播放器内核图dom绑定动画
animations.runAnimation(this.$refs.cdWrapper, "move", done); // 执行动画 动画执行完后调用done
// 动画执行完后 done执行的时候跳到下一个钩子函数 afterEnter
},
afterEnter() {
// 动画完成之后 清除注册的动画
animations.unregisterAnimation("move");
this.$refs.cdWrapper.style.animation = "";
},
// 后半场动画 播放器内核 收起 大图缩小为左下角小图并隐藏时(隐藏的是播放器整个播放器内核 左下角看到的小图是原本就有该标签) 触发
leave(el, done) {
console.log(1111);
this.$refs.cdWrapper.style.transition = "all 0.4s";
// this.$refs.cdWrapper.style.transition = "all 4s";
const { x, y, scale } = this._getPosAndScale();
// 播放器内核图(.player .normal-player .middle .middle-l .cd-wrapper)绑定动画
// 后半场动画: 播放器内核从大图过渡到播放器内核收起时底部播放器左边小图
// 记忆技巧: 在后半场动画中 这里的x和y和scale表示该dom**要去 走的、要缩放的值** 是还没发生的事情
this.$refs.cdWrapper.style[
transform
] = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
this.$refs.cdWrapper.addEventListener("transitionend", done);
// transitionend执行完后 done执行的时候跳到下一个钩子函数 afterLeave
},
afterLeave() {
// 动画完成之后 清除注册的动画
this.$refs.cdWrapper.style.transition = "";
this.$refs.cdWrapper.style[transform] = "";
},
// 获取播放器内核图片中心的初始位置坐标到播放器内核图收起后底部播放器左边旋转图中心初始位置坐标的距离
// 和缩放尺寸
// js方式操作css3
_getPosAndScale() {
// mini播放器cd图片
const targetWidth = 40; // 播放器内核图收起后底部播放器左边旋转图宽度
const paddingLeft = 40; // 播放器内核图收起后底部播放器左边旋转图中心x坐标距离左边界的内边距
const paddingBottom = 30; // 播放器内核图收起后底部播放器左边旋转图中心Y坐标距离下边界的内边距
// 中间的大cd图片
const paddingTop = 80; // 播放器内核图(.player .normal-player .middle{top: 80px})到顶部的边距
const width = window.innerWidth * 0.8; // 播放器内核图的宽度(.player .normal-player .middle .middle-l .cd-wrapper{ width: 80%; })
// 播放器内核图收起后底部播放器左边旋转图初始化缩放比例
const scale = targetWidth / width;
// 播放器内核图中心的X坐标 到 播放器内核收起后底部播放器左边旋转图中心的X坐标的距离 从大图到小图 所以X取负数
const x = -(window.innerWidth / 2 - paddingLeft);
// 播放器内核图中心的Y坐标 到 播放器内核收起后底部播放器左边旋转图中心的Y坐标的距离 从大图到小图 所以Y取正数
const y = window.innerHeight - paddingTop - width / 2 - paddingBottom;
return {
x,
y,
scale
};
},
}
scss部分
<style scoped lang="stylus" rel="stylesheet/stylus">
@import '~common/stylus/variable';
@import '~common/stylus/mixin';
.player {
.normal-player {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 150;
background: $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;
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: 0px 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-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 {
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;
no-wrap();
font-size: $font-size-medium;
color: $color-text;
}
.desc {
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>
common/js/dom.js部分
export function addClass(el, className) {
if (hasClass(el, className)) {
return;
}
let newClass = el.className.split(' ');
newClass.push(className);
el.className = newClass.join(' ');
};
export function hasClass(el, className) {
let reg = new RegExp('(^|\\s)' + className + '(\\s|$)');
return reg.test(el.className);
};
export function getData(el, name, val) {
const prefix = 'data-';
name = prefix + name;
if (val) {
return el.setAttribute(name, val);
} else {
return el.getAttribute(name);
}
};
// 根据浏览器支持情况给style添加前缀
let elementStyle = document.createElement('div').style;
let vendor = (() => {
let transformNames = {
webkit: 'webkitTransform',
Moz: 'MozTransform',
O: 'OTransform',
ms: 'msTransform',
standard: 'transform'
}
for (let key in transformNames) {
if (elementStyle[transformNames[key]] !== undefined) {
return key;
}
}
return false;
})();
export function prefixStyle(style) {
if (vendor === false) {
return false;
}
if (vendor === 'standard') {
return style;
}
return vendor + style.charAt(0).toUpperCase() + style.substr(1);
}