The content is long, please read patiently

This section uses Redux to manage song states for core playback

What is a Redux? Redux is a state container, an application data flow framework, mainly used for application state management. It uses a single constant state tree (object) to manage and store the state of the entire application, which cannot be modified directly. For the Chinese version of Redux, see www.redux.org.cn

Redux is not coupled to any framework, and Redux is used in React to provide the React-Redux library

Use Redux to manage song-related state properties

In our application, there are many song list pages. If you click the song in the list page, the clicked song will be played. At the same time, the play all button in the list page will be added to the playlist after clicking. The songs that the song player component wants to play, the song list, and a display play page property are managed using Redux. To achieve background playback, place the song player component outside the routing component within the App component, that is, the outermost component of each list page component. After the App component is mounted, the player component will remain in the entire App and will not be destroyed (except for exiting the application).

Install redux and React-Redux first

npm install redux react-redux --save
Copy the code

The React-Redux library contains the idea of separating container components from presentation components, using Redux at the top level, and the rest of the internal components for presentation only. All data is passed in through props

instructions Container components Display components
location At the top level, routing Intermediate and child components
Read the data Get state from Redux Get data from props
Modify the data Send actions to Redux Call the callback function from props

The state of design

In this case, create a new redux directory under SRC, then create actions. Js, actiontypes.js, reducers. Js and store.js

actionTypes.js

Export const SHOW_PLAYER = "SHOW_PLAYER"; Export const CHANGE_SONG = "CHANGE_SONG"; Export const REMOVE_SONG_FROM_LIST = "REMOVE_SONG"; // export const SET_SONGS = "SET_SONGS";Copy the code

Actiontypes.js holds action constants to be performed

actions.js

Import * as ActionTypes from "./ ActionTypes "/** * Action is the payload for transferring data from the app to the store. It is the only source of store data */ // the Action creation function used to create Action objects. Export function showPlayer(showStatus) {return {type:ActionTypes.SHOW_PLAYER, showStatus}; } export function changeSong(song) { return {type:ActionTypes.CHANGE_SONG, song}; } export function removeSong(id) { return {type:ActionTypes.REMOVE_SONG_FROM_LIST, id}; } export function setSongs(songs) { return {type:ActionTypes.SET_SONGS, songs}; }Copy the code

Actions.js holds the object to be acted on and must have a type attribute indicating the action to be performed. It is best to define modules as applications grow in size

reducers.js

