VUE移动端音乐APP学习【九】:播放器基础样式及展开收起

播放器基础样式

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">&#xe614;</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">&#xe633;</i>
          </div>
          <div class="icon i-left">
            <i class="iconfont icon-prev">&#xea86;</i>
          </div>
          <div class="icon i-center">
            <i class="iconfont icon-play">&#xe60d;</i>
          </div>
          <div class="icon i-right">
            <i class="iconfont icon-next">&#xea8b;</i>
          </div>
          <div class="icon i-right">
            <i class="iconfont icon-not-favorite">&#xe613;</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">&#xe7fe;</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>
View Code

播放器展开收起效果

点击歌曲时,通过mapGetters获取到currentSong数据填入到DOM中;同时在收起展开图标添加点击事件。点击切换播放器实现展开收起效果就需要修改fullScreen,可以通过vuex提供的mapMutations映射修改fullScreen。

//返回图标(即收起)
<div class="back" @click="back">
    <i class="iconfont icon-back">&#xe614;</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);
        }
      }

  •  在enter和leave-to进入和移出设置动画效果
&.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 = ''; },

 

posted @ 2021-04-08 23:27  小风车吱呀转  阅读(12)  评论(0编辑  收藏