React Native中实现音频进度条

3

我一直在尝试使用Expo在React Native中构建媒体播放器,以便在我的音乐项目中播放音频。

我已经成功地设计了一个具有所需外观的播放器,但是我仍然有一个很大的限制。我希望能够实现一个进度条,显示歌曲播放的进度。

这是我的播放器设计。另外,我该如何在iOS上替换这个进度条?

render() {
        return (
            <View >
                <View style={styles.container} >
                    <Image
                        style={styles.imageStyle}
                        source={{uri: this.state.coverName || this.MusicPlayer.getCurrentItemCover()}}
                    />  
                    <View >
                        <Text style = {styles.artistName}> {this.state.artistName || this.MusicPlayer.getCurrentItemArtistName()}</Text>   

                    </View>
                    <View style={{paddingRight:2, paddingLeft:2}}>
                        <Text style={styles.songStyle}> {this.state.title || this.MusicPlayer.getCurrentSongTitle()}</Text>
                    </View>
                     <ProgressBarAndroid style={{marginLeft:10, marginRight:10}} styleAttr="Horizontal" color="#2196F3" indeterminate={false} progress={0.5} />
                    <View style={{flexDirection:'row', padding:10, alignItems:'center', justifyContent:'center'}}>
                        <Text style={styles.iconStyle2} onPress={this.playPrev}>
                            <Feather name="rewind" size={20}  style={styles.text}  />
                        </Text>

                        {this.state.playing?
                            <Text style={styles.iconStyle2} onPress={this.startStopPlay}>
                                <Feather name="pause" size={24}  style={styles.text}  />
                            </Text>
                        :
                            <Text style={styles.iconStyle2} onPress={this.startStopPlay}>
                                <Feather name="play-circle" size={24}  style={styles.text}  />
                            </Text>
                        }

                        <Text style={styles.iconStyle2} onPress={this.playNext}>
                            <Feather name="fast-forward" size={20}  style={styles.text}  />
                        </Text>
                    </View>                        
                </View>

            </View>
        );
    }

}

我的播放功能

 startPlay = async (index = this.index, playing = false) => {
        const url = this.list[index].url;
        this.index = index;
        console.log(url);
        // Checking if now playing music, if yes stop that
        if(playing) {
            await this.soundObject.stopAsync();
        } else {
            // Checking if item already loaded, if yes just play, else load music before play
            if(this.soundObject._loaded) {
                await this.soundObject.playAsync();
            } else {
                await this.soundObject.loadAsync(url);
                await this.soundObject.playAsync();
            }
        }
    };

我的主要目标是在移动设备上实现一个与此类似的小型播放器。 enter image description here 我正在使用React Native Expo版本进行开发。

你可以使用npm插件来添加控件吗?(除非你决定自己开发控件..)看看这个https://www.npmjs.com/package/react-native-media-controls。此外,Github上还有一个[track-player](https://github.com/react-native-kit/react-native-track-player)值得研究。 - Rachel Gallen
Rachel Gallen,非常感谢您的回复。我已经查看了trackPlayer,但它并不符合我的要求。例如,我正在使用包含多个歌曲等内容的对象数组进行工作。我还查看了媒体控件,它看起来很有趣,但是在我的情况下,我如何获取那些参数,例如音频文件持续时间等。我在音频控件中没有看到任何这些东西。谢谢。 - Nges Brian
嗨,还有其他插件可用,它们只是示例。npm 音乐控制 看起来更适合你(链接上的图片只显示了一个带有一个按钮的进度按钮,但你可以自定义它)。根据我的经验,值得检查 github 上的源文件,以获取一些关于如何调整/改进它以满足你自己的喜好/需求的提示。希望这能帮到你。 - Rachel Gallen
1个回答

22

enter image description here

音乐播放器的完整代码,适用于Android和iOS平台,SeekBar.js。

import React, { Component } from 'react';

import { defaultString } from '../String/defaultStringValue';
import {
  View,
  Text,
  StyleSheet,
  Image,
  Slider,
  TouchableOpacity,
} from 'react-native';

function pad(n, width, z = 0) {
  n = n + '';
  return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
}

const minutesAndSeconds = (position) => ([
  pad(Math.floor(position / 60), 2),
  pad(position % 60, 2),
]);

