React Native入门详解–实战开发中常见的需求

我心飞翔 分类:javascript

本文中主要介绍

  • RN开发-应用适配方案
  • RN开发-路由解决方案
  • RN开发-如何实现渐变色
  • ......

该教程默认你了解RN组件的基本使用。
RN组件的基本使用:juejin.cn/post/694537…

正餐开始

1、优化目录接口

在项目开发时,我们对项目的目录结构进行优化。

饿1312.jpg

新增目录:

  • api:http请求模块,封装了应用中所有请求,便于开发复用和后期维护
  • components:公共组件,应用的公共组件都放在这个目录中
  • asstes:公共资源(图片和样式),应用本地的图片都放在这个目录中
  • views:视图模块,应用所有的页面组件都放在这里
  • utils:工具模块,应用中用的工具函数都放到这里
  • router:路由模块,用来配置的应用的路由

2、组件的适配

RN中组件单位默认为dp,而设计图通常是px.在开发过程中,势必会遇上屏幕适配(ios 好几种尺寸的屏幕以及 android 各种尺寸的屏幕)的问题。

核心原理

虽然设备屏幕尺寸是不一致的,但是组件尺寸屏幕宽度比例是固定的,这个比例和效果图元素尺寸效果图宽度是一致:
效果图元素尺寸(px) / 效果图宽度(px) = 组件尺寸(dp)/屏幕宽度(dp)
即:
组件尺寸(dp) = 效果图元素尺寸(px) * 屏幕宽度(dp) / 效果图宽度(px)

适配工具

在utils中新建pxTodp.js

// 用来完成适配的工具
import { Dimensions } from 'react-native';

// 设备宽度,单位 dp
const deviceWidthDp = Dimensions.get('window').width;

// 设计稿宽度(这里为640px),单位 px
const uiWidthPx = 540;

// px 转 dp(设计稿中的 px 转 rn 中的 dp)
export default (uiElePx) => {
  return uiElePx * deviceWidthDp / uiWidthPx;
}
 

3、路由导航

第三方库React Navigation应该是实现RN路由导航最成熟的方案了,用它准没错。
React Navigation官网:reactnavigation.org/

React Navigation路由导航模式

React Navigation提供了tabNavigatorstackNavigatorDrawerNavigator三种路由导航模式,因为第三种抽屉式导航路由很少用到,这里主要介绍前两种。

React Naigation核心依赖

  • react-native-gesture-handler:用于手势切换页面。
  • react-native-screens:用于原生层释放未展示的页面,改善 app 内存使用。
  • react-native-safe-area-context: 用于保证页面显示在安全区域(主要针对刘海屏)。
  • @react-native-community/masked-view:用在头部导航栏中返回按钮的颜色设置。
  • @react-navigation/native:为 React Navigation 的核心。
  • react-native-reanimated:抽屉式路由导航DrawerNavigator的核心依赖,但是官网将它列入核心的依赖中还是安上吧

开始安装:

yarn add @react-navigation/native
yarn add react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
 

TabNavigator

如果需要实现向微信这种点击屏幕底部的tab栏实现路由切换,就可以使用tabNavigator.
wx.jpg

基础配置

  • 1、安装Tabnavigator相关依赖
yarn add @react-navigation/bottom-tabs
 
  • 2、将导航路由容器组件挂载到页面上(router/index.js)
import React, { Component } from 'react';
import { NavigationContainer } from '@react-navigation/native';
function App() {
  return (
    <NavigationContainer>
        ...放入tab导航路由/stack导航路由
    </NavigationContainer>
  );
}
export default App;
 
  • 3、配置底部导航器和导航视图(router/tabNavigator.js)
