react-native英语测评APP开发


React Native英语测评APP开发

背景:

由于需要调用流利说接口进行DEMO展示,所以采用react native在windows机器上进行android平台APP开发

开发参考链接:

  • https://www.react-native.cn/docs/environment-setup

react-native简介:

React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的JS框架 React 在原生移动应用平台的衍生产物,支持iOS和安卓两大平台。RN使用Javascript语言,类似于HTML的JSX,以及CSS来开发移动应用,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域

1.搭建开发环境

1.1.安装Node和JDK

注意:

Node 的版本应大于等于 12,而 JDK 的版本必须是 1.8(目前不支持 1.9 及更高版本,注意 1.8 版本官方也直接称 8 版本)。安装完 Node 后建议设置 npm 镜像(淘宝源)以加速后面的过程(或使用fan qiang工具)。

不要使用 cnpm!cnpm 安装的模块路径比较奇怪,packager 不能正常识别!

1.1.1.安装Node

简介:

简单的说 Node.js 就是运行在服务端的 JavaScript,Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台,Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好

相关资源:

  • node.js下载地址

下载合适的版本后安装,下载安装后使用cmd中输入node -v查看安装是否成功

# 使用nrm工具切换淘宝源
npx nrm use taobao
# 如果之后需要切换回官方源可使用
npx nrm use npm

1.1.2.安装JDK

简介:

JDK是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVM+Java系统类库)和JAVA工具

需要注册Oracle账号才能下载

相关资源:

  • Java SE Development Kit 下载地址

  • 切换java版本参考博客

# 如果切换不成功,使用命令行where java查看java安装路径,然后删除里面的jivapath文件夹,保留现在的想要的安装文件夹即可
C:\Users\K0802389>where java
C:\Program Files\Common Files\Oracle\Java\javapath\java.exe # 找到并删除这个javapath
D:\app\JDK8\install\bin\java.exe

1.2.安装Yarn

简介:

Yarn是 Facebook 提供的替代 npm 的工具,可以加速 node 模块的下载

npm install -g yarn

安装完 yarn 之后就可以用 yarn 代替 npm 了,例如用yarn代替npm install命令,用yarn add 某第三方库名代替npm install 某第三方库名

1.3.安装watchman

可先跳过安装,如果后面应用报错再进行安装

1.3.1.安装chocolatey

首先我们需要安装chocolatey,然后使用chocolatey来安装watchman,安装教程可以百度,在chocolatey安装中遇到问题如下:

解决方案见如下博客:

  • https://blog.csdn.net/cckevincyh/article/details/92082374

如遇到下图中的警告,请删除chocolatey文件夹

1.3.2.安装watchman

参照Watchman 的安装说明来从源码来编译和安装 Watchman。

Watchman是由 Facebook 提供的监视文件系统变更的工具。安装此工具可以提高开发时的性能(metro 可以快速捕捉文件的变化从而实现实时刷新)

1.4.安装Android开发环境

1.4.1.安装 Android Studio

首先下载和安装 Android Studio,国内用户可能无法打开官方链接,请自行使用搜索引擎搜索可用的下载链接。安装界面中选择"Custom"选项,确保选中了以下几项:

  • Android SDK
  • Android SDK Platform
  • Android Virtual Device

然后点击"Next"来安装选中的组件(如果没有,就直接安装,后面再安装也行)。

安装后就可以打开Android Studio了

打开之后因为google的大部分业务需要FQ,所以首先找一个代理软件,在电脑能fan qiang之后,还需要给android studio配置代理让其能够下载东西

  • 找到SDK Manager

  • 配置HTTP代理,选择SOCKS

如果设置完之后,设置SDK路径时报错,请退出应用后使用管理员权限运行Android Studio,重新运行时会显示需要下载的内容

在 SDK Manager 中选择"SDK Platforms"选项卡,然后在右下角勾选"Show Package Details"。展开Android 10 (Q)选项,确保勾选了下面这些组件(重申你必须使用稳定的代理软件,否则可能都看不到这个界面):

  • Android SDK Platform 29
  • Intel x86 Atom_64 System Image(官方模拟器镜像文件,使用非官方模拟器不需要安装此组件)

