React-Native實現仿微信發送語音

最近在做社交功能,需要收發語音,所以就仿照微信做了一個語音錄制功能

使用的是react-native-audio

GitHub地址:https://github.com/jsierles/react-native-audio
配置按照GitHub上配置就可以,挺好配置的

iOS支持的編碼格式:lpcm, ima4, aac, MAC3, MAC6, ulaw, alaw, mp1, mp2, alac, amr
Android支持的編碼:aac, aac_eld, amr_nb, amr_wb, he_aac, vorbis

簡單說下我遇到的問題,android上錄制的在ios上不能播放最后發現錄制的語音設置參數少設置了
最后把錄制格式設定為如下android和ios問題完美解決

        AudioRecorder.prepareRecordingAtPath(audioPath, {
            SampleRate: 22050,
            Channels: 1,
            AudioQuality: 'Low',
            AudioEncoding: 'aac',
            OutputFormat: 'aac_adts',
        });

效果圖如下:


總體思路就是把下面的小方塊注冊為手勢模塊去監聽用戶手勢的變化,然后在state里面設置一些參數根據手勢的變化給用戶反饋

import {AudioRecorder, AudioUtils} from 'react-native-audio';
/*
                this.audioPath
                注意這個是你錄音后文件的路徑+文件名,
                可以使用react-native-audio下的AudioUtils路徑也可以使用其他路徑,
                如果名稱重復會覆蓋掉原有的錄音文件
*/
    this.audioPat = AudioUtils.DocumentDirectoryPath + '/test.aac', //路徑下的文件名
        this.state = {
            actionVisible: false,
            paused: false,
            recordingText: "",
            opacity: 'white',
            recordingColor: "transparent",
            text: I18n.t('message.Chat.Voice.holdToTalk'),
            currentTime: null,        //開始錄音到現在的持續時間
            recording: false,         //是否正在錄音
            stoppedRecording: false,  //是否停止了錄音
            finished: false,          //是否完成錄音
            hasPermission: undefined, //是否獲取權限
        }
  componentDidMount() {
    this.prepareRecordingPath(this.audioPath);
    //添加監聽
    AudioRecorder.onProgress = (data) => {
      this.setState({
        currentTime: Math.floor(data.currentTime)
      }, () => {
        if (this.state.currentTime >= maxTime) {
          Alert.alert(I18n.t('message.Chat.Voice.speakTooLong'))
          this._cancel(false)
        }
      });
    };
    AudioRecorder.onFinished = (data) => {
      // Android callback comes in the form of a promise instead.
      if (Platform.OS === 'ios') {
        this._finishRecording(data.status === "OK", data.audioFileURL);
      }
    };
    //手勢
    this.Gesture = {
      onStartShouldSetResponder: (evt) => true,
      onMoveShouldSetResponder: (evt) => true,
      onResponderGrant: (evt) => {
        if (!this.state.hasPermission) {
          Alert.alert(I18n.t('message.Chat.Voice.jurisdiction'))
        }
        this.setState({
          opacity: "#c9c9c9",
          recordingText: I18n.t('message.Chat.Voice.fingerStroke'),
          text: I18n.t('message.Chat.Voice.releaseEnd'),
          icon: "ios-mic",
          recordingColor: 'transparent'
        }, _ => RecordView.show(this.state.recordingText, this.state.recordingColor, this.state.icon));
        this._record();
      },
      onResponderReject: (evt) => {
      },
      onResponderMove: (evt) => {
        if (evt.nativeEvent.pageY < this.recordPageY - UpperDistance) {
          if (this.state.recordingColor != 'red') {
            this.setRecordView(I18n.t('message.Chat.Voice.loosenFingers'), 'red', "ios-mic-off")
          }
        } else if (this.state.recordingColor != 'transparent') {
          this.setRecordView(I18n.t('message.Chat.Voice.fingerStroke'), 'transparent', "ios-mic")
        }
      },
      onResponderRelease: (evt) => {
        this.setState({
          opacity: "white",
          text: I18n.t('message.Chat.Voice.holdToTalk')
        });
        RecordView.hide();
        let canceled;
        if (evt.nativeEvent.locationY < 0 ||
          evt.nativeEvent.pageY < this.recordPageY) {
          canceled = true;
        } else {
          canceled = false;
        }
        this._cancel(canceled)
      },
      onResponderTerminationRequest: (evt) => true,
      onResponderTerminate: (evt) => {
      },
    }
  }
  setRecordView = (recordingText, recordingColor, icon) => {
    this.setState({
      recordingText: recordingText,
      recordingColor: recordingColor,
      icon: icon
    }, _ => RecordView.show(this.state.recordingText, this.state.recordingColor, this.state.icon));
  }