import React, { Component } from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
// 导入路由视图组件
import LifeScreen from '../views/life/index';
import AmuseScreen from '../views/amuse';
import HomeScreen from '../views/home/index';
import ServiceScreen from '../views/service';
import MineScreen from '../views/mine';
// 导入适配工具
import pxTodp from '../utils/adaptive.js'
// 创建一个tab导航器
const Tab = createBottomTabNavigator();
export default function TabNavigator() {
return (
{/* tab导航器组件 */}
<Tab.Navigator 
{/* 默认被激活的路由 */}
initialRouteName="Home"
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, size }) => {
let sourceImg;
{/* 切换被激活的tab页 */}
if (route.name === 'Life') {
sourceImg = focused
? require('../asstes/tabBar/life_active_big.png')
: require('../asstes/tabBar/life_default_big.png');
} else if (route.name === 'Amuse') {
sourceImg = focused
? require('../asstes/tabBar/amuse_active_big.png')
: require('../asstes/tabBar/amuse_default_big.png');
} else if (route.name === 'Home') {
sourceImg = focused
? require('../asstes/tabBar/home_active_big.png')
: require('../asstes/tabBar/home_default_big.png');
} else if (route.name === 'Service') {
sourceImg = focused
? require('../asstes/tabBar/service_active_big.png')
: require('../asstes/tabBar/service_default_big.png');
} else if (route.name === 'Mine') {
sourceImg = focused
? require('../asstes/tabBar/mine_active_big.png')
: require('../asstes/tabBar/mine_default_big.png');
}
return <Image 
source={sourceImg} 
focused={focused} 
style={{ width: pxTodp(40), height: pxTodp(40) }} 
/>;
},
})}
{/* 导航器配置对象 */}
tabBarOptions={{
{/* 被激活导航器文字颜色 */}
activeTintColor: 'red',
{/* 导航器默认文字颜色 */}
inactiveTintColor: 'gray',
{/*底部导航器tabbar的样式 */}
tabStyle : {
backgroundColor: '#ddd',
paddingBottom: 15,
borderRightWidth: 1,
borderRightColor: '#fff'
},
}}
>
{/* 路由视图组件 */}
{/* name:路由名称,是路由匹配规则类似于前端的路径, */}
<Tab.Screen name="Life" component={LifeScreen} 
{/* title:配置导航器中文字 */}
options={{ title:'生活'}} 
/>
<Tab.Screen name="Amuse" options={{ title: "娱乐" }} component={AmuseScreen} />
<Tab.Screen name="Home" options={{ title: "主页", }} component={HomeScreen} />
<Tab.Screen name="Service" options={{ title: "服务" }} component={ServiceScreen} />
<Tab.Screen name="Mine" options={{ title: "我的" }} component={MineScreen} />
</Tab.Navigator>
)
}
  • 4、将tab导航器挂载导航路由容器中(router/index.js)
// 导入tab导航器组件
import TabNavigator from './tabNavigator';
function App() {
return (
<NavigationContainer>
<TabNavigator/>
</NavigationContainer>
);
}
  • 5、效果预览

view1.jpg

可能会遇到的问题

  • 拉起软键盘时,将底部导航器顶起来。如下图

微信截图_20210415160711.png
修改android\app\src\main\AndroidManifest.xml

 
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="stateAlwaysHidden|adjustPan|adjustResize">
// 将android:windowSoftInputMode属性设为:"stateAlwaysHidden|adjustPan|adjustResize"
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

添加动画

tabNavigator式路由导航功能已经实现了,我们可以在此基础上添加一个路由切换tabbar动画效果。当路由激活时,tabbar图标由小变大的入场动画.效果如下:
390x186_1617343751820.gif

  • 1、导入Animated动画组件
import { Animated } from 'react-native';
  • 2、 在tabNavigator组件声明动画值控制所有tabbar动画状态