然后点击"SDK Tools"选项卡,同样勾中右下角的"Show Package Details"。展开"Android SDK Build-Tools"选项,确保选中了 React Native 所必须的29.0.2版本。你可以同时安装多个其他版本。

然后还是在"SDK Tools"选项卡,点击"NDK (Side by side)",同样勾中右下角的"Show Package Details",选择20.1.5948944版本进行安装。

最后点击"Apply"来下载和安装这些组件。

1.4.2.配置环境变量

小技巧:windows中win+d可以快速清理屏幕

右键我的电脑->属性->高级系统设置 -> 高级 -> 环境变量 -> 新建,创建一个名为ANDROID_HOME的环境变量(系统或用户变量均可),指向你的 Android SDK 所在的目录(具体的路径可能和下图不一致,请自行确认):

SDK 默认是安装在下面的目录:

C:\Users\你的用户名\AppData\Local\Android\Sdk

你可以在 Android Studio 的"Preferences"菜单中查看 SDK 的真实路径,具体是Appearance & BehaviorSystem SettingsAndroid SDK

在环境变量Path中添加如下

%ANDROID_HOME%\platform-tools
%ANDROID_HOME%\emulator
%ANDROID_HOME%\tools
%ANDROID_HOME%\tools\bin

1.4.3.使用真机进行调试

请参考博客:

  • https://blog.csdn.net/fuckingone/article/details/84727321

1.4.3.启动AVD模拟器

1.4.3.1.安装HAXM问题

出现了HAXM package在Android Studio中安装不上的情况,网上找到很多资料,设置VT-X虚拟技术或者是禁用Hyper-V都试过了,但是没用

  • Hyper-V

于是自己去看了下需要的HAXM版本,找到了官网上,对应版本下载下来

  • HAXM github官网

然后再在intel目录下面创建Hardware_Accelerated_Execution_Manager,将下载的HAXM解压到目录下面,我是直接安装的,电脑上可以直接安装,但是在Android Studio中依然是没有显示已安装,于是继续尝试,发现在Android Studio中点击安装HAXM时下载了同样的东西并出现了Hardware_Accelerated_Execution_Manager-2

目录中多出来了一个package.xml的东西,由于出现错误后,Android Studio中如果点击finish文件会被删除,所以先不点击finish按钮,把Hardware_Accelerated_Execution_Manager-2中package.xml复制到Hardware_Accelerated_Execution_Manager中,然后点击finish完成时,文件夹Hardware_Accelerated_Execution_Manager居然被删除了,然后把Hardware_Accelerated_Execution_Manager-2重新命名为Hardware_Accelerated_Execution_Manager,重新启动了一下Android Studio,发现显示已经安装,自此解决安装时重名问题

1.4.3.2.安装emulator

安装上之后,准备使用AVD模拟器的时候发现no emulator installed的错误,于是将sdk目录下的emulator文件夹删除了,再进行安装emulator发现就可以识别了

1.4.3.2.启动

启动过程中遇到了很多问题,一直报错The emulator process for AVD Pixel_2_API_29 has terminated.,这时候我们需要查看日志信息

日志中看到了如下信息,大致意思就是缺少vulkan-1.dll文件,于是在网上搜到了解决方案

2021-07-01 18:00:48,415 [  62545]   INFO - manager.EmulatorProcessHandler - Emulator: cannot add library vulkan-1.dll: failed
2021-07-01 18:00:48,415 [  62545]   INFO - manager.EmulatorProcessHandler - Emulator: cannot add library vulkan-1.dll: failed
2021-07-01 18:00:48,668 [  62798]   INFO - manager.EmulatorProcessHandler - Emulator: emulator: Android emulator version 30.7.5.0 (build_id 7491168) (CL:N/A)
2021-07-01 18:00:48,676 [  62806]   INFO - manager.EmulatorProcessHandler - Emulator: Process finished with exit code -1073741819 (0xC0000005)
2021-07-01 18:00:48,676 [  62806]   WARN - manager.EmulatorProcessHandler - Emulator terminated with exit code -1073741819

