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阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容