Import {combineReducers} from 'redux' import * as ActionTypes from "./ ActionTypes "/** * reducer is a pure function Const initialState = {showStatus: false, // Show the status [] // List of songs}; Function showStatus(showStatus = initialstate. showStatus, action) { switch (action.type) { case ActionTypes.SHOW_PLAYER: return action.showStatus; default: return showStatus; Function song(song = initialstate. song, action) {switch (action.type) {case actiontypes.change_song: return action.song; default: return song; Function songs(songs = initialstate.songs, action) {switch (action.type) {case actiontypes.set_songs: return action.songs; case ActionTypes.REMOVE_SONG_FROM_LIST: return songs.filter(song => song.id ! == action.id); default: return songs; }} / / merge Reducer const Reducer = combineReducers ({showStatus, song, songs}); export default reducerCopy the code

Reducers.js stores pure functions for updating the current playing song, playing song list, and showing or hiding the playing page state. Be sure to keep the Reducer function pure and never do the following

  1. Modify incoming parameters
  2. Perform operations that have side effects, such as API requests and route jumps
  3. Call impure functions such as date.now () or math.random ()

store.js

Import {createStore} from "redux" import reducer from "./reducers" const store = createStore(reducer); export default storeCopy the code

Add Redux to your App to connect to redux. React-redux provides a Provider component and a connect method. Provider is used to pass stores, connect is used to connect components to Redux, and any component wrapped from Connect () can get a Dispatch method as props for the component, as well as anything needed in global state

Create a root.js file in the Components directory to wrap the App component and pass the store

Root.js

import React from "react" import {Provider} from "react-redux" import store from ".. /redux/store" import App from "./App" class Root extends React.Component { render() { return ( <Provider store={store}> <App/> </Provider> ); } } export default RootCopy the code

The Provider receives a Store object

Modify index.js to replace App component with Root component

//import App from './components/App';
import Root from './components/Root';
//ReactDOM.render(<App />, document.getElementById('root'));
ReactDOM.render(<Root />, document.getElementById('root'));
Copy the code

Operating state

Connect the Album components developed in the previous section to Redux using the Connect method. In order to distinguish the container components from the UI components, we put the container components that need to connect to Redux into a separate directory and only need to introduce the UI components. Create a containers directory under SRC, and then create the UI component Album for album.js

Album.js

import {connect} from "react-redux" import {showPlayer, changeSong, setSongs} from ".. /redux/actions" import Album from ".. // dispatchtoprops = (dispatchtoprops) => ({showMusicPlayer: (status) => { dispatch(showPlayer(status)); }, changeCurrentSong: (song) => { dispatch(changeSong(song)); }, setSongs: (songs) => { dispatch(setSongs(songs)); }}); export default connect(null, mapDispatchToProps)(Album)Copy the code

The first parameter of connect is used to map store to the component props, the second parameter is used to map Dispatch to the props, and the Album component is passed in. There is no need to get the store state and null is passed in

Go back to the albu.js component under Components and add the song list click event

/ / selectSong(song) {return (e) => {this.props. SetSongs ([song]); this.props.changeCurrentSong(song); }; }Copy the code
let songs = this.state.songs.map((song) => {
    return (
        <div className="song" key={song.id} onClick={this.selectSong(song)}>
            ...
        </div>
    );
});
Copy the code

SetSongs and changeCurrentSong in the appeal code are both mapped to component props via mapDispatchToProps

Change the import album.js to the album.js under containers in recommend.js

//import Album from ".. /album/Album" import Album from "@/containers/Album"Copy the code

To test if you can change the state, create a play directory under Components and then create player.js to get the state information

import React from "react"

class Player extends React.Component {
    render() {
        console.log(this.props.currentSong);
        console.log(this.props.playSongs);
    }
}

export default Player
Copy the code

Create the corresponding container component player.js under the Containers directory

import {connect} from "react-redux" import {showPlayer, changeSong} from ".. /redux/actions" import Player from ".. // const mapStateToProps = (state) => ({showStatus: const mapStateToProps => (const mapStateToProps = (state) => ({showStatus: state.showStatus, currentSong: state.song, playSongs: state.songs }); // mapDispatchToProps const mapDispatchToProps = (dispatch) => ({showMusicPlayer: (status) => { dispatch(showPlayer(status)); }, changeCurrentSong: (song) => { dispatch(changeSong(song)); }}); Export default Connect (mapStateToProps, mapDispatchToProps)(Player)Copy the code

The mapStateToProps function maps the state of the Store to the props of the component. The Player component subscribes to the Store and calls the Render method to trigger the update when the state of the store changes

Introduce the container component Player in app.js

import Player from ".. /containers/Player"Copy the code

Put it in the following position

<Router>
  <div className="app">
    ...
     <div className="music-view">
        ...
    </div>
    <Player/>
  </div>
</Router>
Copy the code

After starting the application, open the console to view the statusClick on the song on the album page to check its status

Encapsulating the Progress component

In this project, the progress of song playing will be displayed in two places, one is the playing component and the other is the Mini playing component. We extracted the functions of the progress bar and passed props according to the business needs

Create progress.js and progress.styl in the Play directory under Components

Progress.js

import React from "react"

import "./progress.styl"

class Progress extends React.Component {
    componentDidUpdate() {

    }
    componentDidMount() {

    }
    render() {
        return (
            <div className="progress-bar">
                <div className="progress" style={{width:"20%"}}></div>
                <div className="progress-button" style={{left:"70px"}}></div>
            </div>
        );
    }
}

export default Progress
Copy the code

Progress.styl check out the source code, with the source address at the end

The Progress component receives Progress, disableButton, disableButton, onDragStart, Properties such as drag callback function (onDrag) and drag accept callback function (onDragEnd)

Next, add Progress and drag to the Progress component

Prop-types Is used to type check the props passed and import prop-types

import PropTypes from "prop-types"
Copy the code
Progress.propTypes = {
    progress: PropTypes.number.isRequired,
    disableButton: PropTypes.bool,
    disableDrag: PropTypes.bool,
    onDragStart: PropTypes.func,
    onDrag: PropTypes.func,
    onDragEnd: PropTypes.func
};
Copy the code

Note: Prop-types have been installed in the previous sections

I’m going to add ref to the element

<div className="progress-bar" ref="progressBar">
    <div className="progress" style={{width:"20%"}} ref="progress"></div>
    <div className="progress-button" style={{left:"70px"}} ref="progressBtn"></div>
</div>
Copy the code

Import the React – DOM and get the total length of the DOM and progress bar in componentDidMount

let progressBarDOM = ReactDOM.findDOMNode(this.refs.progressBar);
let progressDOM = ReactDOM.findDOMNode(this.refs.progress);
let progressBtnDOM = ReactDOM.findDOMNode(this.refs.progressBtn);
this.progressBarWidth = progressBarDOM.offsetWidth;
Copy the code

Get props in the Render method, replacing the value in the dead style. The code is modified as follows

// Progress value: 0-1 let {progress, disableButton} = this.props; if (! progress) progress = 0; // let progressButtonOffsetLeft = 0; if(this.progressBarWidth){ progressButtonOffsetLeft = progress * this.progressBarWidth; } return ( <div className="progress-bar" ref="progressBar"> <div className="progress-load"></div> <div className="progress" style={{width:`${progress * 100}%`}} ref="progress"></div> { disableButton === true ? "" : <div className="progress-button" style={{left:progressButtonOffsetLeft}} ref="progressBtn"></div> } </div> );Copy the code

Progress is used to control the current progress value in the appeal code, and progressButtonOffsetLeft is used to control the position of the button from the start of the progress bar. Renders an empty string when disableButton is true or the button element when disableButton is not true

The drag-and-drop function is implemented using touchStart, TouchMove and TouchEnd on the mobile terminal. Add the following code to componentDidMount

let {disableButton, disableDrag, onDragStart, onDrag, onDragEnd} = this.props; if (disableButton ! == true && disableDrag ! == true) {// Let downX = 0; // let buttonLeft = 0; progressBtnDOM.addEventListener("touchstart", (e) => { let touch = e.touches[0]; downX = touch.clientX; buttonLeft = parseInt(touch.target.style.left, 10); if (onDragStart) { onDragStart(); }}); progressBtnDOM.addEventListener("touchmove", (e) => { e.preventDefault(); let touch = e.touches[0]; let diffX = touch.clientX - downX; let btnLeft = buttonLeft + diffX; if (btnLeft > progressBarDOM.offsetWidth) { btnLeft = progressBarDOM.offsetWidth; } else if (btnLeft < 0) { btnLeft = 0; } // set button left value touch.target.style.left = btnLeft + "px"; // Set progressdom.style. width = btnLeft/this.progressBarWidth * 100 + "%"; if (onDrag) { onDrag(btnLeft / this.progressBarWidth); }}); progressBtnDOM.addEventListener("touchend", (e) => { if (onDragEnd) { onDragEnd(); }}); }Copy the code

Determine whether buttons and drag-and-drop are enabled, and then add touchStart, TouchMove, and TouchEnd events to progressBtnDOM. The drag starts by recording the position where the touch starts downX and the left value of the button buttonLeft. The drag calculates the diffx of the drag distance and then resets the left value of the button to btnLeft. BtnLeft is the distance from the left of the progress bar after drag, divided by the total progress length is the current progress ratio. This value multiplied by 100 is the width of the progressDOM. Call the event preventDefault function in drag to prevent the default behavior that some browsers have when touch moves the window forward or backward. The corresponding callback event is called at the end of each event, passing in the current progress value in the onDrag callback function

Finally, add the following code in componentDidUpdate to solve the problem that the total progress can not be correctly obtained after component update

// Get the progress bar width if (! this.progressBarWidth) { this.progressBarWidth = ReactDOM.findDOMNode(this.refs.progressBar).offsetWidth; }Copy the code

Developing playback components

The playback function is mainly implemented using the AUDIO element of H5, combined with canplay, timeUpdate, ended and error events. The CanPlay event is raised when the audio is ready to play. The timeUpdate event is triggered during playback, which can obtain the current playback event and total duration of the song, and use these two to update the playback status of the component. After the playing is complete, a Ended event is triggered, in which songs are switched according to the playing mode

The songs

Create a player.styl style file in the Play directory under Components. Player.js was created when testing the status on it. The Player component has the functions of switching song playing mode, previous song, next song, play, pause and so on. We’re going to put currentTime, playProgress, playStatus and currentPlayMode into the state manager of the player component, and we’re going to put currentSong, The current location of the song currentIndex is given to itself

Player.js

import React from "react" import ReactDOM from "react-dom" import {Song} from "@/model/song" import "./player.styl" class Player extends React.Component { constructor(props) { super(props); this.currentSong = new Song( 0, "", "", "", 0, "", ""); this.currentIndex = 0; // Playback modes: list-list-single - shuffle- random this. PlayModes = ["list", "single", "shuffle"]; this.state = { currentTime: 0, playProgress: 0, playStatus: false, currentPlayMode: 0 } } componentDidMount() { this.audioDOM = ReactDOM.findDOMNode(this.refs.audio); this.singerImgDOM = ReactDOM.findDOMNode(this.refs.singerImg); this.playerDOM = ReactDOM.findDOMNode(this.refs.player); this.playerBgDOM = ReactDOM.findDOMNode(this.refs.playerBg); } render() { let song = this.currentSong; let playBg = song.img ? song.img : require("@/assets/imgs/play_bg.jpg"); // Let playButtonClass = this.state.playStatus === true? "icon-pause" : "icon-play"; song.playStatus = this.state.playStatus; return ( <div className="player-container"> <div className="player" ref="player"> ... <div className="singer-middle"> <div className="singer-img" ref="singerImg"> <img src={playBg} alt={song.name} onLoad={ (e) = > {/ * load set after the completion of background images, prevent images load too slowly to have no background * / enclosing playerBgDOM. The style.css. BackgroundImage url = ` ` (" ${playBg} "); } }/> </div> </div> <div className="singer-bottom"> ... </div> <div className="player-bg" ref="playerBg"></div> <audio ref="audio"></audio> </div> </div> ); } } export default PlayerCopy the code

The above omit part of the code, the complete code is viewed in the source code

The player.styl code is omitted

Import the Progress component and pass in playProgress in the following location

import Progress from "./Progress"
Copy the code
<div className="play-progress">
    <Progress progress={this.state.playProgress}/>
</div>
Copy the code

Add the following code at the beginning of the Render method, while adding the Canplay and timeUpdate events to the Audio element at componentDidMount. Determine whether the current song has been switched. If the song has been switched, set a new SRC, and then play the audio that triggers the canplay event. Update progress status and current playback time while playing

/ / gets the current play songs from redux if (this. Props. CurrentSong && enclosing props. CurrentSong. Url) {/ / current song hair change if (this. CurrentSong. Id! == this.props.currentSong.id) { this.currentSong = this.props.currentSong; this.audioDOM.src = this.currentSong.url; // To load the resource, ios calls this.audiodom.load (); }}Copy the code
this.audioDOM.addEventListener("canplay", () => {
    this.audioDOM.play();
    this.startImgRotate();

    this.setState({
        playStatus: true
    });

}, false);

this.audioDOM.addEventListener("timeupdate", () => {
	if (this.state.playStatus === true) {
	    this.setState({
	        playProgress: this.audioDOM.currentTime / this.audioDOM.duration,
	        currentTime: this.audioDOM.currentTime
	    });
	}
}, false);
Copy the code

Audio does not play automatically on mobile without touching the screen for the first time. Add an isFirstPlay property to the constructor function, determine if this property is true after component updates, start playing if it is tru, then set it to false

this.isFirstPlay = true;
Copy the code
ComponentDidUpdate () {if (this.isfirstPlay === true) {this.audiodom.play (); this.isFirstPlay = false; }}Copy the code

Go back to albu.js and add events to play all the buttons

/** * playAll = () => {if (this.state.songs. Length > 0) {// Add props. this.props.changeCurrentSong(this.state.songs[0]); this.props.showMusicPlayer(true); }}Copy the code
<div className="play-button" onClick={this.playAll}> < I className="icon-play"></ I >< span>Copy the code

Add style control to show and hide the Player element of the Player component

<div className="player" ref="player" style={{display:this.props.showStatus === true ? "block" : "none"}}>
  ...
</div>
Copy the code

Click to display the play component, and then play the song

Song control

Add song mode cut, previous, next, and pause and change functions to the playback component

Switch song mode and add click events to the element

changePlayMode = () => { if (this.state.currentPlayMode === this.playModes.length - 1) { this.setState({currentPlayMode:0}); } else { this.setState({currentPlayMode:this.state.currentPlayMode + 1}); }}Copy the code
<div className="play-model-button"  onClick={this.changePlayMode}>
    <i className={"icon-" + this.playModes[this.state.currentPlayMode] + "-play"}></i>
</div>
Copy the code

Play or pause

playOrPause = () => { if(this.audioDOM.paused){ this.audioDOM.play(); this.startImgRotate(); this.setState({ playStatus: true }); }else{ this.audioDOM.pause(); this.stopImgRotate(); this.setState({ playStatus: false }); }}Copy the code
<div className="play-button" onClick={this.playOrPause}>
    <i className={playButtonClass}></i>
</div>
Copy the code

The last one, the next one

previous = () => { if (this.props.playSongs.length > 0 && this.props.playSongs.length ! == 1) { let currentIndex = this.currentIndex; If (this. State. CurrentPlayMode = = = 0) {/ / list on the if (currentIndex = = = 0) {currentIndex = this. Props. PlaySongs. Length - 1. }else{ currentIndex = currentIndex - 1; }} else if (this. State. CurrentPlayMode = = = 1) {/ / single cycle currentIndex = this. CurrentIndex; } else {/ / random broadcast let index = parseInt (Math) random (). * this props. PlaySongs. Length, 10); currentIndex = index; } this.props.changeCurrentSong(this.props.playSongs[currentIndex]); this.currentIndex = currentIndex; } } next = () => { if (this.props.playSongs.length > 0 && this.props.playSongs.length ! == 1) { let currentIndex = this.currentIndex; If (this. State. CurrentPlayMode = = = 0) {/ / list on the if (currentIndex = = = this. Props. PlaySongs. Length - 1) {currentIndex = 0; }else{ currentIndex = currentIndex + 1; }} else if (this. State. CurrentPlayMode = = = 1) {/ / single cycle currentIndex = this. CurrentIndex; } else {/ / random broadcast let index = parseInt (Math) random (). * this props. PlaySongs. Length, 10); currentIndex = index; } this.props.changeCurrentSong(this.props.playSongs[currentIndex]); this.currentIndex = currentIndex; }}Copy the code
<div className="previous-button" onClick={this.previous}>
    <i className="icon-previous"></i>
</div>
...
<div className="next-button" onClick={this.next}>
    <i className="icon-next"></i>
</div>
Copy the code

First judge the play mode, when the play mode is the list play is directly the current position +1, and then get the next song, the next song vice versa. Continue playing the current song while playing in single loop mode. Gets a random integer from 0 to the length of the playlist when play mode is random. Play mode does not work if there is only one current song

To drag the Progress of a song, use the Props callback function for Progress, add the onDrag and onDragEnd properties to the Progress component, and add the constructor dragProgress property to record the Progress of the drag

this.dragProgress = 0;
Copy the code
<Progress progress={this.state.playProgress}
      onDrag={this.handleDrag}
      onDragEnd={this.handleDragEnd}/>
Copy the code
handleDrag = (progress) => { if (this.audioDOM.duration > 0) { this.audioDOM.pause(); this.stopImgRotate(); this.setState({ playStatus: false }); this.dragProgress = progress; } } handleDragEnd = () => { if (this.audioDOM.duration > 0) { let currentTime = this.audioDOM.duration * this.dragProgress; this.setState({ playProgress: this.dragProgress, currentTime: currentTime }, () => { this.audioDOM.currentTime = currentTime; this.audioDOM.play(); this.startImgRotate(); this.setState({ playStatus: true }); this.dragProgress = 0; }); }}Copy the code

The dragging progress is recorded in the dragging. When the dragging ends, the playback time after the dragging and the dragging progress are obtained to update the Player component. After the component is updated, it continues to play from the time after the dragging

Add ended events to audio for post-playback processing. Error event handling is also added

this.audioDOM.addEventListener("ended", () => { if (this.props.playSongs.length > 1) { let currentIndex = this.currentIndex; If (this. State. CurrentPlayMode = = = 0) {/ / list on the if (currentIndex = = = this. Props. PlaySongs. Length - 1) {currentIndex = 0; }else{ currentIndex = currentIndex + 1; }} else if (this. State. CurrentPlayMode = = = 1) {/ / single loop / / this. Continue to play the songs audioDOM. The play (); return; } else {/ / random broadcast let index = parseInt (Math) random (). * this props. PlaySongs. Length, 10); currentIndex = index; } this.props.changeCurrentSong(this.props.playSongs[currentIndex]); this.currentIndex = currentIndex; } else {the if (this. State. CurrentPlayMode = = = 1) {/ / single loop / / this. Continue to play the songs audioDOM. The play (); } else {// pause this.audiodom.pause (); this.stopImgRotate(); this.setState({ playProgress: 0, currentTime: 0, playStatus: false }); } } }, false); Enclosing audioDOM. AddEventListener (" error ", () = > {alert (" error loading songs!" )}, false);Copy the code

The error event is simply a reminder that can automatically switch to the next song

The renderings are as follows

Develop Mini player components

The Mini player component relies on the Audio tag as well as the current playing time, always length, and progress of the song. These properties are already used in the Player component, so the Mini component is used as a child of the Player component

Create miniplayer. js and miniplayer.styl in the play directory. MiniPlayer receives the current song and the playback progress. You also need to introduce Progress to show the playback Progress

MiniPlayer.js

import React from "react" import Progress from "./Progress" import "./miniplayer.styl" class MiniPlayer extends React.Component { render() { let song = this.props.song; let playerStyle = {}; if (this.props.showStatus === true) { playerStyle = {display:"none"}; } if (! song.img) { song.img = require("@/assets/imgs/music.png"); } let imgStyle = {}; if (song.playStatus === true) { imgStyle["WebkitAnimationPlayState"] = "running"; imgStyle["animationPlayState"] = "running"; } else { imgStyle["WebkitAnimationPlayState"] = "paused"; imgStyle["animationPlayState"] = "paused"; } let playButtonClass = song.playStatus === true ? "icon-pause" : "icon-play"; return ( <div className="mini-player" style={playerStyle}> <div className="player-img rotate" style={imgStyle}> <img src={song.img} alt={song.name}/> </div> <div className="player-center"> <div className="progress-wrapper"> <Progress disableButton={true} progress={this.props.progress}/> </div> <span className="song"> {song.name} </span> <span className="singer"> {song.singer} </span> </div> <div className="player-right"> <i className={playButtonClass}></i> <i className="icon-next ml-10"></i> </div> <div className="filter"></div> </div> ); } } export default MiniPlayerCopy the code

The miniplayer.styl code is available in the source code

Import MiniPlayer in the Player component

import MiniPlayer from "./MiniPlayer"
Copy the code

Place it in the following position and pass in song and playProgress

<div className="player-container">
    ...
    <MiniPlayer song={song} progress={this.state.playProgress}/>
</div>
Copy the code

Call the parent component’s play pause and next method control song in the MiniPlayer component. Start by writing methods that handle click events in MiniPlayer

handlePlayOrPause = (e) => { e.stopPropagation(); If (this.props.song. Url) {// call the parent's playOrPause method this.props. PlayOrPause (); } } handleNext = (e) => { e.stopPropagation(); If (this.props.song-url) {// Call the parent component to play the next method this.props. Next (); }}Copy the code

Add click events

<div className="player-right">
    <i className={playButtonClass} onClick={this.handlePlayOrPause}></i>
    <i className="icon-next ml-10" onClick={this.handleNext}></i>
</div>
Copy the code

Pass the playOrPause and next methods to the Player component

<MiniPlayer song={song} progress={this.state.playProgress}
    playOrPause={this.playOrPause}
    next={this.next}/>
Copy the code

The Player and MiniPlayer components have opposite display states. At some point, only one component is displayed and the other is hidden. Next, the Player and MiniPlayer components are displayed and hidden. Add show and hide methods to the Player component

hidePlayer = () => {
    this.props.showMusicPlayer(false);
}
showPlayer = () => {
    this.props.showMusicPlayer(true);
}
Copy the code
<div className="header">
    <span className="header-back" onClick={this.hidePlayer}>
        ...
    </span>
    ...
</div>
Copy the code

Pass showStatus and showPlayer to the MiniPlayer component

<MiniPlayer song={song} progress={this.state.playProgress}
    playOrPause={this.playOrPause}
    next={this.next}
    showStatus={this.props.showStatus}
    showMiniPlayer={this.showPlayer}/>
Copy the code

After hidePlayer is called, update redux showStatus to false, trigger render, pass showStatus to MiniPlayer, and MiniPlayer decides whether to show or hide based on showStatus

Play the component and song list by clicking on the animation

Play component shows and hides animation

It’s too blunt to simply show and hide for the player component, so we animate the show and hide for the player component. The react-transition-group plugin will be used in this tutorial. This animation uses the react-transition-group hook function, as shown below

Introduce the CSSTransition animation component into the Player component

import { CSSTransition } from "react-transition-group"
Copy the code

Wrap the playback component with CSSTransition

<div className="player-container">
    <CSSTransition in={this.props.showStatus} timeout={300} classNames="player-rotate">
    <div className="player" ref="player" style={{display:this.props.showStatus === true ? "block" : "none"}}>
        ...
    </div>
    </CSSTransition>
    <MiniPlayer song={song} progress={this.state.playProgress}
                playOrPause={this.playOrPause}
                next={this.next}
                showStatus={this.props.showStatus}
                showMiniPlayer={this.showPlayer}/>
</div>
Copy the code

The CSSTransition hook function controls the style of the player element

<div className="player" ref="player">
...
</div
Copy the code

Then add onEnter and onExited hook functions to CSSTransition. OnEnter is called when in is true and the component starts to enter, onExited when in is false and the component’s state has changed to leave

<CSSTransition in={this.props.showStatus} timeout={300} classNames="player-rotate"
   onEnter={() => {
       this.playerDOM.style.display = "block";
   }}
   onExited={() => {
       this.playerDOM.style.display = "none";
   }}>
   ...
</CSSTransition>
Copy the code

The player looks like this

.player
  position: fixed
  top: 0
  left: 0
  z-index: 1001
  width: 100%
  height: 100%
  color: #FFFFFF
  background-color: #212121
  display: none
  transform-origin: 0 bottom
  &.player-rotate-enter
    transform: rotateZ(90deg)
    &.player-rotate-enter-active
      transition: transform .3s
      transform: rotateZ(0deg)
  &.player-rotate-exit
    transform: rotateZ(0deg) translate3d(0, 0, 0)
    &.player-rotate-exit-active
      transition: all .3s
      transform: rotateZ(90deg) translate3d(100%, 0, 0)
Copy the code

Song click notes fall animation

Back in the Album component, as each song is clicked, we appear a note at each clicked position and then start falling in a parabolic trajectory. Translate on the X-axis and Y-axis. Use two elements, the outer element is translated on the Y-axis and the inner element is translated on the X-axis. After the transition is complete, cSS3’s transitionEnd is used to listen for the element to complete the transition and then reset its position for the next movement

Create a new util directory under SRC and create event.js to get the transitionEnd event name, compatible with older WebKit kernel browsers

event.js

function getTransitionEndName(dom){ let cssTransition = ["transition", "webkitTransition"]; let transitionEnd = { "transition": "transitionend", "webkitTransition": "webkitTransitionEnd" }; for(let i = 0; i < cssTransition.length; i++){ if(dom.style[cssTransition[i]] ! == undefined){ return transitionEnd[cssTransition[i]]; } } return undefined; } export {getTransitionEndName}Copy the code

Import event.js in Album

import {getTransitionEndName} from "@/util/event"
Copy the code

Put three note elements in the Album and write the style of note elements in app.styl for the convenience of sharing this style later

<CSSTransition in={this.state.show} timeout={300} classNames="translate">
<div className="music-album">
	...
	<div className="music-ico" ref="musicIco1">
		<div className="icon-fe-music"></div>
	</div>
	<div className="music-ico" ref="musicIco2">
		<div className="icon-fe-music"></div>
	</div>
	<div className="music-ico" ref="musicIco3">
		<div className="icon-fe-music"></div>
	</div>
</div>
</CSSTransition>
Copy the code

In the app. Styl increase

.music-ico position: fixed z-index: 1000 margin-top: -7px margin-left: -7px color: #FFD700 font-size: 14px display: None transition: transform 1s Cubic - Bezier (.59, -0.1,.83,.67) transform: translate3d(0, 0, 0) div transition: transform 1sCopy the code

The. Music-ico transition type is a Bezier curve type. This value causes the y-shifted value to transition to a negative value (a negative end value) and then to the target value. Here I modulated the Bezier curve as follows

Can go to thecubic-bezier.comThe address selects the desired Bessel value

Write a method to initialize notes and start a note fall animation

initMusicIco() { this.musicIcos = []; this.musicIcos.push(ReactDOM.findDOMNode(this.refs.musicIco1)); this.musicIcos.push(ReactDOM.findDOMNode(this.refs.musicIco2)); this.musicIcos.push(ReactDOM.findDOMNode(this.refs.musicIco3)); This.musicos.foreach ((item) => {// initialize state item.run = false; let transitionEndName = getTransitionEndName(item); item.addEventListener(transitionEndName, function() { this.style.display = "none"; this.style["webkitTransform"] = "translate3d(0, 0, 0)"; this.style["transform"] = "translate3d(0, 0, 0)"; this.run = false; let icon = this.querySelector("div"); icon.style["webkitTransform"] = "translate3d(0, 0, 0)"; icon.style["transform"] = "translate3d(0, 0, 0)"; }, false); }); } startMusicIcoAnimation({clientX, clientY}) { if (this.musicIcos.length > 0) { for (let i = 0; i < this.musicIcos.length; i++) { let item = this.musicIcos[i]; If (item.run === false) {item.style.top = clientY + "px"; item.style.left = clientX + "px"; item.style.display = "inline-block"; setTimeout(() => { item.run = true; item.style["webkitTransform"] = "translate3d(0, 1000px, 0)"; item.style["transform"] = "translate3d(0, 1000px, 0)"; let icon = item.querySelector("div"); icon.style["webkitTransform"] = "translate3d(-30px, 0, 0)"; icon.style["transform"] = "translate3d(-30px, 0, 0)"; }, 10); break; }}}}Copy the code

Get all the note elements to add to the musicIcos array, and then traverse to add the transitionEnd event to each element. The event handler resets the position of the note element. Add a custom attribute run to each note DOM object to indicate whether the current element is in motion. Start the transition animation by iterating through the musicIcos array while starting the note animation, finding an element with run as false and setting left and top based on the clientX and clientY of the event object, and immediately stopping the loop. This is done so that when the previous element is unmoved, the next unmoved element is moved, when the movement is complete, run becomes false, and the next click continues

We call initMusicIco in componentDidMount

this.initMusicIco();
Copy the code

StartMusicIcoAnimation is then called in the song click event

selectSong(song) {
    return (e) => {
        this.props.setSongs([song]);
        this.props.changeCurrentSong(song);
        this.startMusicIcoAnimation(e.nativeEvent);
    };
}
Copy the code

E.ativeevent obtains the native event object, which is distributed by better-Scroll in Scroll component. Before version 1.6.0, Better-Scroll did not pass clientX and clientY. Better Scroll has been upgraded to 1.6.0

The renderings are as follows

Developing playlists

Considering that the playlist has a lot of list data, if it is placed in the Player component, the render function will be called every time the playback progress is updated, which will affect the performance of the list, so the playlist component and the Player component are divided into two components and put into the MusicPlayer component. They interact with each other through the parent component MusicPlayer

Create musicPlayer.js in the Play directory and import Player.js

MusicPlayer.js

import React from "react"
import Player from "@/containers/Player"

class MusicPlayer extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            <div className="music-player">
                <Player/>
            </div>
        );
    }
}
export default MusicPlayer;
Copy the code

Import musicPlayer. js in app.js instead of the original Player component

//import Player from ".. /containers/Player" import MusicPlayer from "./play/MusicPlayer"Copy the code
<Router>
  <div className="app">
    ...
    {/*<Player/>*/}
    <MusicPlayer/>
  </div>