stackoverflow解决方法:

  • https://stackoverflow.com/questions/65696048/android-studio-emulator-cannot-add-library-vulkan-1-dll-failed

本人是在创建了advanceFeature.ini之后在里面加入如下代码禁用vulkan,然后重新启动Android Studio即可以连接

Vulkan = off
GLDirectMem = on

1.4.5.创建新项目

由于项目全由react-native来搭建,所以直接使用搭建命令即可,如果是进行android+react-native混合开发,请参考react-native官方文档

# 安装
npm install - g react-native-cli
# 创建项目
npx react-native init lls

1.4.6.编译运行项目

确保你先运行了模拟器或者连接了真机,然后在你的项目目录中运行yarn android或者yarn react-native run-android

# 进入目录
cd lls
# 启动
yarn android
# 或者
yarn react-native run-android

此时如果没有关掉代理软件的话会出现红屏现象,可通过关掉代理软件解决

由于需要安装相关依赖,也可以使用Android Studio来进行,使用Android Studio打开新建的项目中的android目录,点击运行按钮则可以自动安装

注意:打开的是android目录,如果不是此目录,则并不会安装相关依赖

2.开发项目

此次项目开发主要用到的技术栈有:

  • react-native
  • react-redux
  • react-saga

其中react-native进行与android进行交互,react-redux进行状态管理,react-saga进行异步处理

主要插件有:

  • react-native-ui-lib(图标插件)
  • lottie-react-native(动画插件)
  • react-native-audio-recorder-player(语音播放插件)
  • react-native-spinkit(提示插件)

项目目录

2.1.react-redux

简介ReduxJavaScript 状态容器,提供可预测化的状态管理。可以理解为全局数据状态管理工具,用来做组件通信,当没有使用 redux 时兄弟组件间传值将很麻烦,代码很复杂冗余。使用 redux 定义全局单一的数据 Store,可以自定义 Store 里面存放哪些数据,整个数据结构会更加清晰

redux原理图:

相关资源

  • 中文redux网站

  • redux搭建参考博客

# 注意redux版本
yarn add redux@4.1.1
yarn add react-redux

2.2.react-saga

简介:react-saga主要用途在于进行异步处理,截获redux中的action状态,进行websocket连接和http请求等,做完异步操作之后再进行dispatch转发,reducers捕获并更新状态

# 异步触发
yarn add redux-saga
  1. 在redux配置文件中配置redux-saga
// reduxConfig.js
// redux库里面提供的方法,创建store和middleware中间件
import {createStore,applyMiddleware,compose } from 'redux';
// redux-logger打印logger的中间件,具体效果可以看下图
import {createLogger} from 'redux-logger';
// rootReducer下一步会创建
import RootReducer from './RootReducer';
import createSagaMiddleware, { END } from 'redux-saga'

export default function ConfigureStore(initialState){
    const sagaMiddleware = createSagaMiddleware() // 初始化
    const store = createStore(
        RootReducer,//reducer模块 下面会创建并解释
        initialState,//state初始值
        compose (
            applyMiddleware(createLogger(),sagaMiddleware) // 添加
        )
    )
    store.runSaga = sagaMiddleware.run
    store.close = () => store.dispatch(END)
    return store
}
  1. 创建sagasConfig/index.js,写入要进行异步操作的函数并导出
import { take, put, call, fork, select, all,takeEvery,takeLatest, delay} from 'redux-saga/effects'
import * as Types from "../reduxConfig/ActionTypes"
import {api} from "../services"