export default function TabNavigator() {
// 设置tab激活状态的缩放动画的初始值
let tabAnimate = {
Life: new Animated.Value(1),
Amuse: new Animated.Value(1),
Home: new Animated.Value(1),
Service: new Animated.Value(1),
Mine: new Animated.Value(1),
}  
return <Tab.Navigator
initialRouteName="Home"
......
</Tab.Navigator>
  • 3、路由切换时,被激活路由配置动画并执行
<Tab.Navigator
initialRouteName="Home"
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, size }) => {
let sourceImg;
// 切换被激活的tab页
if (route.name && focused) {
// 设置tab icon缩放为0
tabAnimate[route.name].setValue(0)
// 设置tab icon缩放为0到1的动画 并执行这个动画
Animated.timing(tabAnimate[route.name], { duration: 200, toValue: 1, useNativeDriver: true }).start();
}
if (route.name === 'Life') {
sourceImg = focused
? require('../asstes/tabBar/life_active_big.png')
: require('../asstes/tabBar/life_default_big.png');
} else if (route.name === 'Amuse') {
sourceImg = focused
? require('../asstes/tabBar/amuse_active_big.png')
: require('../asstes/tabBar/amuse_default_big.png');
} else if (route.name === 'Home') {
sourceImg = focused
? require('../asstes/tabBar/home_active_big.png')
: require('../asstes/tabBar/home_default_big.png');
} else if (route.name === 'Service') {
sourceImg = focused
? require('../asstes/tabBar/service_active_big.png')
: require('../asstes/tabBar/service_default_big.png');
} else if (route.name === 'Mine') {
sourceImg = focused
? require('../asstes/tabBar/mine_active_big.png')
: require('../asstes/tabBar/mine_default_big.png');
}
// 使用Animated.Image组件,并将缩放动画值绑定到transform.scale上
return <Animated.Image source={sourceImg} focused={focused} style={{ width: pxTodp(40), height: pxTodp(40), transform: [{ scale: focused ? tabAnimate[route.name] : 1 }] }} />;
},
})}
>......</Tab.Navigator>

StackNavigator

这个一个堆栈式路由模式,采用堆栈式的页面导航来实现各个界面跳转。

基础配置

  • 1、安装stackNavigator相关依赖
yarn add @react-navigation/stack
  • 2、将导航路由容器组件挂载到页面上(router/index.js)
import React, { Component } from 'react';
import { NavigationContainer } from '@react-navigation/native';
function App() {
  return (
    <NavigationContainer>
        ...放入tab导航路由/stack导航路由
    </NavigationContainer>
  );
}
export default App;
 
  • 3、配置Stack导航器(router/stackNavigator.js)
import React from 'react'
import { createStackNavigator } from '@react-navigation/stack';
// 导入导航视图组件
import LifeScreen from '../views/life/index.js';
import AmuseScreen from '../views/amuse';
import HomeScreen from '../views/home/index';
import ServiceScreen from '../views/service';
import MineScreen from '../views/mine';
// 生成一个Stack导航器
const Stack = createStackNavigator();
export default function stackNavigtor() {
return (
<Stack.Navigator initialRouteName="Main">
{/* initialRouteName:用来配置默认匹配的路由 */}
{/* 页面顶部导航器标题默认为 name,可通过options.title自定义 */}
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Life" component={LifeScreen} options={{
// title:配置顶部导航器标题
title: '生活',
}}/>
<Stack.Screen name="Amuse" component={AmuseScreen} options={{ title: "娱乐" }} />
<Stack.Screen name="Service" component={ServiceScreen} />
<Stack.Screen name="Mine" component={MineScreen} />
<Stack.Screen name="Search" component={Search} options={{ 
// headerShown:隐藏顶部导航器 
headerShown: false
}} />
</Stack.Navigator>
)
}
  • 4、将tab导航器挂载导航路由容器中(router/stackNavigator.js)
import StackNavigator from './stackNavigator.js';
function App() {
return (
<NavigationContainer>
<StackNavigator/>
</NavigationContainer>
);
}
  • 5、效果预览

stack.png

路由模式嵌套

在实际业务场景中我们的应用往往是TabNavigator和StackNavigator导航模式同时使用,例如应用进入后展示TabNavigator相关路由,在某些路由屏幕中需要跳转到非TabNavigator页面中。如下图,这种场景就需要做两种路由模式的嵌套