</Router>
Copy the code

Go ahead and create playerlist. js and playerList.styl under Play

PlayerList.js

import React from "react"
import ReactDOM from "react-dom"

import "./playerlist.styl"

class PlayerList extends React.Component {

    render() {
        return (
            <div className="player-list">
            </div>
        );
    }
}
export default PlayerList
Copy the code

Playerlist.styl code in the source view

The PlayerList needs to get the song list from Redux, so wrap the PlayerList as a container component first. Create playerlist.js under the containers directory with the following code

import {connect} from "react-redux" import {changeSong, removeSong} from ".. /redux/actions" import PlayerList from ".. / components/play/PlayerList Redux "/ / mapping the global state to the components on the props of const mapStateToProps = (state) = > ({currentSong: state.song, playSongs: state.songs }); // mapDispatchToProps const mapDispatchToProps = (dispatch) => ({changeCurrentSong: (song) => { dispatch(changeSong(song)); }, removeSong: (id) => { dispatch(removeSong(id)); }}); Export Default Connect (mapStateToProps, mapDispatchToProps)(PlayerList)Copy the code

Then import the PlayerList container component in musicPlayer.js

import PlayerList from "@/containers/PlayerList"
Copy the code
<div className="music-player">
    <Player/>
    <PlayerList/>