// 处理任务函数
function* fetchStatusCode(act) {
  // console.log("fetchStatusCode",act)
  sts = act["status"]
  if (sts){
    // 异步处理 
    // put相当于dispatch,可以让redux store中更新
    yield put(Types.updateStatusCode.update(sts));
  }   
}
// CHANGE_STATUS_CODE为redux进行dispatch的action,这里的takeEvery是捕获每个CHANGE_STATUS_CODE任务,然后再在fetchStatusCode函数中进行处理
function* changeStatusCode() {
  // 读取文件
  yield takeEvery(Types.CHANGE_STATUS_CODE,fetchStatusCode)
}
// 默认导出
export default function* root() {
  yield all([
    fork(changeStatusCode)
  ])
}
  1. reduxConfig/ActionTypes.js存放转发类型和进行saga进行异步之后callback调用的函数更新redux store
export const CHANGE_STATUS_CODE = "CHANGE_STATUS_CODE"
export const UPDATE_STATUS_CODE = "UPDATE_STATUS_CODE"

function action(type, payload = {}) {
    return {type, ...payload}
}
// 转发action
export const updateStatusCode = {
    update: (status) => action(UPDATE_STATUS_CODE,{status}),
}
  1. 在App.js中配置saga和运行saga
import * as React from 'react';
import { View, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HelloWorld from './MainInterface';
import ConfigureStore from './reduxConfig/ConfigStore';
import { Provider } from 'react-redux'
import rootSaga from './sagasConfig'
import {LogBox } from 'react-native';

LogBox.ignoreLogs(['Reanimated 2']);
// 从reduxConfig.js中导出的store
store = ConfigureStore()
// 执行store.runSaga方法,其中rootSaga为sagasConfig/index.js中内容
store.runSaga(rootSaga)

 function App() {
   return (
    
      ...
     
   );
 }
 
 export default App;
  1. 在组件中调用dispatch函数,调试saga是否捕获action
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import {
changeStatusCode
} from "./reduxConfig/CountAction"
class HelloWorld extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0,
      anim: new Animated.Value(0),
      deviceWidth: 100,
    }
  }
    
    render(){
        return 
    }
}

function mapStateToProps(state) { // 将state传给props
  return {
    fetch_result_status: state["Counter"]["fetch_result_status"],
  }
}

function matchDispatchToProps(dispatch) {
  return bindActionCreators({
    changeStatus: changeStatusCode,
  }, dispatch); // 绑定动作事件
}

export default connect(mapStateToProps, matchDispatchToProps)(HelloWorld); // 连接Redux

注意:如果是在进行异步http请求时,由于android9.0之后禁用了http请求,需要配置android安全认证

  • 配置res/xml/network_security_config.xml
  • 修改AndroidManifest.xml
//network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>

    


//AndroidManifest.xml
 ......
 android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:networkSecurityConfig="@xml/network_security_config" // 添加此字段
      android:usesCleartextTraffic="true" // 添加此字段
      android:theme="@style/AppTheme">
      2.3.react-native-ui-lib

简介:此库主要提供一些UI组件,如卡片,抽屉等等

# 安装UI
yarn add react-native-ui-lib
# 安装触摸方式,最好不要yarn和npm混用,防止版本出现问题,安装1.10.3版本的,别的版本会出现问题
yarn add react-native-gesture-handler@1.10.3
# 这里的react-native最好不要安装在全局,如果要在命令行运行包的话,前面加yarn就好
yarn react-native link react-native-gesture-handler

运行时报错:unable-to-resolve-module-react-native-reanimated

  • 解决办法

解释:由于react-native-reanimated报错,经过查找node_modules中内容,并没有发现有适配0.66版本的react-native的文件,所以决定不使用react native 0.66版本,降低到0.65.1,并且react-native-reanimated的版本也必须为2.2.2,不然会报错

yarn remove react-native@0.66.3
yarn add react-native@0.65.1
yarn add react-native-reanimated@2.2.2

2.4.lottie-react-native