appRouter.png

  • 1、将导航路由容器组件挂载到页面上(router/index.js)
  • 2、配置Stack导航器(router/stackNavigator.js)
    import React from 'react'
    import { createStackNavigator } from '@react-navigation/stack';
    // 导入导航视图组件
    import DetailScreen from '../views/detail';
    import ListScreen from '../views/list';
    // ... 其他非tab页面
    const Stack = createStackNavigator();
    export default function stackNavigtor() {
    return (
    <Stack.Navigator>
    <Stack.Screen name="Detail" component={DetailScreen} />
    <Stack.Screen name="List" component={ListScreen} />
    {/* 其他屏幕组件...... */}
    </Stack.Navigator>
    )
    }
    
  • 3、配置Tab导航器(router/tabNavigator.js):同TabNavigator板块中基础配置一致
  • 4、将Tab导航器导入、并挂载到Stack导航器中(router/stackNavigator.js)
    // 导入Tab导航器
    import TabNavigator from './tabNavigator.js';
    export default function stackNavigtor() {
    return (
    <Stack.Navigator
    initialRouteName="Main"
    >
    {/* initialRouteName="Main"默认展示Tab导航路由 */}
    <Stack.Screen name="Main" component={TabNavigator} />
    <Stack.Screen name="Detail" component={DetailScreen} />
    <Stack.Screen name="List" component={ListScreen} />
    {/* ...... */}
    </Stack.Navigator>
    )
    }
    
  • 5、将Stack导航器导入、并挂载到导航路由容器组件中(router/index.js)
    import StackNavigator from './stackNavigator.js';
    function App() {
    return (
    <NavigationContainer>
    <StackNavigator/>
    </NavigationContainer>
    );
    }
    

编程式导航

使用Stack导航器时,我们有时需要通过代码实现路由切换,例如点击页面某个点位或者自定义导航器的按钮
navigation:导航器会为每个屏幕组件传入props属性navigation通过它可以实现路由切换

navigation.navigate( routerName )

  • 作用::切换路由页面,当前页面切换到当前页面无效。例如,在A商品详情页跳转到B商品详情页,本质是一个页面详情页跳往详情页只是路由参数不同。
  • 参数:routerName(路由名称 )

navigation.push( routerName )

  • 作用:切换路由页面,一直往堆栈里加可以切换到当前页面
  • 参数:routerName(路由名称 )

navigation.replace( routerName )

  • 作用:切换新路由并替换旧路由记录
  • 参数:routerName(路由名称 )

navigation.goback()

  • 作用:返回上一个路由页面

navigation.popToTop()

  • 作用:回到路由堆栈第一个路由页面
import React from 'react'
import { View, Text, Button } from 'react-native'
import pxTodp from '../../utils/adaptive';
export default function Home(props) {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<View style={{ flexDirection: "row", justifyContent: "center", alignItems: "center", height: pxTodp(500) }}>
<Text style={{ marginRight: pxTodp(20) }}>跳转到生活页面(navigate)</Text>
<Button title="跳转" onPress={() => { props.navigation.navigate("Life")}}></Button>
</View>
<View style={{ flexDirection: "row", justifyContent: "center", alignItems: "center", height: pxTodp(500) }}>
<Text style={{ marginRight: pxTodp(20) }}>跳转到生活页面(push)</Text>
<Button title="跳转" onPress={() => { props.navigation.push("Life")}}></Button>
</View>
<View style={{ flexDirection: "row", justifyContent: "center", alignItems: "center", height: pxTodp(500) }}>
<Text style={{ marginRight: pxTodp(20) }}>跳转到生活页面replace</Text>
<Button title="跳转" onPress={() => { props.navigation.replace("Life")}}></Button>
</View>
<View style={{ flexDirection: "row", justifyContent: "center", alignItems: "center", height: pxTodp(500) }}>
<Text style={{ marginRight: pxTodp(20) }}>返回上级页面</Text>
<Button title="跳转" onPress={() => { props.navigation.goback()}}></Button>
</View>
<View style={{ flexDirection: "row", justifyContent: "center", alignItems: "center", height: pxTodp(500) }}>
<Text style={{ marginRight: pxTodp(20) }}>跳转首页</Text>
<Button title="跳转" onPress={() => { props.navigation.popToTop()}}></Button>
</View>
</View >
)
}
 

路由参数

传递参数

大部分导航API的第二个参数为params对象,通过这个对象来传递路由参数

<Button
title="跳转到详情"
onPress={() => {
navigation.navigate('Details', {
itemId: 86,
otherParam: '你想传递参数',
});
}}
/>

接受参数

通过props的route.params中获取参数

function DetailsScreen({ route, navigation }) {
const { itemId } = route.params;
const { otherParam } = route.params;
return (...)
}

初始化路由参数

<Stack.Screen
name="Details"
component={DetailsScreen}
initialParams={{ itemId: 42 }}
/><

4、渐变色

RN渐变色可以通过 react-native-linear-gradient 这个组件实现

  • 安装依赖
yarn add react-native-linear-gradient
  • 在页面中使用