</div>
Copy the code

Now the PlayerList component can get the playlist data from Redux. The Scroll component is also used for the playlist. Clicking on the playlist button in the playlist component displays the playlist. Put this property in the parent component MusicPlayer, and the PlayerList props gets this property to activate show and hide animations. And together they use functions that get the property of the current playing song position and change the song position, passed in through the MsuciPlayer component

Musicplayer.js adds two states, a way to change where a song is played and a way to change the state of the playlist display

constructor(props) { super(props); this.state = { currentSongIndex: 0, show: }} changeCurrentIndex = (index) => {this.setState({currentSongIndex: index}); } showList = (status) => { this.setState({ show: status }); }Copy the code

Pass the state and methods to the props child components

<Player currentIndex={this.state.currentSongIndex}
        showList={this.showList}
        changeCurrentIndex={this.changeCurrentIndex}/>
<PlayerList currentIndex={this.state.currentSongIndex}
            showList={this.showList}
            changeCurrentIndex={this.changeCurrentIndex}
            show={this.state.show}/>
Copy the code

PlayerList uses a state to control show and hide, and CSSTransition’s hook function modifies the state

this.state = {
    showList: false
};
Copy the code
<div className="player-list">
    <CSSTransition in={this.props.show} classNames="fade" timeout={500}
                   onEnter={() => {
                       this.setState({showList:true});
                   }}
                   onEntered={() => {
                       this.refs.scroll.refresh();
                   }}
                   onExited={() => {
                       this.setState({showList:false});
                   }}>
    <div className="play-list-bg" style={this.state.showList === true ? {display:"block"} : {display:"none"}}>
        ...
    </div>
    </CSSTransition>