render() {
    const { opacity, text } = this.state
    return (
      <View style={styles.Box}>
        <View
          {...this.Gesture}        //注冊為手勢組件
          onLayout={this.handleLayout}
          ref={(record) => this.record = record}
          style={[styles.textBoxStyles, { backgroundColor: opacity }]}>
          <Text>{text}</Text>
        </View>
      </View>
    )
  }

上面彈出層的浮框實現為

使用的是一個三方庫teaset
GitHub地址:https://github.com/rilyu/teaset

class RecordView {
  static key = null;
  static show(text, color, icon) {
    let showIcon;
    if (RecordView.key) RecordView.hide()
    if (color == 'red') {
      showIcon = (<Image source={ic_ch3x} style={styles.imageStyles} />)
    } else if (icon == 'short') {
      showIcon = (<Image source={SHORT4} style={styles.imageStyles} />)
    } else {
      showIcon = (
        <View>
          <Icon name={'ios-mic'} style={styles.IconStyles} />
          <Spinner size={24} type="ThreeBounce" color='white' />
        </View>
      );
    }

    RecordView.key = Toast.show({
      text: (
        <Text style={[styles.textStyles, { backgroundColor: color }]}>
          {text}
        </Text>
      ),
      icon: showIcon,
      position: 'center',
      duration: 1000000,
    });
  }
    static hide() {
        if (!RecordView.key) return;
        Toast.hide(RecordView.key);
        RecordView.key = null;
    }
}

我把代碼直接貼上吧,沒單獨從項目中摘出來,就貼整個文件了

