React Navigation 4.x

编辑于2020年01月13日

近期将项目中的 react native 升级到了 0.60.x 版本,同步的也将 React Navigation 升级到了 4.x。这篇博客记录了 4.x 版本的一些基本用法以及在实现项目中一些常见功能的实现,其中 rn 基于 0.60 版本。

安装

4.x 版本从 react-navigation 中移除了各类导航器,同时还依赖了一些其他的包需要手动安装。

npm install react-navigation react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context

rn 0.60 版本之后,安装完成之后会自动 link,低版本安装过程见官网说明

Android 端需要手动进行一些修改,编辑 android/app/build.gradle,在 dependencies 中添加如下内容:

implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02'

WX20200113-140721

编辑 Android 中的 MainActivity.java,添加如下内容:

package com.reactnavigation.example;

import com.facebook.react.ReactActivity;
+ import com.facebook.react.ReactActivityDelegate;
+ import com.facebook.react.ReactRootView;
+ import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;

public class MainActivity extends ReactActivity {

  @Override
  protected String getMainComponentName() {
    return "Example";
  }

+  @Override
+  protected ReactActivityDelegate createReactActivityDelegate() {
+    return new ReactActivityDelegate(this, getMainComponentName()) {
+      @Override
+      protected ReactRootView createRootView() {
+        return new RNGestureHandlerEnabledRootView(MainActivity.this);
+      }
+    };
+  }
}

最后在 index.js 或者 app.js 中导入 react-native-gesture-handler 依赖即可。

import 'react-native-gesture-handler';

基本使用

4.x 版本移除了各类导航器,需要手动安装,这里安装一下 StackNavigatorBottomTabNavigator

npm install react-navigation-stack @react-native-community/masked-view react-navigation-tabs

添加三个页面组件:

const Home = props => {
  const { navigation } = props
  return (
    <View>
      <TouchableOpacity onPress={() => navigation.push('Second')}>
        <View>
          <Text>Second page</Text>
        </View>
      </TouchableOpacity>
    </View>
  )
}

const My = () => {
  return (
    <View>
      <Text>My</Text>
    </View>
  )
}

const Second = () => {
  return (
    <View>
      <Text>Second</Text>
    </View>
  )
}

创建导航器,这一部分写法和之前一样:

const MainTab = createBottomTabNavigator({
  Home,
  My,
})

const AppStack = createStackNavigator({
  Main: {
    screen: MainTab,
  },
  Second,
})

export default function App() {
  const AppContainer = createAppContainer(AppStack)

  return <AppContainer />
}

screenshot-1

调整 TabNavigator Header 标题

从上图可以看到,Home 和 My 页面顶部标题都是现实的 Main,因为这两个页面都在 BottomTabNavigator 中,共用了一个 Header。我们可以通过 Main 页面的 navigationOptions 来动态修改标题:

Main: {
  screen: MainTab,
  navigationOptions: ({ navigation }) => {
    const { routeName } = navigation.state.routes[navigation.state.index]
    // You can do whatever you like here to pick the title based on the route name
    const headerTitle = routeName

    return {
      headerTitle,
    }
  }
}

这里直接使用了 routeName 作为标题,也可以根据 routeName 匹配其他的标题文字。效果如下:
tab_title

添加 Tabbar 图标

通过 BottomTabNavigator navigationOptions 中的 tabBarIcon 属性可以设置 Tabbar 的图标。

首先实现一个 TabbarIcon 组件,根据 routeName 返回相应的图片,同时图片会读取 tintColor 属性设置颜色:

const IMAGES = {
  Home: require('./assets/icons/home.png'),
  My: require('./assets/icons/my.png'),
}

const TabbarIcon = ({ routeName, tintColor }) => {
  return (
    <Image
      source={IMAGES[routeName]}
      style={[styles.image, { tintColor: tintColor }]}
      resizeMode="contain"
    />
  )
}

const styles = StyleSheet.create({
  image: {
    height: 24,
  },
})

然后通过 BottomTabNavigatordefaultNavigationOptions 属性设置不同页面的图标:

defaultNavigationOptions: ({ navigation }) => {
  const { routeName } = navigation.state
  return {
    tabBarIcon: props => <TabbarIcon {...props} routeName={routeName} />,
  }
}