const SeekBar = ({
  trackLength,
  currentPosition,
  onSeek,
  onSlidingStart,
}) => {
  const elapsed = minutesAndSeconds(currentPosition);
  const remaining = minutesAndSeconds(trackLength - currentPosition);
  return (
    <View style={styles.container}>
      <View style={{ flexDirection: 'row' }}>
        <Text style={[styles.text, { color: defaultString.darkColor }]}>
          {elapsed[0] + ":" + elapsed[1]}
        </Text>
        <View style={{ flex: 1 }} />
        <Text style={[styles.text, { width: 40, color: defaultString.darkColor }]}>
          {trackLength > 1 && "-" + remaining[0] + ":" + remaining[1]}
        </Text>
      </View>
      <Slider
        maximumValue={Math.max(trackLength, 1, currentPosition + 1)}
        onSlidingStart={onSlidingStart}
        onSlidingComplete={onSeek}
        value={currentPosition}
        minimumTrackTintColor={defaultString.darkColor}
        maximumTrackTintColor={defaultString.lightGrayColor}
        thumbStyle={styles.thumb}
        trackStyle={styles.track}
      />
    </View>
  );
};

export default SeekBar;

const styles = StyleSheet.create({
  slider: {
    marginTop: -12,
  },
  container: {
    paddingLeft: 16,
    paddingRight: 16,
    paddingTop: 16,
  },
  track: {
    height: 2,
    borderRadius: 1,
  },
  thumb: {
    width: 10,
    height: 10,
    borderRadius: 5,
    backgroundColor: defaultString.darkColor,
  },
  text: {
    color: 'rgba(255, 255, 255, 0.72)',
    fontSize: 12,
    textAlign: 'center',
  }
});

Player.js

import React, { Component } from 'react';
import {
  View,
  Text,
  StatusBar,
} from 'react-native';
import Header from './Header';
import AlbumArt from './AlbumArt';
import TrackDetails from './TrackDetails';
import SeekBar from './SeekBar';
import Controls from './Controls';
import Video from 'react-native-video';

export default class Player extends Component {
  constructor(props) {
    super(props);
    this.state = {
      paused: true,
      totalLength: 1,
      currentPosition: 0,
      selectedTrack: 0,
      repeatOn: false,
      shuffleOn: false,
    };
  }

  setDuration(data) {
    this.setState({ totalLength: Math.floor(data.duration) });
  }

  setTime(data) {
    this.setState({ currentPosition: Math.floor(data.currentTime) });
  }

  seek(time) {
    time = Math.round(time);
    this.refs.audioElement && this.refs.audioElement.seek(time);
    this.setState({
      currentPosition: time,
      paused: false,
    });
  }

  onBack() {
    if (this.state.currentPosition < 10 && this.state.selectedTrack > 0) {
      this.refs.audioElement && this.refs.audioElement.seek(0);
      this.setState({ isChanging: true });
      setTimeout(() => this.setState({
        currentPosition: 0,
        paused: false,
        totalLength: 1,
        isChanging: false,
        selectedTrack: this.state.selectedTrack - 1,
      }), 0);
    } else {
      this.refs.audioElement.seek(0);
      this.setState({
        currentPosition: 0,
      });
    }
  }

  onForward() {
    if (this.state.selectedTrack < this.props.tracks.length - 1) {
      this.refs.audioElement && this.refs.audioElement.seek(0);
      this.setState({ isChanging: true });
      setTimeout(() => this.setState({
        currentPosition: 0,
        totalLength: 1,
        paused: false,
        isChanging: false,
        selectedTrack: this.state.selectedTrack + 1,
      }), 0);
    }
  }



