今天就写一下歌曲播放这个功能,进度条拉伸以及歌曲时间的变化,当我们改变一个状态的时候,其他几个都相应改变,这个功能还是有一点复杂的…
就是这个,当我们可以播放,暂停,当我们播放的时候,进度条改变以及歌曲时间改变,当我们拉动进度条的时候,歌曲时间暂停,当我们松开的时候,直接跳到这个地方歌词的位置…
话不多说了,开整
一.在我们pages文件夹下,建立一个player文件夹,里面创建一个app-player-bar和一个store文件夹
二.那肯定是封装我们的网络请求了
在services文件夹下,创建一个player.js
import request from './request';
export function getSongDetail(ids) {
return request({
url: "/song/detail",
params: {
ids
}
})
}
三.我们需要在我们创建的player文件夹下的store里面去,把数据存到我们的redux里面,这里说一下,我是把所有网络请求的数据都放在我们的redux里面进行管理,而且每一个页面我都分别创建一个子redux,最后再把它们合并到主redux里面,通过redux-thunk
来到我们的store
首先是constants.js,定义常量啊
export const CHANGE_CURRENT_SONG = "player/CHANGE_CURRENT_SONG";
再是actionCreators.js,写方法
import { getSongDetail } from '@/services/player';
//引入我们player中封装的网络请求
import * as actionTypes from './constants';
const changeCurrentSongAction = (currentSong) => ({
type: actionTypes.CHANGE_CURRENT_SONG,
currentSong
})
export const getSongDetailAction = (ids) => {
return dispatch => {
getSongDetail(ids).then(res => {
dispatch(changeCurrentSongAction(res.songs[0]));
})
}
}
再来到我们的reducer.js
import { Map } from 'immutable';
//提升性能,深拷贝,不清楚的可以百度一下
import * as actionTypes from './constants';
const defaultState = Map({
currentSong: {}
});
function reducer(state = defaultState, action) {
switch(action.type) {
case actionTypes.CHANGE_CURRENT_SONG:
return state.set("currentSong", action.currentSong);
default:
return state;
}
}
export default reducer;
之后就是index.js了,导出reducer
import reducer from './reducer';
export {
reducer
}
四.数据存到redux之后,我们需要到我们的主redux文件里面进行合并
reducer.js
// combineReducers:合成reducer,因为可能有多个reducer
// 类似:immutable,深拷贝
import { combineReducers } from 'redux-immutable';
//怕命名冲突,as是重命名
import { reducer as recommendReducer } from '../pages/discover/c-pages/recommend/store';
import { reducer as playerReducer } from '../pages/player/store';
const cReducer = combineReducers({
recommend: recommendReducer, //这是我们之前demo中定义的,之前的博客
player: playerReducer //这是我们播放的子redux
});
export default cReducer;
五.OK啦,数据全部弄好了,接下来可以去组件里面了
来到我们app-player-bar文件夹下,创建一个index.js,一个style.js
index.js
老规矩,还是分三步走
第一步,导入我们的配置
import React, { memo, useState, useEffect, useRef, useCallback } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { getSizeImage, formatDate, getPlaySong } from '@/utils/format-utils';
import { Slider } from 'antd';
import { getSongDetailAction } from '../store/actionCreators';
import {
PlaybarWrapper,
Control,
PlayInfo,
Operator
} from './style';
getSizeImage是我们改变图片大小的,formatDate是格式化时间戳的,getPlaySong是我们要获取音乐的mp3,网易云搞的坏事…Slider就是我们那个进度条,在antdui库里面获取的
format-utils.js
export function getSizeImage(imgUrl, size) {
return `${imgUrl}?param=${size}x${size}`;
}
export function formatDate(time, fmt) {
let date = new Date(time);
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
}
let o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
};
for (let k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
let str = o[k] + '';
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
}
}
return fmt;
};
function padLeftZero(str) {
return ('00' + str).substr(str.length);
};
export function formatMonthDay(time) {
return formatDate(time, "MM月dd日");
}
export function formatMinuteSecond(time) {
return formatDate(time, "mm:ss");
}
export function getPlaySong(id) {
return `https://music.163.com/song/media/outer/url?id=${id}.mp3`;
}
第二步,我们的逻辑代码,这个有点复杂,我们也分几步好吧
useState
const [currentTime, setCurrentTime] = useState(0);
const [progress, setProgress] = useState(0);
const [isChanging, setIsChanging] = useState(false);
const [isPlaying, setIsPlaying] = useState(false);
解析
currentTime:我们歌曲播放的时间
progress:进度条的位置
isChanging: 因为我们滑动进度条的时候和我们歌曲播放的过程中,都会改变我们进度条的位置,这样它们两个就冲突了,啥意思呢,就是当我们把进度条滑到50,但是歌曲刚刚播放,这个时候进度条它就会马上回到我们播放的位置,两者冲突,所以必须有个判断
isPlaying:就是我们播放暂停,来回切换
useDispatch, useSelector, shallowEqual
const { currentSong } = useSelector(state => ({
currentSong: state.getIn(["player", "currentSong"])
}), shallowEqual);
const dispatch = useDispatch();
解析
useSelector:获取我们redux中的state
shallowEqual:优化性能,有一个浅层比较
useDispatch:获取我们的dispatch
useRef,audioRef
const audioRef = useRef();
useEffect(() => {
dispatch(getSongDetailAction(167876));
}, [dispatch]);
useEffect(() => {
audioRef.current.src = getPlaySong(currentSong.id);
}, [currentSong]);
解析
useRef:这是给我们audio标签绑定的一个ref
<audio ref={audioRef}/>
useEffect:发送网络请求,获取我们redux里面的数据,通过dispatch,下面那个获取歌曲的src地址
处理一些我们的数据,比如转换时间戳啊,地址等等
const picUrl = (currentSong.al && currentSong.al.picUrl) || "";
const singerName = (currentSong.ar && currentSong.ar[0].name) || "未知歌手";
const duration = currentSong.dt || 0;
const showDuration = formatDate(duration, "mm:ss");
const showCurrentTime = formatDate(currentTime, "mm:ss");
再来看下面的逻辑
1.歌曲的暂停与播放
const playMusic = () => {
isPlaying ? audioRef.current.pause(): audioRef.current.play();
setIsPlaying(!isPlaying);
}
2.当我们歌曲播放的时候,进度条和我们歌曲时间的改变
const timeUpdate = (e) => {
if (!isChanging) {
setCurrentTime(e.target.currentTime * 1000);
setProgress(currentTime / duration * 100);
}
}
3.当我们拉动进度条的时候,当 Slider 的值发生改变时,把改变后的值作为参数传入
const sliderChange = useCallback((value) => {
setIsChanging(true);
const currentTime = value / 100 * duration;
setCurrentTime(currentTime);
setProgress(value);
}, [duration]);
至于为什么用useCallback,useCallback主要用在,当把一个回调函数传到一个自定义组件内部时候用
4.把当前值作为参数传入,这个地方要注意我上面说的那种情况,播放时候和我们拉动进度条的时候冲突了,进度条的位置,不过我已经解决了在这
const sliderAfterChange = useCallback((value) => {
const currentTime = value / 100 * duration / 1000;
audioRef.current.currentTime = currentTime;
setCurrentTime(currentTime * 1000);
setIsChanging(false);
if (!isPlaying) {
playMusic();
}
}, [duration,isPlaying, playMusic]);
好了,逻辑代码完毕
第三步,接下来是页面布局
return (
<PlaybarWrapper className="sprite_player">
<div className="content wrap-v2">
<Control isPlaying={isPlaying}>
<button className="sprite_player prev"></button>
<button className="sprite_player play" onClick={e => playMusic()}></button>
<button className="sprite_player next"></button>
</Control>
<PlayInfo>
<div className="image">
<a href="/#">
<img src={getSizeImage(picUrl, 35)} alt="" />
</a>
</div>
<div className="info">
<div className="song">
<span className="song-name">{currentSong.name}</span>
<a href="#/" className="singer-name">{singerName}</a>
</div>
<div className="progress">
<Slider defaultValue={30}
value={progress}
onChange={sliderChange}
onAfterChange={sliderAfterChange}/>
<div className="time">
<span className="now-time">{showCurrentTime}</span>
<span className="divider">/</span>
<span className="duration">{showDuration}</span>
</div>
</div>
</div>
</PlayInfo>
<Operator>
<div className="left">
<button className="sprite_player btn favor"></button>
<button className="sprite_player btn share"></button>
</div>
<div className="right sprite_player">
<button className="sprite_player btn volume"></button>
<button className="sprite_player btn loop"></button>
<button className="sprite_player btn playlist"></button>
</div>
</Operator>
</div>
<audio ref={audioRef} onTimeUpdate={timeUpdate}/>
</PlaybarWrapper>
)
这是这个页面所有代码
import React, { memo, useState, useEffect, useRef, useCallback } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { getSizeImage, formatDate, getPlaySong } from '@/utils/format-utils';
import { Slider } from 'antd';
import { getSongDetailAction } from '../store/actionCreators';
import {
PlaybarWrapper,
Control,
PlayInfo,
Operator
} from './style';
export default memo(function HYAppPlayerBar() {
// props and state
const [currentTime, setCurrentTime] = useState(0);
const [progress, setProgress] = useState(0);
const [isChanging, setIsChanging] = useState(false);
const [isPlaying, setIsPlaying] = useState(false);
// redux hook
const { currentSong } = useSelector(state => ({
currentSong: state.getIn(["player", "currentSong"])
}), shallowEqual);
const dispatch = useDispatch();
// other hooks
const audioRef = useRef();
useEffect(() => {
dispatch(getSongDetailAction(167876));
}, [dispatch]);
useEffect(() => {
audioRef.current.src = getPlaySong(currentSong.id);
}, [currentSong]);
// other handle
const picUrl = (currentSong.al && currentSong.al.picUrl) || "";
const singerName = (currentSong.ar && currentSong.ar[0].name) || "未知歌手";
const duration = currentSong.dt || 0;
const showDuration = formatDate(duration, "mm:ss");
const showCurrentTime = formatDate(currentTime, "mm:ss");
// handle function
const playMusic = () => {
isPlaying ? audioRef.current.pause(): audioRef.current.play();
setIsPlaying(!isPlaying);
}
const timeUpdate = (e) => {
if (!isChanging) {
setCurrentTime(e.target.currentTime * 1000);
setProgress(currentTime / duration * 100);
}
}
//useCallback:当把一个回调函数传到一个自定义组件内部时候用
const sliderChange = useCallback((value) => {
setIsChanging(true);
const currentTime = value / 100 * duration;
setCurrentTime(currentTime);
setProgress(value);
}, [duration]);
const sliderAfterChange = useCallback((value) => {
const currentTime = value / 100 * duration / 1000;
audioRef.current.currentTime = currentTime;
setCurrentTime(currentTime * 1000);
setIsChanging(false);
if (!isPlaying) {
playMusic();
}
}, [duration,isPlaying, playMusic]);
return (
<PlaybarWrapper className="sprite_player">
<div className="content wrap-v2">
<Control isPlaying={isPlaying}>
<button className="sprite_player prev"></button>
<button className="sprite_player play" onClick={e => playMusic()}></button>
<button className="sprite_player next"></button>
</Control>
<PlayInfo>
<div className="image">
<a href="/#">
<img src={getSizeImage(picUrl, 35)} alt="" />
</a>
</div>
<div className="info">
<div className="song">
<span className="song-name">{currentSong.name}</span>
<a href="#/" className="singer-name">{singerName}</a>
</div>
<div className="progress">
<Slider defaultValue={30}
value={progress}
onChange={sliderChange}
onAfterChange={sliderAfterChange}/>
<div className="time">
<span className="now-time">{showCurrentTime}</span>
<span className="divider">/</span>
<span className="duration">{showDuration}</span>
</div>
</div>
</div>
</PlayInfo>
<Operator>
<div className="left">
<button className="sprite_player btn favor"></button>
<button className="sprite_player btn share"></button>
</div>
<div className="right sprite_player">
<button className="sprite_player btn volume"></button>
<button className="sprite_player btn loop"></button>
<button className="sprite_player btn playlist"></button>
</div>
</Operator>
</div>
<audio ref={audioRef} onTimeUpdate={timeUpdate}/>
</PlaybarWrapper>
)
})
style.js
import styled from 'styled-components';
export const PlaybarWrapper = styled.div`
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 52px;
background-position: 0 0;
background-repeat: repeat;
.content {
display: flex;
align-items: center;
justify-content: space-between;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0;
height: 47px;
}
`
export const Control = styled.div`
display: flex;
align-items: center;
.prev, .next {
width: 28px;
height: 28px;
}
.prev {
background-position: 0 -130px;
}
.play {
width: 36px;
height: 36px;
margin: 0 8px;
background-position: 0 ${props => props.isPlaying ? "-165px": "-204px"};
}
.next {
background-position: -80px -130px;
}
`
export const PlayInfo = styled.div`
display: flex;
width: 642px;
align-items: center;
.image {
width: 34px;
height: 34px;
border-radius: 5px;
}
.info {
flex: 1;
color: #a1a1a1;
margin-left: 10px;
.song {
color: #e1e1e1;
position: relative;
top: 8px;
left: 8px;
.singer-name {
color: #a1a1a1;
margin-left: 10px;
}
}
.progress {
display: flex;
align-items: center;
.ant-slider {
width: 493px;
margin-right: 10px;
.ant-slider-rail {
height: 9px;
background: url(${require("@/assets/img/progress_bar.png")}) right 0;
}
.ant-slider-track {
height: 9px;
background: url(${require("@/assets/img/progress_bar.png")}) left -66px;
}
.ant-slider-handle {
width: 22px;
height: 24px;
border: none;
margin-top: -7px;
background: url(${require("@/assets/img/sprite_icon.png")}) 0 -250px;
}
}
.time {
.now-time {
color: #e1e1e1;
}
.divider {
margin: 0 3px;
}
}
}
}
`
export const Operator = styled.div`
display: flex;
position: relative;
top: 5px;
.btn {
width: 25px;
height: 25px;
cursor: pointer;
}
.favor {
background-position: -88px -163px;
}
.share {
background-position: -114px -163px;
}
.right {
width: 126px;
padding-left: 13px;
background-position: -147px -248px;
.volume {
background-position: -2px -248px;
}
.loop {
background-position: -3px -344px;
}
.playlist {
width: 59px;
background-position: -42px -68px;
}
}
`
好啦,最后一步,到我们的app.js中,将其引入
import React, { memo } from 'react'
//共享redux
import { Provider } from 'react-redux';
// 可以将路由划分到一个文件里面
import {renderRoutes} from 'react-router-config'
import { HashRouter } from 'react-router-dom';
import routes from './router'
import store from './store';
import LSHAppHeader from '@/components/app-header'
import LSHAppFooter from '@/components/app-footer'
import HYAppPlayerBar from './pages/player/app-player-bar';
export default memo(function App() {
return (
<Provider store={store}>
<HashRouter>
<LSHAppHeader/>
{renderRoutes(routes)}
<LSHAppFooter/>
<HYAppPlayerBar/>
</HashRouter>
</Provider>
)
})
注意:有些是之前的demo
好啦,完成哈哈
如果有不清楚的话,可以去我的github,有源码,下一篇博客,就更新我们歌词显示这部分内容了,歌词滚动哦,激动吧