还可以通过 tabBarOptions 设置激活和未激活的颜色 :

tabBarOptions: {
  inactiveTintColor: 'rgba(0,0,0,0.45)',
  activeTintColor: '#722ed1',
}

tab_icon

统一路由风格

StackNavigator 在 Android 中的表现和 iOS 中存在一些差异,通过暴露的一些配置项我们可以统一风格,这里统一使用 iOS 的风格。

首先实现一个 HeaderBackImage 组件,统一返回图标:

const IS_IOS = Platform.OS === 'ios'

export default props => {
  return (
    <View style={styles.imgContainer}>
      <Image
        source={require('./assets/icons/icon_back.png')}
        style={[styles.image, { tintColor: props.tintColor }]}
        {...props}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  imgContainer: {
    paddingRight: IS_IOS ? 6 : 15,
    paddingLeft: IS_IOS ? 15 : 0,
  },
  image: {
    backgroundColor: 'transparent',
    height: 16,
    width: 10,
    resizeMode: 'contain',
  },
})

修改 StackNavigator 配置:

defaultNavigationOptions: {
  headerStyle: {
    elevation: 0, // 移除 Android Header 阴影
    shadowOpacity: 0, // 移除 iOS Header 阴影
  },
  headerBackImage: HeaderBackImage,
  headerTitleAlign: 'center', // Android 标题居中
  headerBackTitleVisible: false, // 隐藏 iOS 返回按钮标题
  headerPressColorAndroid: 'transparent', // 移除 Android 点击返回按钮效果
  cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, // 切换路由时水平动画
  headerStyleInterpolator: HeaderStyleInterpolators.forUIKit, // 切换路时 Header 动画
},
headerMode: 'float', // 页面共用一个 Header,切换时应用动画

tab_a_android-1

沉浸式状态栏

之前我写过一篇关于实现沉浸式状态栏的博客,在 4.x 版本中实现该功能变得更加方便。

纯色

首先我们给 Header 添加一个背景色看看默认是什么效果。

headerStyle: {
  backgroundColor: '#722ed1',
},
headerTintColor: '#fff',

statusbar

可以看到 Android 端状态栏依旧是灰色。我们可以设置 StatusBar 的属性来实现沉浸式:

<>
  <StatusBar backgroundColor="transparent" translucent />
  <AppContainer />
</>

status_android

可以看到设置了 translucent 之后,内容并没有往上移动到状态,4.x 版本默认处理了这种情况。

背景图

有时页面顶部有一张背景图或者整个页面有个全屏背景,这时我们就需要 Header 透明并且内容能衍生到状态栏底部。

我们先修改 Second 页面,添加一个背景图片:

const Second = () => {
  return (
    <ImageBackground
      style={{ flex: 1 }}
      source={require('../../assets/img/bg.jpg')}>
      <Text>I have a full screen background image</Text>
    </ImageBackground>
  )
}

WX20200113-161531

可以看到图片并没有撑满整个屏幕,设置 headerTransparent 属性为 true 可以使 Header 透明并浮在页面上,这样内容就会撑满。

Second.navigationOptions = {
  headerTransparent: true,
}

full

此时虽然全屏了,但是内容却跑到 Header 底部除了,我们需要添加上边距预留出 Header 的位置,通过 useHeaderHeight 可以获取到 Header 的高度。

import { useHeaderHeight } from 'react-navigation-stack'

const headerHeight = useHeaderHeight()

full_margin

处理状态栏文字

RN 中切换到不同的页面,可能需要显示不能颜色的文字(深色、浅色),在之前的博客中介绍了使用高阶组件的方法,监听 navigation 的 willFocus 事件切换。4.x 版本中仍需要使用此方案,不过这次通过 hook 来实现。

// useStatusBar.js

import { useEffect } from 'react'
import { StatusBar } from 'react-native'

const useStatusBar = (navigation, barStyle) => {
  useEffect(() => {
    const onWillFocus = () => {
      StatusBar.setBarStyle(barStyle)
    }

    StatusBar.setBarStyle(barStyle)

    const listener = navigation.addListener('willFocus', onWillFocus)

    return () => listener.remove()
  }, [])
}

export default useStatusBar

当页面挂载之后设置状态栏的风格,后续当从其他页面返回时会触发 willFocus 事件。