  render() {
    const track = this.props.tracks[this.state.selectedTrack];
    const video = this.state.isChanging ? null : (
      <Video source={{ uri: track.audioUrl }} // Can be a URL or a local file.
        ref="audioElement"
        playInBackground={true}
        playWhenInactive={true}
        paused={this.state.paused}               // Pauses playback entirely.
        resizeMode="cover"           // Fill the whole screen at aspect ratio.
        repeat={true}                // Repeat forever.
        onLoadStart={this.loadStart} // Callback when video starts to load
        onLoad={this.setDuration.bind(this)}    // Callback when video loads
        onProgress={this.setTime.bind(this)}    // Callback every ~250ms with currentTime
        onEnd={this.onEnd}           // Callback when playback finishes
        onError={this.videoError}    // Callback when video cannot be loaded
        style={styles.audioElement} />
    );

    return (
      <View style={styles.container}>
        {/* <StatusBar hidden={true} /> */}
        {/* <Header message="Playing From Charts" /> */}
        <AlbumArt url={track.albumArtUrl} />
        <TrackDetails title={track.title} artist={track.artist} />
        <SeekBar
          onSeek={this.seek.bind(this)}
          trackLength={this.state.totalLength}
          onSlidingStart={() => this.setState({ paused: true })}
          currentPosition={this.state.currentPosition}
        />
        <Controls
          onPressRepeat={() => this.setState({ repeatOn: !this.state.repeatOn })}
          repeatOn={this.state.repeatOn}
          shuffleOn={this.state.shuffleOn}
          forwardDisabled={this.state.selectedTrack === this.props.tracks.length - 1}
          onPressShuffle={() => this.setState({ shuffleOn: !this.state.shuffleOn })}
          onPressPlay={() => this.setState({ paused: false })}
          onPressPause={() => this.setState({ paused: true })}
          onBack={this.onBack.bind(this)}
          onForward={this.onForward.bind(this)}
          paused={this.state.paused} />
        {video}
      </View>
    );
  }
}

const styles = {
  container: {
    flex: 1,
    backgroundColor: '#ffffff',
  },
  audioElement: {
    height: 0,
    width: 0,
  }
};

AlbumArt.js

:专注于提供音乐专辑封面的JavaScript库。

import React, { Component } from 'react';

import {
  View,
  Text,
  StyleSheet,
  Image,
  TouchableHighlight,
  TouchableOpacity,
  Dimensions,
} from 'react-native';

const AlbumArt = ({
  url,
  onPress
}) => (
    <View style={styles.container}>
      <TouchableOpacity onPress={onPress}>
        <View
          style={[styles.image, {
            elevation: 10, shadowColor: '#d9d9d9',
            shadowOffset: { width: 0, height: 0 },
            shadowOpacity: 1,
            shadowRadius: 2,
            borderRadius: 20,
            backgroundColor: '#ffffff'
          }]}
        >
          <Image
            style={[styles.image, { borderRadius: 20 }]}
            source={{ uri: url }}
          />
        </View>
      </TouchableOpacity>
    </View>
  );

export default AlbumArt;

const { width, height } = Dimensions.get('window');
const imageSize = width - 100;

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    marginTop: 30,
    paddingLeft: 24,
    paddingRight: 24,
  },
  image: {
    width: imageSize,
    height: imageSize,
  },
})

App.js

import React, { Component } from 'react';
import Player from './Player';
import { BackHandler } from 'react-native';
import i18n from '../../Assets/I18n/i18n';
import { Actions } from 'react-native-router-flux';
export default class MusicPlayer extends Component {
  constructor(props) {
    super(props);
    const { navigation } = this.props;
    this.state = {
      song: navigation.getParam('songid')
    };
    this.props.navigation.setParams({
      title: i18n.t('Panchkhan')
    })
  }
  componentWillMount() {
    BackHandler.addEventListener('hardwareBackPress', this.handleBackButton);
  }

  componentWillUnmount() {
    BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton);
  }

  handleBackButton = () => {
    Actions.pop();
    return true;
  };
  render() {
    const TRACKS = [
      {
        title: 'Stressed Out',
        artist: 'Twenty One Pilots',
        albumArtUrl: "https://cdn-images-1.medium.com/max/1344/1*fF0VVD5cCRam10rYvDeTOw.jpeg",
        audioUrl: this.state.song
      }
    ];
    return <Player tracks={TRACKS} />
  }
}

Controls.js

:控件库,用于创建 Web 应用程序中的用户界面元素和交互式组件。
import React, { Component } from 'react';
import { defaultString } from '../String/defaultStringValue';

import {
  View,
  Text,
  StyleSheet,
  Image,
  TouchableOpacity,
} from 'react-native';

