最近抽空将播放器的界面做了一些更新,如图所示:

一共分两路更新:

  1. CD 界面重构
  2. 增加倍速播放,歌词解析插件升级

# CD 界面重构

进入到 normal-player/index.js 中,将 CDWrapper 中的内容换成如下所示的代码:

// 可旋转 needle
<div className={`needle ${playing? '' : 'pause'}`}></div>
<div className="cd">
  <img
    className={`image play ${playing? '' : 'pause'}`}
    src={song.al.picUrl + "?param=400x400"}
    alt=""
  />
</div>
<p className="playing_lyric">{currentPlayingLyric}</p>

在 style.js 中:

import disc from './disc.png';
import needle from './needle.png';

export const CDWrapper = styled.div`
  margin: auto;
  position: absolute;
  width: 100%;
  top: 0;
  bottom: 0;
  display: flex;
  justify-content: center;
  box-sizing: border-box;
  .needle {
    position: absolute;
    top: -6.67vw;
    left: 48vw;
    width: 25vw;
    height: 40vw;
    z-index: 100;
    background-image: url (${needle});
    ${style.bgFull ()};
    transform-origin: 4.5vw 4.5vw;
    transition: all 0.3s;
    transform: rotate (0);
    &.pause {
      transform: rotate (-30deg);
    }
  }
  .cd {
    top: 16%;
    position: absolute;
    width: 70%;
    height: 70vw;
    background-image: url (${disc});
    border: 4px solid ${style ["border-color-v2"]};
    border-radius: 50%;
    ${style.bgFull ()};
    .image {
      position: absolute;
      left: 0;right: 0;
      top: 0;bottom: 0;
      width: 68%;
      height: 68%;
      margin: auto;
      border-radius: 50%;
    }
    .play {
      animation: ${rotate} 20s linear infinite;
      &.pause {
        animation-play-state: paused;
      }
    }
  }
  .playing_lyric {
    position: absolute;
    margin: auto;
    width: 80%;
    top: 95vw;
    font-size: 14px;
    line-height: 20px;
    white-space: normal;
    text-align: center;
    color: rgba (255, 255, 255, 0.5);
  }
`;

needle 和 disc 图片大家可以进入这个链接获取: 点击获取 (opens new window)

另外,global-style.js 中的内容也有所更新:

const bgFull = () => {
  return `
    background-position: 50%;
    background-size: contain;
    background-repeat: no-repeat;
  `
};

export default {
  //...
  "border-color-v2": "rgba (228, 228, 228, 0.1)",
  bgFull
};

另一部分是 Top 部分的更新。

// JSX
<Top className="top">
  <div className="back" onClick={() => toggleFullScreenDispatch (false)}>
    <i className="iconfont icon-back">&#xe662;</i>
  </div>
  <div className="text">
    <h1 className="title">{song.name}</h1>
    <h1 className="subtitle">{getName (song.ar)}</h1>
  </div>
</Top>

//style.js
export const Top = styled.div`
  box-sizing: border-box;
  position: absolute;
  display: flex;
  align-items: center;
  margin-bottom: 15px;
  border-bottom: 1px solid ${style ["border-color-v2"]};
  padding-bottom: 5px;
  width: 100%;
  height: 8%;
  .back {
    margin-left: 5px;
    z-index: 50;
    .iconfont {
      display: block;
      padding: 9px;
      font-size: 24px;
      color: ${style ["font-color-desc"]};
      font-weight: bold;
      transform: rotate (90deg);
    }
  }
  .text {
    flex: 1;
    display: flex;
    flex-direction: column;
    margin-top: 10px;
  }
  .title {
    line-height: 25px;
    font-size: ${style ["font-size-l"]};
    color: ${style ["font-color-desc"]};
    ${style.noWrap ()};
  }
  .subtitle {
    line-height: 20px;
    font-size: ${style ["font-size-m"]};
    color: ${style ["font-color-desc-v2"]};
    ${style.noWrap ()};
  }
`;

现在,就能看到文章开始的那个效果啦!

# 倍速播放功能添加

首先需要作好数据层的准备。因为目前来说我们希望歌曲播放的速度是一个全局性的变量,即使更换了歌曲依然按照速度不会变。因此特意把它放到了 redux 中存储。

//constants.js
export const CHANGE_SPEED = 'player/CHANGE_SPEED';

//reducer.js
const defaultState = fromJS ({
  //...
  speed: 1
});
export default (state = defaultState, action) => {
  switch (action.type) {
    //...
    case actionTypes.CHANGE_SPEED:
      return state.set ('speed', action.data);
    default:
      return state;
  }
}

//actionCreators.js
export const changeSpeed = (data) => ({
  type: CHANGE_SPEED,
  data
});

