一、将.pk8 和.pem 转换成 react-native 的 debug.keystore
Step1. 安装 openssl
参考:https://stackoverflow.com/questions/42918916/npm-install-openssl-failed-on-windows-10
Step2. 把 pkcs8 格式的私钥转换为 pkcs12 格式,生成 platform.priv.pem 文件
openssl pkcs8 -in platform.pk8 -inform DER -outform PEM -out platform.priv.pem -nocrypt
Step3. 生成 pkcs12 格式的密钥文件,生成 platform.pk12 文件,最后的 brilliance 是 keystore 的 alias,需要输入两次密码,我们这里默认为 android
openssl pkcs12 -export -in platform.x509.pem -inkey platform.priv.pem -out platform.pk12 -name brilliance
Step4. 生成 platform.keystore
keytool -importkeystore -deststorepass android -destkeypass android -destkeystore platform.keystore -srckeystore platform.pk12 -srcstoretype PKCS12 -srcstorepass android -alias brillianc
二、React-Native 重命名 package & 重命名 app
1. 重命名 package
Step1. 重命名文件夹
android/app/src/main/java/MY/APP/OLD_ID/ 重命名为: android/app/src/ main/java/MY/APP/NEW_ID/
这里的 NEW_ID 也可能是多级文件夹,例如: com/fungmo/a08
Step2. 配置包 ID
1.在 android/app/src/main/java/MY/APP/NEW_ID/MainActivity.java 中:
package MY.APP.NEW_ID; //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
2.在 android/app/src/main/java/MY/APP/NEW_ID/MainApplication.java 中:
package MY.APP.NEW_ID; //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
import MY.APP.NEW_ID.generated.BasePackageList; //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
Class<?> aClass = Class.forName("MY.APP.NEW_ID.ReactNativeFlipper"); //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
3.在 android/app/src/main/AndroidManifest.xml 中:
package="MY.APP.NEW_ID" //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
4.在 android/app/build.gradle 中:
applicationId "MY.APP.NEW_ID" //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
5.在 android/app/BUCK 中:
android_build_config(
package="MY.APP.NEW_ID" //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
)
android_resource(
package="MY.APP.NEW_ID" //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
)
Step3. 最后进行 Gradle 清理(在 /android 文件夹中):
gradlew clean //cmd
//或者
./gradlew clean //powershell
2、重命名 app
生成器不会覆盖位于 android/app/src/main/res/values/ 文件夹中的 strings.xml 文件,因此必须 手动更改 app_name 变量
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources>
<string name="app_name">用户手册</string>
</resources>
三、RN 启动屏
npm i react-native-splash-screen --save
Step1.
转到app/src/main/java/[packageName]并创建一个新文件SplashActivity.java然后将以下代码复制粘贴到其中。
package com.packagename; // Replace this with your package name 替换为自己的 package 名
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
}
Step2.
去app/src/main/AndroidManifest.xml和修改它,如下所示使用SplashActivity: 在<application>标签内添加以下活动。
<activity
android:name=".SplashActivity"
android:theme="@style/SplashTheme"
android:label="@string/app_name"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
从MainActivity标签中删除以下意图。
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
并添加android:exported="true"该活动。 现在,您的AndroidManifest.xml应该如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.packagename">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:allowBackup="false"
android:theme="@style/AppTheme">
<activity
android:name=".SplashActivity"
android:theme="@style/SplashTheme"
android:label="@string/app_name"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true"
>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
Step3.
现在,我们将声明SplashThemefor SplashActivity。转到app/src/main/res/values/styles.xml并在<resources>中添加以下样式。
<style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:background">@drawable/background_splash</item>
<item name="android:statusBarColor">@color/background</item>
</style>
Step4.
转到android\app\src\main\res\values并创建一个文件(colors.xml如果尚不存在)。 我们在上面使用了背景颜色常量,因此必须将其添加到colors.xml文件中。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Insert your background color for the splash screen -->
<color name="background">#fff</color>
</resources>
Step5.
转到android/app/src/main/res/drawable(如果尚不存在,则创建drawable文件夹)并将您的启动屏幕图像(名称应为splash_screen.png)放在此处,并background_splash.xml使用以下代码创建文件:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/background" />
<item
android:drawable="@drawable/splash_screen"
android:height="300dp"
android:width="300dp"
android:gravity="center"
/>
</layer-list>
如果您的初始屏幕的尺寸是等于设备屏幕的尺寸,在<item>标签中删除android:height和android:width。
Step6.
react-native-splash-screen在您的项目中安装模块,然后SplashScreen从 App.js 文件中导入它。 import SplashScreen from 'react-native-splash-screen'; 我们只需要显示初始屏幕,直到安装第一个组件,然后useEffect在 App 组件主体内(返回之前)制作一个钩子,如下所示: 不要忘了import useEffect from 'react'。
useEffect(() => {
SplashScreen.hide();
}, []);
Step7.
转到app/src/main/java/[packageName]/MainActivity.java并导入以下模块,然后导入其他模块。
import org.devio.rn.splashscreen.SplashScreen;
import android.os.Bundle;
将此方法添加到MainActivity类的顶部。
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.show(this, R.style.SplashStatusBarTheme);
super.onCreate(savedInstanceState);
}
Step8.
去android/app/src/main/res/values/styles.xml添加SplashStatusBarTheme,就像我们在第 3 步一样。
<style name="SplashStatusBarTheme" parent="SplashScreen_SplashTheme">
<item name="android:statusBarColor">@color/background</item>
</style>
如果不这样做,则在加载应用程序的 JS 代码时,StatusBar 的颜色将变为黑色。
Step9.
转到android/app/src/main/res/并创建一个新文件夹layout(如果尚不存在)。在该文件夹中,创建一个文件launch_screen.xml(,需要此文件react-native-splash-screen library)。在该文件内,使用以前创建的背景创建布局,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_splash"
/>
Step10.
·android/app/src/main/res/values/colors.xml·像在步骤 4 中一样转到并添加以下标签,否则,该应用程序将崩溃。不要更改颜色值。
<color name="primary_dark">#000</color>
四、ReactNative 签名打包 apk(android)
step1. 生成一个签名密钥
C:\Program Files\Java\jdk1.8.0_271\bin 目录中,执行
keytool -genkeypair -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
生成 my-release-key.keystore 密钥库文件
Step2. 设置
gradle变量
1.把 my-release-key.keystore 文件放到你工程中的 android/app 文件夹下。 2.编辑项目目录 /android/gradle.properties 如果没有 gradle.properties 文件你就自己创建一个,添加如下的代码(注意把其中的**替换为相应密码)
MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_STORE_PASSWORD=*****
MYAPP_RELEASE_KEY_PASSWORD=*****
Step3. 把签名配置加入到项目的
android/app/build.gradle配置中
...
android {
...
defaultConfig { ... }
signingConfigs {
+ release {
if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
storeFile file(MYAPP_RELEASE_STORE_FILE)
storePassword MYAPP_RELEASE_STORE_PASSWORD
keyAlias MYAPP_RELEASE_KEY_ALIAS
keyPassword MYAPP_RELEASE_KEY_PASSWORD
}
}
}
buildTypes {
release {
...
+ signingConfig signingConfigs.release
}
}
}
...
step4. 生成发行 APK 包
项目 android 目录中,执行
gradlew assembleRelease //cmd 或者
./gradlew assembleRelease //PowerShell
生成的 APK 文件位于 android/app/build/outputs/apk/release/app-release.apk
如果需要重新打包,需先删除 \android\app\build\outputs\apk 中的 release 文件夹
step5. 测试发行版本
npx react-native run-android --variant=release
五、RN/Expo 渲染本地图片(资源)列表
由于 RN 中引用本地图片是用require,而 require 是运行时编译的,所以不能在 require 中添加变量。参考 Link
Step1. 将所需要的本地图片 Require 到一处
const image1 = require('../assets/Image1.png')
const image2 = require('../assets/Image2.png')
Step2. 创建一个数组对象
const data = [
{"id":1, "url": image1},
{"id":2, "url": image2}
]
Step3. 这样就可以使用 map 渲染 data 数据并正确使用本地图片了
const listItems = data.map(item =>
<View key={item.id}>
<Image source={item.url} />
</View>
)
六、Lottie(lottie-react-native)
Lottie是以json格式导出的Adobe After Effects动画库,并在移动设备和Web上渲染。
本文只介绍RN安卓端的配置。
一. 安装 (React Native >= 0.60.0)
yarn add lottie-react-native
二. 配置文件(如果应用在Android上崩溃,则表示自动链接无效。才需要进行以下配置:)
//1. android/app/src/main/java/<AppName>/MainApplication.java
// 在文件入口(头部),添加
import com.airbnb.android.react.lottie.LottiePackage;
// 在List <ReactPackage> getPackages()中,添加
packages.add(new LottiePackage());
//2. android/app/build.gradle
// 在 dependencies 块中,添加
implementation project(':lottie-react-native')
//3. android/settings.gradle
include ':lottie-react-native'
project(':lottie-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/lottie-react-native/src/android')
三. 使用
import React from 'react';
import LottieView from 'lottie-react-native';
export default class BasicExample extends React.Component {
render() {
return <LottieView source={require('./assets/my.json')} autoPlay loop />;
}
}
// 其中,assets 是手动在项目根目录中创建的文件夹,这里的my.json即是lottie动画文件
动画文件:lottiefiles
四. 安装完新依赖/新添文件后需要重新编译到安卓设备中,yarn android
七、RN表单验证
这里纯手写,当然也可以使用react-hooks-form等第三方库。
注意:select 的处理方式
import React, { Component } from "react";
import { StyleSheet, KeyboardAvoidingView, TouchableWithoutFeedback, Keyboard } from "react-native";
import { Layout, Input, Button, Select, SelectItem, IndexPath } from "@ui-kitten/components";
const validEmailRegex = RegExp(
/^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
);
const validateForm = errors => {
let valid = true;
Object.values(errors).forEach(val => val.length > 0 && (valid = false));
return valid;
};
const data = [//1.select初始数据
'A',
'B',
'C',
];
export default class FormComponent extends Component {
constructor(props) {
super(props);
this.state = {
selectedIndex: new IndexPath(0),//2.select初始选中
fullName: null,
email: null,
password: null,
errors: {
fullName: "",
email: "",
password: ""
}
};
}
handlePress = () => {
if (this.isNotEmpty()) {
if (validateForm(this.state.errors)) {
Keyboard.dismiss();
// alert("Created successfully.");
const postData = {
"typeFeedBack":data[this.state.selectedIndex.row],//3.select选中数据
}
console.log(postData)
}
} else {
validateForm(this.state.errors);
}
};
handleChange = (field, value) => {
let errors = this.state.errors;
switch (field) {
case "fullName":
errors.fullName = value.length < 5 ? "Full Name must be 5 characters long!" : "";
break;
case "email":
errors.email = validEmailRegex.test(value) ? "" : "Email is not valid!";
break;
case "password":
errors.password = value.length < 8 ? "Password must be 8 characters long!" : "";
break;
default:
break;
}
this.setState({ errors, [field]: value });
};
isNotEmpty = () => {
const { fullName, email, password } = this.state;
let isNoError = true;
if (!fullName) {
this.setState(prevState => ({
errors: {
...prevState.errors,
fullName: "Full Name is required."
}
}));
isNoError = false;
}
if (!email) {
this.setState(prevState => ({
errors: {
...prevState.errors,
email: "Email Address is required."
}
}));
isNoError = false;
}
if (!password) {
this.setState(prevState => ({
errors: {
...prevState.errors,
password: "Password is required."
}
}));
isNoError = false;
}
return isNoError;
};
render() {
const { selectedIndex, fullName, email, password, errors } = this.state;
const displayValue = data[selectedIndex.row];//4.select选中的值
const renderOption = (title: string, index: number) => (//5.select option
<SelectItem key={index} title={title} />
);
return (
<KeyboardAvoidingView behavior="padding" style={{ flex: 1 }}>
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<Layout style={styles.container}>
<Select //6. select组件渲染
selectedIndex={selectedIndex}
onSelect={(index) => this.setState({ selectedIndex: index })}
value={displayValue}
>
{data.map(renderOption)}
</Select>
<Input
value={fullName}
label="Full Name"
captionTextStyle={styles.captionTextStyle}
caption={errors.fullName.length > 0 && errors.fullName}
status={errors.fullName.length > 0 ? "danger" : ""}
onChangeText={value => this.handleChange("fullName", value)}
/>
<Input
value={email}
label="Email Address"
keyboardType="email-address"
autoCapitalize="none"
captionTextStyle={styles.captionTextStyle}
caption={errors.email.length > 0 && errors.email}
status={errors.email.length > 0 ? "danger" : ""}
onChangeText={value => this.handleChange("email", value)}
/>
<Input
value={password}
label="Password"
secureTextEntry
captionTextStyle={styles.captionTextStyle}
caption={errors.password.length > 0 && errors.password}
status={errors.password.length > 0 ? "danger" : ""}
onChangeText={value => this.handleChange("password", value)}
/>
<Button style={styles.btn} onPress={this.handlePress}>
Submit
</Button>
</Layout>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 10,
width: 600
},
captionTextStyle: {
color: "red"
},
btn: {
marginVertical: 5
}
});
react-hooks-form 使用示例(包含select)
import React from "react";
import { StyleSheet } from "react-native";
import { Layout, Input, Button, Select, SelectItem, IndexPath, Text } from "@ui-kitten/components";
import { useForm, Controller } from "react-hook-form";
const selectData = [
'A',
'B',
'C',
];
export default function FormComponent() {
const { control, handleSubmit, errors } = useForm();
const onSubmit = (data: any) => {
const postData = {
"typeFeedBack": displayValue,//select选的值
"firstName": data.firstName,
"lastName": data.lastName
}
console.log(postData)
};
const [selectedIndex, setSelectedIndex] = React.useState(new IndexPath(0));
const displayValue = selectData[selectedIndex.row];
const renderOption = (title: string, index: number) => (
<SelectItem key={index} title={title} />
);
return (
<Layout style={styles.container} level='4'>
<Controller
control={control}
render={() => (
<Select
style={styles.input}
placeholder='Default'
value={displayValue}
selectedIndex={selectedIndex}
onSelect={index => setSelectedIndex(index)}>
{selectData.map(renderOption)}
</Select>
)}
name="typeFeedBack"//这里这个名字不重要,只要随便取一个没有的就可以。因为select我要单独处理
defaultValue=""//必须,可以为空
/>
<Controller
control={control}
render={({ onChange, onBlur, value }) => (
<Input
style={styles.input}
label='姓氏'
onBlur={onBlur}
onChangeText={value => onChange(value)}
value={value}
/>
)}
name="firstName"
rules={{ required: true }}
defaultValue=""
/>
{errors.firstName && <Text>This is required.</Text>}
<Controller
control={control}
render={({ onChange, onBlur, value }) => (
<Input
style={styles.input}
label='名字'
onBlur={onBlur}
onChangeText={value => onChange(value)}
value={value}
/>
)}
name="lastName"
rules={{ required: true }}
defaultValue=""
/>
{errors.lastName && <Text>This is required.</Text>}
<Button style={styles.button} onPress={handleSubmit(onSubmit)}>提交</Button>
</Layout>
);
}
const styles = StyleSheet.create({
container: {
paddingLeft: 20,
paddingRight: 20,
paddingBottom: 20
},
input: {
width: 500,
marginTop: 20
},
button: {
marginTop: 20
}
})