const Controls = ({
  paused,
  shuffleOn,
  repeatOn,
  onPressPlay,
  onPressPause,
  onBack,
  onForward,
  onPressShuffle,
  onPressRepeat,
  forwardDisabled,
}) => (
    <View style={styles.container}>
      <TouchableOpacity activeOpacity={0.0} onPress={onPressShuffle}>
        <Image style={[{ tintColor: defaultString.darkColor } , styles.secondaryControl, shuffleOn ? [] : styles.off]}
          source={require('../img/ic_shuffle_white.png')} />
      </TouchableOpacity>
      <View style={{ width: 40 }} />
      <TouchableOpacity onPress={onBack}>
        <Image style={{ tintColor: defaultString.darkColor }} source={require('../img/ic_skip_previous_white_36pt.png')} />
      </TouchableOpacity>
      <View style={{ width: 20 }} />
      {!paused ?
        <TouchableOpacity onPress={onPressPause}>
          <View style={styles.playButton}>
            <Image style={{ tintColor: defaultString.darkColor }} source={require('../img/ic_pause_white_48pt.png')} />
          </View>
        </TouchableOpacity> :
        <TouchableOpacity onPress={onPressPlay}>
          <View style={styles.playButton}>
            <Image style={{ tintColor: defaultString.darkColor }} source={require('../img/ic_play_arrow_white_48pt.png')} />
          </View>
        </TouchableOpacity>
      }
      <View style={{ width: 20 }} />
      <TouchableOpacity onPress={onForward}
        disabled={forwardDisabled}>
        <Image style={[forwardDisabled && { opacity: 0.3 }, { tintColor: defaultString.darkColor }]}
          source={require('../img/ic_skip_next_white_36pt.png')} />
      </TouchableOpacity>
      <View style={{ width: 40 }} />
      <TouchableOpacity activeOpacity={0.0} onPress={onPressRepeat}>
        <Image style={[{ tintColor: defaultString.darkColor }, styles.secondaryControl, repeatOn ? [] : styles.off]}
          source={require('../img/ic_repeat_white.png')} />
      </TouchableOpacity>
    </View>
  );

export default Controls;

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    paddingTop: 8,
  },
  playButton: {
    height: 72,
    width: 72,
    borderWidth: 1,
    borderColor: defaultString.darkColor,
    borderRadius: 72 / 2,
    alignItems: 'center',
    justifyContent: 'center',
  },
  secondaryControl: {
    height: 18,
    width: 18,
  },
  off: {
    opacity: 0.30,
  }
})

控制详情.js

import React, { Component } from 'react';
import { defaultString } from '../String/defaultStringValue';

import {
  View,
  Text,
  StyleSheet,
  Image,
  TouchableHighlight,
  TouchableOpacity,
  Dimensions,
} from 'react-native';

const TrackDetails = ({
  title,
  artist,
  onAddPress,
  onMorePress,
  onTitlePress,
  onArtistPress,
}) => (
  <View style={styles.container}>
    {/* <TouchableOpacity onPress={onAddPress}>
      <Image style={styles.button}
        source={require('../img/ic_add_circle_outline_white.png')} />
    </TouchableOpacity> */}
    <View style={styles.detailsWrapper}>
      <Text style={styles.title} onPress={onTitlePress}>{title}</Text>
      <Text style={styles.artist} onPress={onArtistPress}>{artist}</Text>
    </View>
    {/* <TouchableOpacity onPress={onMorePress}>
      <View style={styles.moreButton}>
        <Image style={styles.moreButtonIcon}
          source={require('../img/ic_more_horiz_white.png')} />
      </View>
    </TouchableOpacity> */}
  </View>
);

export default TrackDetails;

const styles = StyleSheet.create({
  container: {
    paddingTop: 24,
    flexDirection: 'row',
    paddingLeft: 20,
    alignItems: 'center',
    paddingRight: 20,
  },
  detailsWrapper: {
    justifyContent: 'center',
    alignItems: 'center',
    flex: 1,
  },
  title: {
    fontSize: 16,
    fontWeight: 'bold',
    color: defaultString.darkColor,
    textAlign: 'center',
  },
  artist: {
    color: defaultString.darkColor,
    fontSize: 12,
    marginTop: 4,
  },
  button: {
    opacity: 0.72,
  },
  moreButton: {
    borderColor: 'rgb(255, 255, 255)',
    borderWidth: 2,
    opacity: 0.72,
    borderRadius: 10,
    width: 20,
    height: 20,
    alignItems: 'center',
    justifyContent: 'center',
  },
  moreButtonIcon: {
    height: 17,
    width: 17,
  }
});


1
最好的答案!非常感谢分享这段代码。 - Manuela
经过长时间的研究,终于得出了最佳答案。非常感谢您分享了优秀的代码。 - R.Mohanraj
滑块(Slider)的 thumbStyletrackStyle 属性真的存在吗?在滑块文档中找不到它们。 - Muhammad Qasim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接