简介:此库主要用来加载AE动画,Airbnb出品、开源。Lottile组件库本来是适用于Android和iOS平台。大体流程为:

  • 使用Adobe After Effects软件做出特效动画

  • 通过bodymovin项目工具把特效动画采用JSON格式文件进行导出

  • 用Lottie库进行解析JSON文件并且在移动端上面渲染效果即可

通过查看github中的版本要求,由于我的环境中装的是react-native@0.65版本,所以选择安装lottie-react-native @4.1.3,如果报错很有可能是版本不匹配,请检查版本

yarn add lottie-react-native@4.1.3

然后是动画json文件的获取,这里直接找到免费的动画下载地址,找到自己喜欢的动画,然后下载json文件

  • 免费动画插件地址

使用示例:

import LottieView from 'lottie-react-native';
class HelloWorld extends Component {

 render() {
    return (
        
        	...
            
			...
      
    )
  }
}

2.5.react-native-audio-recorder-player

简介:此插件主要用来播放声音和录音然后将录音上传服务端

相关资源

  • git官方地址
  • 如何清除界面log信息

错误信息

Could not resolve all artifacts for configuration ':classpath'
Could not download kotlin-util-klib-1.5.0.jar (org.jetbrains.kotlin:kotlin-util-klib:1.5.0)

原因分析:主要原因是网络不好,下载不了,这个问题是经常出现的,有时候需要更新代理地址,例如更新代理地址为新加坡或其他地方

2.6.react-native-spinkit

简介:此组件主要是弹窗提示组件,用于弹窗提醒用户操作是否合规

相关资源

  • GitHub官网

2.7.使用示例

相关资源

  • react native实现仿微信语音功能

此组件实现类似于微信语音,参考以上链接进行开发:

  • 过程中发现react-native-audio已经停更很久,所以改用react-native-audio-recorder-player来实现录音功能
  • 弹窗组件由teaset改用react-native-spinkit
// Recoder.js
import React, { Component } from 'react'
import { View, Text } from 'react-native-ui-lib';
import LottieView from 'lottie-react-native';
import {
  StyleSheet,
  Dimensions,
  PermissionsAndroid,
  Platform,
  Alert,
} from 'react-native'
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import {
  transferVoicePath,
  changeStatusCode
} from "./reduxConfig/CountAction"
import AudioRecorderPlayer from 'react-native-audio-recorder-player';
import Toast from 'react-native-root-toast';
import Ionicons from 'react-native-vector-icons/Ionicons';
import Spinner from "react-native-spinkit";
import RNFetchBlob from 'rn-fetch-blob'

const dirs = RNFetchBlob.fs.dirs;
const path = Platform.select({
  ios: 'hello.m4a',
  android: `${dirs.CacheDir}/hello.mp3`,
});


class Recoder extends Component {
  constructor(props) {
    super(props)
    this.state = {
      disabled: false,
      isLoggingIn: false,
      recordSecs: 0,
      recordTime: '00:00:00',
      currentPositionSec: 0,
      currentDurationSec: 0,
      playTime: '00:00:00',
      duration: '00:00:00',
      mp3Path: "",
      buttonColor: "#0000FF"
    }
    this.audioRecorderPlayer = new AudioRecorderPlayer();
    this.audioRecorderPlayer.setSubscriptionDuration(0.1); // optional. Default is 0.5
  }

