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

2,052 阅读9分钟

本文中主要介绍

  • 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