还有对于播放速度的配置数据:

//api/config.js
// 倍速播放配置
export const list = [
  {
    key: 0.75,
    name: "x0.75"
  },
  {
    key: 1,
    name:"x1"
  },
  {
    key: 1.25,
    name:"x1.25"
  },
  {
    key: 1.5,
    name:"x1.5"
  },
  {
    key: 2,
    name:"x2"
  }
]

OK, 现在我们来对接 Player 组件。

import { changeSpeed } from './store/actionCreators';
import { list } from "../../../api/config";

// 组件内
const { speed } = props;
const { changeSpeedDispatch } = props;
// 映射 Redux 全局的 state 到组件的 props 上
const mapStateToProps = state => ({
  //...
  speed: state.getIn (["player", "speed"]),
});

// 映射 dispatch 到 props 上
const mapDispatchToProps = dispatch => {
  return {
    //...
    changeSpeedDispatch (data) {
      dispatch (changeSpeed (data));
    }
  };
}

在 normalPlayer 中的 Bottom 部分我们往首部加入:

<List>
  <span > 倍速听歌 </span>
  {
    list.map ((item) => {
      return (
        <ListItem
          key={item.key}
          className={`${speed === item.key ? 'selected': ''}`} >
            {item.name}
        </ListItem>
      )
    })
  }
</List>

其中 List, ListItem 在 style.js 中导出:

export const List = styled.div`
  width: 70%;
  margin: auto;
  display: flex;
  align-items: center;
  height: 30px;
  justify-content: space-around;
  overflow: hidden;
  >span:first-of-type {
    display: block;
    flex: 0 0 auto;
    padding: 5px 0;
    color: ${style ["font-color-desc-v2"]};
    font-size: ${style ["font-size-m"]};
    vertical-align: middle;
  }
`
export const ListItem = styled.span`
  flex: 0 0 auto;
  font-size: ${style ["font-size-m"]};
  padding: 5px 5px;
  border-radius: 10px;
  color: ${style ["font-color-desc-v2"]};
  &.selected {
    color: ${style ["theme-color"]};
    border: 1px solid ${style ["theme-color"]};
    opacity: 0.8;
  }
`

然后引入:

import { List, ListItem } from './style';

现在我们来给每一个 ListItem 绑定点击事件。

const { clickSpeed } = props;
//JSX
<ListItem
  //...
  onClick={() => clickSpeed (item.key)}>
    {item.name}
</ListItem>

这个处理逻辑由父组件传递,我们在父组件来编写具体的逻辑。

useEffect (() => {
  //...
  audioRef.current.src = getSongUrl (current.id);
  audioRef.current.autoplay = true;
  // 这里加上对播放速度的控制
  audioRef.current.playbackRate = speed;
  //...
}, [currentIndex, playList]);
const clickSpeed = (newSpeed) => {
  changeSpeedDispatch (newSpeed);
  //playbackRate 为歌词播放的速度,可修改
  audioRef.current.playbackRate = newSpeed;
  // 别忘了同步歌词
  currentLyric.current.changeSpeed (newSpeed);
  currentLyric.current.seek (currentTime*1000);
}

好,现在歌曲可以正常播放了。但是同时还有一个非常严重的问题,那就是歌词不能倍速播放,也就是歌曲和歌词不同步!

看似是一个难以解决的问题,但是我们只需要稍稍对歌词插件做一些扩展即可:

//api/lyric-parser.js
export default class Lyric {
  constructor (lrc, handler, speed) {
    //...
    this.speed = speed || 1;

    this._init ();
  }
  changeSpeed (speed) {
    this.speed = speed;
  }
}

然后是一个最关键的修改:

_playRest (isSeek=false) {
  //...
  this.timer = setTimeout (() => {
    this._callHandler (this.curLineIndex++)
    if (this.curLineIndex < this.lines.length && this.state === STATE_PLAYING) {
      this._playRest ()
    }
    // 注意定时器的时间
  }, (delay /this.speed))
}

当速度变为 x2 的时候,其实离下一句歌词到来的时间间隔变为了原来的 1 / 2。依此类推。

这样歌词能够正常倍速播放了。

现在的歌词插件可以说是一个相对完整的插件了,我们也可以将它发布到 npm 上作为第三方包供其他开发者使用。做法也非常简单:

  1. 在 www.npmjs.com 网站上注册一个用户
  2. 通过 npm init 创建一个仓库
  3. 通过 npm adduser 登录你的 npm 账户
  4. 使用 npm publish 发布你的代码。(上传后第三方包的名字就是 package.json 中的 name 值)

结果如图所示:

img

阅读全文