  componentDidMount = () => {
    console.log("Recoder componentDidMount")
    // 加载text文件列表
    this._checkPermission()
    this.Gesture = {
      onStartShouldSetResponder: (evt) => true,
      onMoveShouldSetResponder: (evt) => true,
      onResponderGrant: (evt) => {
        // 获取触摸权限
        if (!this.state.hasPermission) {
          Alert.alert("jurisdiction")
        }
        this.setState({
          // opacity: "#c9c9c9",
          recordingText: '正在录音中...',
          text: 'releaseEnd',
          icon: "ios-mic",
          recordingColor: 'transparent',
          buttonEnable: false,
          buttonColor: "#8B8989",
        }, _ => RecordView.show(this.state.recordingText, this.state.recordingColor, this.state.icon));
        // this._record();
        // 改变状态码
        this.props.changeStatus("loading")
        this.onStartRecord();
      },
      onResponderReject: (evt) => {
      },
      onResponderMove: (evt) => {
        let UpperDistance = Dimensions.get('window').height * 0.25 || 0
        // console.log("onResponderMove",this.recordPageY,evt.nativeEvent.pageY,UpperDistance)
        if (evt.nativeEvent.pageY < this.recordPageY - UpperDistance) {
          if (this.state.recordingColor != 'red') {
            this.setState({
              recordingColor: "red",
              buttonEnable: true,
              buttonColor: "#0000FF"
            })
            this.setRecordView('取消发送', 'red', "ios-mic-off")
            this.onStopRecord(true)
          }
        } else if (this.state.recordingColor != 'transparent') {
          this.setRecordView('正在录音中...', 'transparent', "ios-mic")
        }
      },
      onResponderRelease: (evt) => {
        // 松开手指
        this.setState({
          // opacity: "white",
          text: "holdToTalk",
          buttonEnable: true,
          buttonColor: "#0000FF"
        });
        RecordView.hide();
        let canceled;
        if (evt.nativeEvent.locationY < 0 ||
          evt.nativeEvent.pageY < this.recordPageY) {
          canceled = true;
        } else {
          canceled = false;
        }
        if (this.state.recordingColor != "red") {
          this.onStopRecord(false)
          this.setRecordView('结束录音', 'red', "ios-mic-off")
        }
        // 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));
  }

  _cancel = (canceled) => {
    // let filePath = this._stop();
    if (canceled) return;
    if (this.state.currentTime < minTime) {
      this.setRecordView('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) {
    }
  }

  _checkPermission = async () => {
    // 检查是否满足权限
    if (Platform.OS === 'android') {
      try {
        const grants = await PermissionsAndroid.requestMultiple([
          PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
          PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
          PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
        ]);

        console.log('write external stroage', grants);

        if (
          grants['android.permission.WRITE_EXTERNAL_STORAGE'] ===
          PermissionsAndroid.RESULTS.GRANTED &&
          grants['android.permission.READ_EXTERNAL_STORAGE'] ===
          PermissionsAndroid.RESULTS.GRANTED &&
          grants['android.permission.RECORD_AUDIO'] ===
          PermissionsAndroid.RESULTS.GRANTED
        ) {
          console.log('permissions granted');
          this.setState({
            hasPermission: true
          })
        } else {
          console.log('All required permissions not granted');
          return;
        }
      } catch (err) {
        console.warn(err);
        return;
      }
    }
  }

  onStartRecord = async () => {
	// 开始录音
    console.log("Recoder onStartRecord")
    const result = await this.audioRecorderPlayer.startRecorder(path);
    this.audioRecorderPlayer.addRecordBackListener((e) => {
      return;
    });
    // result返回音频存储地址
    console.log(result);
  };

  onStopRecord = async (abnormal) => {
    console.log("onStopRecord")
    const result = await this.audioRecorderPlayer.stopRecorder();
    this.audioRecorderPlayer.removeRecordBackListener();
    this.setState({
      recordSecs: 0,
      mp3Path: result
    });
    console.log(result);
    if (!abnormal) {
      if (this.props.text_id >= 0) {
        this.props.getVoicePath(result, this.props.text_id)
      }
    }
    // this.onStartPlay()
  };
	
  onStartPlay = async () => {
    // 播放音频,传入服务器静态文件音频地址
    console.log('Recoder onStartPlay');
    path = "http://121.12.135.153:8039/pronunciation_accessement/static/Track7.mp3"
    const msg = await this.audioRecorderPlayer.startPlayer(path);
    console.log(msg);
    this.audioRecorderPlayer.addPlayBackListener((e) => {
      this.setState({
        currentPositionSec: e.currentPosition,
        currentDurationSec: e.duration,
        playTime: this.audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)),
        duration: this.audioRecorderPlayer.mmssss(Math.floor(e.duration)),
      });
      return;
    });
  };

  onPausePlay = async () => {
    await this.audioRecorderPlayer.pausePlayer();
  };

  onStopPlay = async () => {
    console.log('Recoder StopPlay');
    this.audioRecorderPlayer.stopPlayer();
    this.audioRecorderPlayer.removePlayBackListener();
  };

  handleLayout = () => {
	// 获取当前组件位置信息
    this.record.measure((fx, fy, width, height, px, py) => {
      console.log("handleLayout", px, py)
      this.recordPageX = px;
      this.recordPageY = py;
    })
  }

  render() {
    const { buttonColor } = this.state
    return (
      
         this.record = record}
          underlayColor='#fff'
        >
          按下说话
          
          {/*  */}
        
      
    )
  }
}