</div>
Copy the code

Change this.currentIndex = currentIndex in player. js to call changeCurrentIndex and get the play song bit in the first line of the render function

//this.currentIndex = currentIndex; / / call the parent component modify current song position this. Props. ChangeCurrentIndex (currentIndex);Copy the code
this.currentIndex = this.props.currentIndex;
Copy the code

Add an event to the playlist button in the Player component that calls the parent component’s showList

showPlayList = () => {
    this.props.showList(true);
}
Copy the code
<div className="play-list-button" onClick={this.showPlayList}>
    <i className="icon-play-list"></i>
</div>
Copy the code

Add click events to the mask background and close buttons in the PlayerList component to hide the playlist

showOrHidePlayList = () => {
    this.props.showList(false);
}
Copy the code
<div className="play-list-bg" style={this.state.showList === true ? {display:"block"} : {display:"none"}} onClick={this.showOrHidePlayList}> {/* Playlist */} <div className="play-list-wrap"> <div ClassName ="play-list-head"> <span className="head-title"> <span className="close" OnClick ={this.showOrHidePlayList}> Close </span> </div>... </div> </div>Copy the code

Clicking on a song in the playlist also plays the current song, clicking on the Delete button to remove the song from the playlist, and then processing the two events. Add two methods to the PlayerList component to play the song and remove the song, and add click events to the song wrap element and delete button