import React from 'react';
import {Text, View} from 'react-native';
import LinearGradinet from 'react-native-linear-gradient';
export default class Home extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<View style={{ flexDirection: "column", justifyContent: "center", alignItems: "center">
<Text>首页</Text>
{/*start:渐变起点的位置,x和y的大小区间为0-1
*end:渐变结束的位置,x和y的大小区间为0-1
*colors:值为数组,数组每项代表渐变颜色的总个数
*locations:用来控制颜色渐变的范围,值为数组,数组每项代表上个颜色渐变的终点位置和当前颜色渐变开始位置 大小区间为0-1
*/}
<LinearGradinet
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
colors={['#9b63cd', '#e0708c', '#000000']}
style={{ width: 200, height: 200 }}
/>
{/* 使用默认location */}
<LinearGradinet
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
colors={['#9b63cd', '#e0708c', '#000000']}
style={{ width: 200, height: 200 }}
locations={[0, 0.5, 1]}
/>
{/* 自定义location */}
<LinearGradinet
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
colors={['#9b63cd', '#e0708c', '#000000']}
style={{ width: 200, height: 200 }}
locations={[0.1, 0.2, 0.5]}
/>
</View>
</View > );
}
}
 

liean.jpg

5、轮播图

轮播图可以通过 react-native-swiper 这个组件实现

  • 安装依赖
yarn add react-native-linear-gradient
  • 在页面中使用
import React, { useState } from 'react'
import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native'
import Swiper from 'react-native-swiper';
import pxTodp from '../../utils/adaptive';
export default function bannerSwiper() {
const [state, setState] = useState({
swiperList: [
{
imgURL: "http://image2.suning.cn/uimg/cms/img/161641067864122785.jpg?format=_is_1242w_610h",
jumpLink: "www.baidu.com"
},
{
imgURL: "http://oss.suning.com/aps/aps_learning/iwogh/2021/03/22/10/iwoghBannerPicture/894481af3bb740aeb9ff24c91ca63f4b.png?format=_is_1242w_610h",
jumpLink: "www.baidu.com"
},
{
imgURL: "http://oss.suning.com/aps/aps_learning/iwogh/2021/03/22/10/iwoghBannerPicture/b5160fc4515d41a2bff2552adaef8510.png?format=_is_1242w_610h",
jumpLink: "www.baidu.com"
},
{
imgURL: "https://oss.suning.com/adpp/creative_kit/material/picture/20200706-0b704b452c38418897f43d944583da53693a083846d5484c.jpg",
jumpLink: "www.baidu.com"
}
]
})
return (
<View style={styles.container}>
<View style={styles.swiperContainer} >
{/* removeClippedSubviews={false} 用来修复第二次循环切换轮播闪烁问题 */}
<Swiper
style={styles.wrapper}
showsButtons={false}
autoplay={true}
paginationStyle={styles.paginationStyle}
dotStyle={styles.dotStyle}
activeDotStyle={styles.activeDotStyle}
removeClippedSubviews={false}
>
{state.swiperList.map(e => {
return <TouchableOpacity key={e.imgURL} style={{
borderRadius: pxTodp(20),
overflow: "hidden",
height: pxTodp(150),
}} activeOpacity={1}><Image source={{ uri: e.imgURL }} style={styles.bannerImg} />
</TouchableOpacity>
})}
</Swiper>
</View>
</View >
)
}
const styles = StyleSheet.create({
container: {
flexDirection: "row",
justifyContent: "center",
backgroundColor: "white",
height: pxTodp(165)
},
swiperContainer: {
width: pxTodp(497),
height: pxTodp(150),
},
paginationStyle: {
position: "absolute",
bottom: 0
},
activeDotStyle: {
opacity: 1,
backgroundColor: "#fff",
},
dotStyle: {
backgroundColor: "#fff",
opacity: 0.5,
width: pxTodp(10),
height: pxTodp(10),
borderRadius: pxTodp(10)
},
wrapper: {
},
bannerImg: {
width: pxTodp(497),
height: pxTodp(150),
resizeMode: "cover",
}
})
 

最后

大家的RN搭建过程可能会不太顺利,如果有需要帮助或者技术上交流的同猿欢迎加 V: gg_0632

回复

我来回复
  • 暂无回复内容