import React, { Component } from 'react';
import {
  Image, PermissionsAndroid, Alert,
  Platform, UIManager, findNodeHandle, DeviceEventEmitter
} from 'react-native';
import styles from './Styles';
import { Toast } from 'teaset';
import I18n from '../../../../I18n';
import { UpperDistance } from '../config';
import Spinner from "react-native-spinkit";
import { Text, Icon, View } from 'native-base'
import { AudioRecorder } from 'react-native-audio';
import Permissions from 'react-native-permissions';
import SHORT4 from '../../../../Images/C2CImg/SHORT4.png';
import ic_ch3x from '../../../../Images/C2CImg/ic_ch3x.png';
import MessageUtil from '../../MessageUtilModel/MessageUtil';
const maxTime = 45;  //最大時間
const minTime = 1; //最小時間
export default class Voice extends Component {
  constructor(props) {
    super(props)
    this.state = {
      paused: false,
      recordingText: "",
      opacity: 'white',
      recordingColor: "transparent",
      text: I18n.t('message.Chat.Voice.holdToTalk'),
      currentTime: null,        //開始錄音到現在的持續時間
      recording: false,         //是否正在錄音
      stoppedRecording: false,  //是否停止了錄音
      finished: false,          //是否完成錄音
      hasPermission: undefined, //是否獲取權限
    }
    // 語音存儲路徑
    const { userId } = this.props.chatFriend || {}
    this.audioPath = MessageUtil.getFilePath(userId, `${(new Date()).getTime()}.aac`)
  }
  componentDidMount() {
    this._checkPermission()
    this.prepareRecordingPath(this.audioPath);
    AudioRecorder.onProgress = (data) => {
      this.setState({
        currentTime: Math.floor(data.currentTime)
      }, () => {
        if (this.state.currentTime >= maxTime) {
          Alert.alert(I18n.t('message.Chat.Voice.speakTooLong'))
          this._cancel(false)
        }
      });
    };
    AudioRecorder.onFinished = (data) => {
      // Android callback comes in the form of a promise instead.
      if (Platform.OS === 'ios') {
        this._finishRecording(data.status === "OK", data.audioFileURL);
      }
    };
    this.Gesture = {
      onStartShouldSetResponder: (evt) => true,
      onMoveShouldSetResponder: (evt) => true,
      onResponderGrant: (evt) => {
        if (!this.state.hasPermission) {
          Alert.alert(I18n.t('message.Chat.Voice.jurisdiction'))
        }
        this.setState({
          opacity: "#c9c9c9",
          recordingText: I18n.t('message.Chat.Voice.fingerStroke'),
          text: I18n.t('message.Chat.Voice.releaseEnd'),
          icon: "ios-mic",
          recordingColor: 'transparent'
        }, _ => RecordView.show(this.state.recordingText, this.state.recordingColor, this.state.icon));
        this._record();
      },
      onResponderMove: (evt) => {
        if (evt.nativeEvent.pageY < this.recordPageY - UpperDistance) {
          if (this.state.recordingColor != 'red') {
            this.setRecordView(I18n.t('message.Chat.Voice.loosenFingers'), 'red', "ios-mic-off")
          }
        } else if (this.state.recordingColor != 'transparent') {
          this.setRecordView(I18n.t('message.Chat.Voice.fingerStroke'), 'transparent', "ios-mic")
        }
      },
      onResponderRelease: (evt) => {
        this.setState({
          opacity: "white",
          text: I18n.t('message.Chat.Voice.holdToTalk')
        });
        RecordView.hide();
        let canceled;
        if (evt.nativeEvent.locationY < 0 ||
          evt.nativeEvent.pageY < this.recordPageY) {
          canceled = true;
        } else {
          canceled = false;
        }
        this._cancel(canceled)
      },
      onResponderTerminationRequest: (evt) => true
    }
    //chatChange type 1前臺 0后臺 2中間
    this.ShowLocation = DeviceEventEmitter.addListener('chatChange', (type) => {
      this._cancel(false)
    })
  }
  componentWillUnmount() {
    this.ShowLocation.remove()
    AudioRecorder.removeListeners()
    this.timer && clearTimeout(this.timer);
  }
  prepareRecordingPath = (audioPath) => {
    AudioRecorder.prepareRecordingAtPath(audioPath, {
      SampleRate: 22050,
      Channels: 1,
      AudioQuality: 'Low',
      AudioEncoding: 'aac',
      OutputFormat: 'aac_adts',
    });
  }
  _checkPermission = async () => {
    const rationale = {
      'title': I18n.t('message.Chat.Voice.tips'),
      'message': I18n.t('message.Chat.Voice.tipsMessage')
    };
    let askForGrant = false
    if (Platform.OS === 'ios') {
      Permissions.check('microphone', { type: 'always' }).then(res => {
        if (res == 'authorized') {
          this.setState({ hasPermission: true })
        } else {
          Permissions.request('microphone', { type: 'always' }).then(response => {
            if (response == 'denied') {
              askForGrant = true
            } else if (response == 'authorized') {
              this.setState({ hasPermission: true });
            }
          });
        }
      });
    } else {
      const status = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, rationale)
      if (status !== "granted") {
        askForGrant = true
      } else {
        this.setState({ hasPermission: true });
      }
    }
    if (askForGrant) {
      Alert.alert(
        'Can we access your microphone and Speech Recognition?',
        'We need access so you can record your voice',
        [
          {
            text: 'Later',
            onPress: () => console.log('Permission denied'),
            style: 'cancel',
          },
          {
            text: 'Open Settings',
            onPress: Permissions.openSettings
          },
        ],
      );
    }
  }
  setRecordView = (recordingText, recordingColor, icon) => {
    this.setState({
      recordingText: recordingText,
      recordingColor: recordingColor,
      icon: icon
    }, _ => RecordView.show(recordingText, recordingColor, icon));
  }

  _cancel = (canceled) => {
    let filePath = this._stop();
    if (canceled) return;
    if (this.state.currentTime < minTime) {
      this.setRecordView(I18n.t('message.Chat.Voice.speakTooShort'), 'transparent', "short")
      this.timer = setTimeout(() => { RecordView.hide() }, 300)
      return;
    }
    this.setState({ currentTime: null })
    let voice = {
      audioPath: this.audioPath,
      currentTime: this.state.currentTime
    }
    setTimeout(() => { this.props.SendVoice(voice) }, 500)
  }
  _pause = async () => {
    if (!this.state.recording) return;
    try {
      const filePath = await AudioRecorder.pauseRecording();
      this.setState({ paused: true });
    } catch (error) {
    }
  }

  _resume = async () => {
    if (!this.state.paused) return;
    try {
      await AudioRecorder.resumeRecording();
      this.setState({ paused: false });
    } catch (error) {
    }
  }

  _stop = async () => {
    if (!this.state.recording) return;

    this.setState({ stoppedRecording: true, recording: false, paused: false });

    try {
      const filePath = await AudioRecorder.stopRecording();

      if (Platform.OS === 'android') {
        this._finishRecording(true, filePath);
      }
      return filePath;
    } catch (error) {
    }
  }
  _finishRecording = (didSucceed, filePath) => {
    this.setState({ finished: didSucceed });
  }
  _record = async () => {
    const { recording, hasPermission, stoppedRecording } = this.state
    const { userId } = this.props.chatFriend || {}
    if (recording) return;
    if (!hasPermission) return;
    if (stoppedRecording) {
      this.audioPath = MessageUtil.getFilePath(userId, `${(new Date()).getTime()}.aac`)
      this.prepareRecordingPath(this.audioPath);
    }
    this.setState({
      recording: true,
      paused: false
    });

    try {
      const filePath = await AudioRecorder.startRecording();
    } catch (error) {
    }
  }
  handleLayout = () => {
    const handle = findNodeHandle(this.record);
    UIManager.measure(handle, (x, y, w, h, px, py) => {
      // this._ownMeasurements = { x, y, w, h, px, py };
      this.recordPageX = px;
      this.recordPageY = py;
    });
  }
  render() {
    const { opacity, text } = this.state
    return (
      <View style={styles.Box}>
        <View
          {...this.Gesture}
          onLayout={this.handleLayout}
          ref={(record) => this.record = record}
          style={[styles.textBoxStyles, { backgroundColor: opacity }]}>
          <Text>{text}</Text>
        </View>
      </View>
    )
  }
}
class RecordView {
  static key = null;
  static show(text, color, icon) {
    let showIcon;
    if (RecordView.key) RecordView.hide()
    if (color == 'red') {
      showIcon = (<Image source={ic_ch3x} style={styles.imageStyles} />)
    } else if (icon == 'short') {
      showIcon = (<Image source={SHORT4} style={styles.imageStyles} />)
    } else {
      showIcon = (
        <>
          <Icon name={'ios-mic'} style={styles.IconStyles} />
          <Spinner size={24} type="ThreeBounce" color='white' />
        </>
      );
    }

    RecordView.key = Toast.show({
      text: (
        <Text style={[styles.textStyles, { backgroundColor: color }]}>
          {text}
        </Text>
      ),
      icon: showIcon,
      position: 'center',
      duration: 1000000,
    });
  }
  static hide() {
    if (!RecordView.key) return;
    Toast.hide(RecordView.key);
    RecordView.key = null;
  }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容