playSong(song, index) { return () => { this.props.changeCurrentSong(song); this.props.changeCurrentIndex(index); this.showOrHidePlayList(); }; } removeSong(id, index) { return () => { if (this.props.currentSong.id ! == id) { this.props.removeSong(id); If (index < this. Props. CurrentIndex) {/ / call the parent component modify current song position this. Props. ChangeCurrentIndex (this. Props. CurrentIndex - 1); }}}; }Copy the code
<div className="item-right">
    <div className={isCurrent ? "song current" : "song"} onClick={this.playSong(song, index)}>
        <span className="song-name">{song.name}</span>
        <span className="song-singer">{song.singer}</span>
    </div>
    <i className="icon-delete delete" onClick={this.removeSong(song.id, index)}></i>
</div>
Copy the code

There is a slight problem with the delete button and the playlist is closed because the click event propagates to its parent element. Play-list-bg. Play-list-wrap adds click events to the first child of.play-list-bg and then prevents the event from propagating

<div className="play-list-wrap" onClick={e => e.stopPropagation()}>
    ...
</div>
Copy the code

At this point the core playback component is complete

conclusion

This section is the most core function, the content is relatively long, the logic is very complex. Music playback is mainly to use H5 audio tag play, pause methods, canplay, timeUpdate, ended and other events, combined with the UI framework, when appropriate to update the UI. When audio loads the page for the first time on the mobile end, it can’t play automatically without touching the screen, because you’ve already touched the screen many times while rendering the App component, so you just need to call the Play method to play. When using React, refine the components as much as possible. The React component update entry only has a render method, which is used to render the UI. If render is called frequently, it is necessary to extract the sub-components that do not need to be updated frequently to avoid unnecessary performance consumption

The react-transition-group library and hook functions onEnter and onExited are used to animate the playback component. After the component has left onExited, the exit state transition has been completed, and the player component is hidden. Note animations mainly use the Transition type of Bezier curves. Bezier curves can also be used to add shopping cart animations, adjust bezier curve values, and then the target translate position is calculated from the target element and the cart position

Full project address: github.com/dxx/mango-m…

Welcome to star

The code for this chapter is in chapter5