// 使用react-native-spinkit实现窗口提示
class RecordView {
  static key = null;
  static show(text, color, icon) {
    let showIcon;
    if (RecordView.key) RecordView.hide()
    if (color == 'red') {
      showIcon = ()
      // showIcon = ()
    } else if (icon == 'short') {
      showIcon = ()
      // showIcon = ()
    } else {
      showIcon = (
        <>
          
          {/*  */}

          
        
      );
    }
    let durationTime = 1
    if(Toast.durations.LONG){
      durationTime = Toast.durations.LONG
    }
    if (icon == "red") {
      durationTime = Toast.durations.SHORT || 1
    }
    // 通过调用 Toast.show(message, options); 可以在屏幕上显示一个toast,并返回一个toast实例
    RecordView.key = Toast.show(text, {
      duration: durationTime, // toast显示时长
      position: Toast.positions.CENTER, // toast位置
      shadow: true, // toast是否出现阴影
      animation: true, // toast显示/隐藏的时候是否需要使用动画过渡
      hideOnPress: true, // 是否可以通过点击事件对toast进行隐藏
      delay: 0, // toast显示的延时
    })
  }

  static hide() {
    if (!RecordView.key) return;
    Toast.hide(RecordView.key);
    RecordView.key = null;
  }
}

const styles = StyleSheet.create({
  viewContainer: {
    flex: 2,
    width: "80%",
    // alignItems: "center",
    // height:"100%",
    padding: 10,
    marginBottom: 10,
    // 组件内垂直对齐
    justifyContent: 'center',

    // borderRadius:0,
  },
  subViewContainer: {
    flex: 1,
    maxHeight: 50,
  },
  backgroundImage: {
    width: "100%",
    height: "100%"
  },
  TextView: {
    flex: 1,
    borderRadius: 5,
    alignItems: "center",
    maxHeight: 50,
    justifyContent: 'center',
    marginTop: 10,
    marginBottom: 10,
  },
  TextContainer: {
    color: "#FFFAFA",
    fontSize: 20,
    fontFamily: "times-new-roman",
  },
  TouchableHighlight: {
    flex: 1,
    backgroundColor: "#0000FF",
    // maxHeight:50,
  }
})

function mapStateToProps(state) {
  return {
    text_id: state["Counter"]["textId"],
  };
}

function matchDispatchToProps(dispatch) {
  return bindActionCreators({
    getVoicePath: transferVoicePath, // 录音完成后触发此函数将音频所在URI地址传给saga进行异步http请求
    changeStatus: changeStatusCode // 改变全局状态
  }, dispatch); // 绑定动作事件
}

export default connect(mapStateToProps, matchDispatchToProps)(Recoder); // 连接Redux

3.打包发布

打包发布请参考:

  • react-native android打包发布
cd android
# CMD窗口下直接gradlew assembleRelease
./gradlew assembleRelease

4.总结

本此开发主要是响应业务需求,开发英语测评APP的demo,考虑到对js比较熟悉,所以使用react-native进行开发,此次开发主要参考react中文文档进行配置和开发,总结开发中遇到的各种问题,以及框架的搭建基本流程,欢迎大家指正