跳到主要内容

React Navigation v5 + React Native Paper = ❤️

·19 分钟阅读
Dawid Urbaniak
Dawid Urbaniak
React Native Paper 团队

这是一篇由 React Native Paper 团队撰写的客座文章。如果您喜欢本指南,请查看 React Native Paper 以获取更多信息!

在这篇博文中,我们将向您展示如何使用 React Navigation v5 和 Paper 构建一个 Twitter 克隆应用。

简介

与之前的版本相比,React Navigation v5 带来了许多重大改进。它不仅提供了跨平台的原生堆栈,而且 API 也从头开始重新设计,以实现以前从未实现过的功能。由于基于组件的 API,所有配置都在 render 方法 中进行。这意味着我们可以访问 propsstatecontext,并且可以动态更改导航器的配置

什么是 React Native Paper?

React Native Paper 是一个 UI 组件库,它实现了 MD 指南。它允许使用高质量的跨平台组件在移动端和 Web 端构建美观的界面。此外,Paper 为您提供完整的 主题支持可访问性RTL,并且它将负责 平台适配。这意味着您可以专注于使用现成的组件构建应用程序,而不是重新实现那些乏味的东西。

在本指南中,我们想向您展示如何将 React Navigation 与 Paper 的组件集成。为了展示集成的所有细节,我们决定构建一个 Twitter 的克隆版本。当然,功能将非常有限,但导航部分和主要屏幕应该看起来和感觉相似。

在下面的 gif 中,您可以看到最终版本的应用程序的样子

Final Result

应用概览

由于原始 Twitter 是一个非常复杂的应用程序,我们将只构建其中的一部分。这意味着我们将实现

  • 抽屉导航
  • 堆栈导航器,包含两个屏幕:显示底部导航的屏幕和推文详情
  • 底部导航,包含 3 个标签页:Feed、通知和消息

本指南将侧重于 React Navigation 和 React Native Paper 的集成。这意味着我不会向您展示如何构建创建这样一个应用程序所需的所有组件,但您始终可以在 github 仓库 中查看完整的实现。

让我们开始吧!

开始入门

我假设您已经本地运行了一个 Expo 项目。如果没有,请确保创建一个。我选择 Expo 而不是纯 React-Native,因为它包含了我们所需的大部分依赖项,因此我们需要做的工作更少。

让我们安装 React Native Paper、React Navigation v5 和其他必需的依赖项。

npm install @react-navigation/native @react-navigation/stack @react-native-community/masked-view @react-navigation/drawer @react-navigation/material-bottom-tabs react-native-paper

下一步,我们将确保这些库的版本兼容。

expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context

在您运行这两个命令后,您应该准备就绪了。让我们开始实现应用程序!

React Navigation 和 React Native Paper 的初始设置

这两个库都需要最少的设置。

对于 React Native Paper,我们需要用 Provider 包裹组件树。您可以在 App.js 文件中导出的组件内执行此操作。

import React from 'react';
import { Provider as PaperProvider } from 'react-native-paper';
import Main from './src/Main';

export default function App() {
return (
<PaperProvider>
<Main />
</PaperProvider>
);
}

PaperProvider 为框架中的所有组件提供主题。它还充当需要在顶层渲染的组件的入口。查看完整的 入门 页面以获取更多信息。

React Navigation 的设置看起来类似。有一个名为 NavigationContainer 的组件,它管理我们的导航树并包含导航状态。它必须包裹所有导航器结构。我们将在 App.tsx 中的 PaperProvider 内渲染此组件。更多信息可以在官方文档中找到。

import React from 'react';
import { Provider as PaperProvider } from 'react-native-paper';
import { NavigationContainer } from '@react-navigation/native';
import Main from './src/Main';

export default function App() {
return (
<PaperProvider>
<NavigationContainer>
<Main />
</NavigationContainer>
</PaperProvider>
);
}

抽屉导航

在我们的 Twitter 克隆应用中,我们希望实现一个在应用中的任何屏幕都可用的抽屉导航。这意味着它必须是最顶层的导航器。

在 React Navigation v5 中,有一个创建导航器的通用模式。从您选择的导航器包中导入 createXNavigator 函数后,您可以使用它返回的值中的 NavigatorScreen 组件。

因此,让我们创建一个基本版本的抽屉导航

import React from 'react';
import { Text, View } from 'react-native';
import { createDrawerNavigator } from '@react-navigation/drawer';

const Drawer = createDrawerNavigator();

function DrawerContent() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Drawer content</Text>
</View>
);
}

function HomeScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
}

export const RootNavigator = () => {
return (
<Drawer.Navigator drawerContent={() => <DrawerContent />}>
<Drawer.Screen name="Home" component={HomeScreen} />
</Drawer.Navigator>
);
};

这就是我们在屏幕上看到的

Simple Drawer

我们可以通过滑动的手势打开抽屉导航,它看起来非常流畅。但是,UI 看起来不是很令人印象深刻,所以让我们向抽屉导航添加更多内容,使其看起来像最终版本一样。

我们将使用

  • 来自 @react-navigation/drawerDrawerContentScrollViewDrawerItem
  • 来自 react-native-paperAvatarTextSwitch

DrawerContentScrollView 组件使抽屉导航可以垂直滚动,并为带有凹槽的设备提供支持,因此强烈建议即使对于自定义抽屉导航也使用它。

来自 React Native Paper 的组件 使 UI 简洁、 Material Design 风格。

import React from 'react';
import { View, StyleSheet } from 'react-native';
import {
DrawerItem,
DrawerContentScrollView,
} from '@react-navigation/drawer';
import {
useTheme,
Avatar,
Title,
Caption,
Paragraph,
Drawer,
Text,
TouchableRipple,
Switch,
} from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';

export function DrawerContent(props) {
return (
<DrawerContentScrollView {...props}>
<View
style={
styles.drawerContent,
}
>
<View style={styles.userInfoSection}>
<Avatar.Image
source={{
uri:
'https://pbs.twimg.com/profile_images/952545910990495744/b59hSXUd_400x400.jpg',
}}
size={50}
/>
<Title style={styles.title}>Dawid Urbaniak</Title>
<Caption style={styles.caption}>@trensik</Caption>
<View style={styles.row}>
<View style={styles.section}>
<Paragraph style={[styles.paragraph, styles.caption]}>
202
</Paragraph>
<Caption style={styles.caption}>Following</Caption>
</View>
<View style={styles.section}>
<Paragraph style={[styles.paragraph, styles.caption]}>
159
</Paragraph>
<Caption style={styles.caption}>Followers</Caption>
</View>
</View>
</View>
<Drawer.Section style={styles.drawerSection}>
<DrawerItem
icon={({ color, size }) => (
<MaterialCommunityIcons
name="account-outline"
color={color}
size={size}
/>
)}
label="Profile"
onPress={() => {}}
/>
<DrawerItem
icon={({ color, size }) => (
<MaterialCommunityIcons name="tune" color={color} size={size} />
)}
label="Preferences"
onPress={() => {}}
/>
<DrawerItem
icon={({ color, size }) => (
<MaterialCommunityIcons
name="bookmark-outline"
color={color}
size={size}
/>
)}
label="Bookmarks"
onPress={() => {}}
/>
</Drawer.Section>
<Drawer.Section title="Preferences">
<TouchableRipple onPress={() => {}}>
<View style={styles.preference}>
<Text>Dark Theme</Text>
<View pointerEvents="none">
<Switch value={false} />
</View>
</View>
</TouchableRipple>
<TouchableRipple onPress={() => {}}>
<View style={styles.preference}>
<Text>RTL</Text>
<View pointerEvents="none">
<Switch value={false} />
</View>
</View>
</TouchableRipple>
</Drawer.Section>
</View>
</DrawerContentScrollView>
);
}

const styles = StyleSheet.create({
drawerContent: {
flex: 1,
},
userInfoSection: {
paddingLeft: 20,
},
title: {
marginTop: 20,
fontWeight: 'bold',
},
caption: {
fontSize: 14,
lineHeight: 14,
},
row: {
marginTop: 20,
flexDirection: 'row',
alignItems: 'center',
},
section: {
flexDirection: 'row',
alignItems: 'center',
marginRight: 15,
},
paragraph: {
fontWeight: 'bold',
marginRight: 3,
},
drawerSection: {
marginTop: 15,
},
preference: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 16,
},
});

抽屉导航的最终版本如下所示

Drawer with components from React Native Paper

堆栈导航器 + Paper 的 Appbar

堆栈导航器为应用程序提供了一种在屏幕之间转换的方式,每次新屏幕都放置在堆栈的顶部。对于这个 Twitter 克隆应用,我们将使用它来从显示推文 Feed 的屏幕过渡到显示推文详情的屏幕。

React Navigation v5 提供了堆栈导航器的两种实现

  • 原生堆栈
  • 基于 JS 的堆栈

它们之间的主要区别在于,基于 JS 的堆栈重新实现了动画和手势,而原生堆栈导航器依赖于平台的原始动画和手势。

在本节中,我们将集成 React Native Paper Appbar 和基于 JS 的堆栈导航器。

第一步,我们将创建一个最小版本的堆栈

import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';

import { Feed } from './feed';
import { Details } from './details';

export const FeedStack = () => {
return (
<Stack.Navigator initialRouteName="Feed">
<Stack.Screen
name="Feed"
component={Feed}
options={{ headerTitle: 'Twitter' }}
/>
<Stack.Screen
name="Details"
component={Details}
options={{ headerTitle: 'Tweet' }}
/>
</Stack.Navigator>
);
};

默认情况下,堆栈导航器配置为具有熟悉的 iOS 和 Android 标题栏。但这不符合我们的需求,因为我们想使用 Paper 的 Appbar 来代替。我们可以通过将 Appbar.Header 组件作为 Stack 的 screenOptions 中的 header 传递来实现。我们还将传递 headerMode 属性,其值为 screen,以获得漂亮的淡入/淡出动画。

import React from 'react';
import { TouchableOpacity } from 'react-native';
import { createStackNavigator } from '@react-navigation/stack';
import { Appbar, Avatar } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';

import { Feed } from './feed';
import { Details } from './details';

const Header = ({ scene, previous, navigation }) => {
const { options } = scene.descriptor;
const title =
options.headerTitle !== undefined
? options.headerTitle
: options.title !== undefined
? options.title
: scene.route.name;

return (
<Appbar.Header theme={{ colors: { primary: theme.colors.surface } }}>
{previous ? (
<Appbar.BackAction
onPress={navigation.pop}
color={theme.colors.primary}
/>
) : (
<TouchableOpacity
onPress={() => {
navigation.openDrawer();
}}
>
<Avatar.Image
size={40}
source={{
uri: 'https://pbs.twimg.com/profile_images/952545910990495744/b59hSXUd_400x400.jpg',
}}
/>
</TouchableOpacity>
)}
<Appbar.Content
title={
previous ? title : <MaterialCommunityIcons name="twitter" size={40} />
}
/>
</Appbar.Header>
);
};

export const FeedStack = () => {
return (
<Stack.Navigator
initialRouteName="FeedList"
headerMode="screen"
screenOptions={{
header: ({ scene, previous, navigation }) => (
<Header scene={scene} previous={previous} navigation={navigation} />
),
}}
>
<Stack.Screen
name="Feed"
component={Feed}
options={{ headerTitle: 'Twitter' }}
/>
<Stack.Screen
name="Details"
component={Details}
options={{ headerTitle: 'Tweet' }}
/>
</Stack.Navigator>
);
};

我们传递给 header 属性的函数可以访问 3 个属性

  • scene
  • previous
  • navigation

借助 scene 属性,我们可以访问堆栈中最顶层屏幕的标题并在标题栏中显示它。Previous 属性告诉我们堆栈中是否有任何其他较低层的屏幕。


最后,navigation 属性允许导航到不同的屏幕,例如打开抽屉导航。

我们尚未涵盖且非常重要的一点是如何在堆栈导航器屏幕之间实际导航。对于标签页或抽屉导航器,我们开箱即用。我们可以滑动以打开/关闭抽屉导航或按下标签页以更改场景。在堆栈中,我们必须自己实现它。

React Navigation 为我们提供了许多不同的导航方式,但我们将主要关注 pushpop。您可以在 navigation 属性中访问这两种方法。

顾名思义,push 方法将新屏幕推送到堆栈上,而 pop 从堆栈中删除当前屏幕。

正如您在上面的代码片段中看到的,每当用户按下标题栏中的后退按钮时,我们都会调用 navigation.pop 函数。这意味着用户将被允许从 Details 返回到 Feed 屏幕。

我们仍然需要实现从 Feed 转到 Details 的选项。我们可以通过在用户按下推文时调用 navigation.push('Details') 来实现。

function onTweetPress() {
navigation.push('Details');
}

FeedDetails 组件的实现相当庞大和复杂,这就是为什么我不会在这里发布它。请务必在 github 仓库 上查看它

我们仅涵盖了屏幕之间导航的基础知识。如果您想了解更多详细信息,请查看官方文档

现在,让我们看看使用堆栈导航器和 Paper 的 Appbar 的应用程序是什么样子。

Stack Navigator with React Native Paper's Appbar

我们仍然缺少导航流程的最后一部分 - 标签页导航器。让我们转到下一节,我们将处理它。

底部导航

在本节中,我们将实现一个带有 3 个标签页的标签页导航器,并且我们将确保此组件现在是堆栈导航器的一个屏幕。

我们将使用来自 React Native Paper 的 底部导航 组件,该组件通过 @react-navigation/material-bottom-tabs 包公开。

首先让我们导入 createMaterialBottomTabNavigator 函数。

import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';

然后我们可以获取对 Tab.Navigator 和 Tab.Screen 组件的引用。

const Tab = createMaterialBottomTabNavigator();

现在,我们准备构建实际的底部导航。我们将渲染一个 Tab.navigator 和 3 个 Tab.Screen 组件作为子组件。每个 Tab.Screen 代表一个标签页。

import React from 'react';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';

import { Feed } from './feed';
import { Messages } from './messages';
import { Notifications } from './notifications';

const Tab = createMaterialBottomTabNavigator();

export const BottomTabs = () => {
return (
<Tab.Navigator
initialRouteName="Feed"
shifting={true}
sceneAnimationEnabled={false}
>
<Tab.Screen
name="Feed"
component={Feed}
options={{
tabBarIcon: 'home-account',
}}
/>
<Tab.Screen
name="Notifications"
component={Notifications}
options={{
tabBarIcon: 'bell-outline',
}}
/>
<Tab.Screen
name="Messages"
component={Messages}
options={{
tabBarIcon: 'message-text-outline',
}}
/>
</Tab.Navigator>
);
};

当我们现在检查手机屏幕时,我们将看到一个美观的 Material Design 底部导航。更重要的是,堆栈导航器与 Tab.Navigator 集成良好,我们仍然可以导航到推文 Details 屏幕。


Stack Navigator with Material Bottom Tabs

FAB 和 Portal

正如 Material Design 指南 中所述,FAB 按钮的目的是方便访问应用程序的主要操作。当然,官方 Twitter 应用程序遵循此模式。根据屏幕类型,它允许通过 FAB 创建新推文或发送私信。当用户更改标签页时,它还会平滑地动画 FAB 的图标,并在特定屏幕上完全隐藏 FAB。

在本节中,我们将实现与我们的应用程序完全相同的行为。我们将使用来自 React Native Paper 的 FABPortal 组件。

Portal 允许在父树中的不同位置渲染组件。这意味着您可以使用它来渲染应该出现在其他元素之上的内容,类似于 Modal。

作为初始步骤,我们将在所有标签页上渲染一个 FAB,然后我们将添加其他功能。

让我们在渲染标签页的同一组件中渲染 FABPortal

import React from 'react';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import { useTheme, Portal, FAB } from 'react-native-paper';

import { Feed } from './feed';
import { Message } from './message';
import { Notifications } from './notifications';

const Tab = createMaterialBottomTabNavigator();

export const BottomTabs = () => {
return (
<React.Fragment>
<Tab.Navigator
initialRouteName="Feed"
backBehavior="initialRoute"
shifting={true}
sceneAnimationEnabled={false}
>
<Tab.Screen
name="Feed"
component={Feed}
options={{
tabBarIcon: 'home-account',
}}
/>
<Tab.Screen
name="Notifications"
component={Notifications}
options={{
tabBarIcon: 'bell-outline',
}}
/>
<Tab.Screen
name="Messages"
component={Message}
options={{
tabBarIcon: 'message-text-outline',
}}
/>
</Tab.Navigator>
<Portal>
<FAB
icon="feather"
style={{
position: 'absolute',
bottom: 100,
right: 16,
}}
/>
</Portal>
</React.Fragment>
);
};

只需几行 JSX,我们就在所有标签页上显示了一个美观的 FAB。让我们实现每当用户进入推文详情屏幕时隐藏它。

我们当前的导航结构应该是

  • StackNavigator 有两个屏幕
  • StackNavigator 的第一个屏幕渲染一个带有 3 个标签页的 TabNavigator
  • StackNavigator 的第二个屏幕渲染推文详情

这意味着渲染 TabNavigator 的组件是 Stack 的一个屏幕。因此,我们可以使用 @react-navigation/native 提供的 useIsFocused hook 并有条件地隐藏 FAB

import React from 'react';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import { useTheme, Portal, FAB } from 'react-native-paper';
import { useIsFocused } from '@react-navigation/native';

import { Feed } from './feed';
import { Message } from './message';
import { Notifications } from './notifications';

const Tab = createMaterialBottomTabNavigator();

export const BottomTabs = () => {
const isFocused = useIsFocused();

return (
<React.Fragment>
<Tab.Navigator
initialRouteName="Feed"
backBehavior="initialRoute"
shifting={true}
>
<Tab.Screen
name="Feed"
component={Feed}
options={{
tabBarIcon: 'home-account',
}}
/>
<Tab.Screen
name="Notifications"
component={Notifications}
options={{
tabBarIcon: 'bell-outline',
}}
/>
<Tab.Screen
name="Messages"
component={Message}
options={{
tabBarIcon: 'message-text-outline',
}}
/>
</Tab.Navigator>
<Portal>
<FAB
visible={isFocused} // show FAB only when this screen is focused
icon="feather"
style={{
position: 'absolute',
bottom: safeArea.bottom + 65,
right: 16,
}}
/>
</Portal>
</React.Fragment>
);
};

在最后一步中,我们将添加根据活动标签页显示不同图标的功能。

我们将利用我们的 BottomTabs 组件作为 Stack 的一个屏幕的优势。这意味着它可以访问作为 prop 传递给每个屏幕的 route 对象。此对象包含有关当前屏幕的信息,这意味着我们可以访问它并有条件地渲染正确的图标。这不是一个非常常见的模式,起初可能会令人困惑,因此请务必阅读有关如何使用它以及可以使用它实现什么目标的完整指南

import React from 'react';
import color from 'color';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import { Portal, FAB } from 'react-native-paper';
import { useIsFocused } from '@react-navigation/native';

import { Feed } from './feed';
import { Message } from './message';
import { Notifications } from './notifications';

const Tab = createMaterialBottomTabNavigator();

export const BottomTabs = (props) => {
// Get a name of current screen
const routeName = getFocusedRouteNameFromRoute(route) ?? 'Feed';
const isFocused = useIsFocused();

let icon = 'feather';

switch (routeName) {
case 'Messages':
icon = 'email-plus-outline';
break;
default:
icon = 'feather';
break;
}

return (
<React.Fragment>
<Tab.Navigator initialRouteName="Feed" shifting={true}>
<Tab.Screen
name="Feed"
component={Feed}
options={{
tabBarIcon: 'home-account',
tabBarColor,
}}
/>
<Tab.Screen
name="Notifications"
component={Notifications}
options={{
tabBarIcon: 'bell-outline',
tabBarColor,
}}
/>
<Tab.Screen
name="Messages"
component={Message}
options={{
tabBarIcon: 'message-text-outline',
tabBarColor,
}}
/>
</Tab.Navigator>
<Portal>
<FAB
visible={isFocused}
icon={icon}
style={{
position: 'absolute',
bottom: 100,
right: 16,
}}
color="white"
/>
</Portal>
</React.Fragment>
);
};
React Native Paper's FAB with Bottom Tabs

正如您在 gif 中看到的,FAB 按钮的工作方式与 Twitter 应用程序中的相同。更重要的是,即使我们没有实现它,它甚至可以正确地动画图标更改。这是我们从 React Native Paper 的 FAB 中开箱即用的行为。

主题化

如今,支持浅色/深色主题不再是一种与众不同的花哨方式,而是已成为一种标准。幸运的是,React Navigation v5 和 React Native Paper 都支持主题化,在本节中,我将指导您完成设置。

React Navigation

React Navigation 导出两个主题

  • DefaultTheme
  • DarkTheme

我们可以从 @react-navigation/native 包中导入它们并传递给 NavigationContainer 以应用主题

import React from 'react';
import { NavigationContainer, DarkTheme } from '@react-navigation/native';

export default function App() {
return (
<NavigationContainer theme={DarkTheme}>{/* content */}</NavigationContainer>
);
}

React Native Paper

与 React Navigation 类似,React Native Paper 也导出两个主题

  • DefaultTheme
  • DarkTheme

导入主题后,我们可以将其传递给 Paper 的 Provider 组件

import * as React from 'react';
import { NavigationContainer, DarkTheme } from '@react-navigation/native';
import {
DarkTheme as PaperDarkTheme,
Provider as PaperProvider,
} from 'react-native-paper';

export default function Main() {
return (
<PaperProvider theme={PaperDarkTheme}>
<NavigationContainer theme={DarkTheme}>
{/* content */}
</NavigationContainer>
</PaperProvider>
);
}

组合主题

由于 React Navigation 和 React Native Paper 都遵循相同的主题化模式,并且主题对象的结构非常相似,我们可以将它们组合成一个对象

import * as React from 'react';
import {
NavigationContainer,
DarkTheme as NavigationDarkTheme,
} from '@react-navigation/native';
import {
DarkTheme as PaperDarkTheme,
Provider as PaperProvider,
} from 'react-native-paper';

const CombinedDarkTheme = {
...PaperDarkTheme,
...NavigationDarkTheme,
colors: { ...PaperDarkTheme.colors, ...NavigationDarkTheme.colors },
};

export default function Main() {
return (
<PaperProvider theme={CombinedDarkTheme}>
<NavigationContainer theme={CombinedDarkTheme}>
{/* content */}
</NavigationContainer>
</PaperProvider>
);
}

如果主题合并的代码看起来很复杂,您可以使用 deepmerge 包。它将大大简化实现。

自定义主题

当然,内置主题不是我们可以应用的唯一主题。这两个库都允许完全自定义,您可以在官方文档中了解它(React NavigationReact Native Paper

在最后一步中,我想向您展示如何动态更改主题。我们将在抽屉导航中实现一个开关,该开关允许用户选择浅色或深色主题。

我们需要将有关当前选定主题的信息存储在某个地方。根组件的本地状态听起来很合理。此外,我们将根据状态有条件地传递不同的主题。

import * as React from 'react';
import {
NavigationContainer,
DefaultTheme as NavigationDefaultTheme,
DarkTheme as NavigationDarkTheme,
} from '@react-navigation/native';
import {
DarkTheme as PaperDarkTheme,
DefaultTheme as PaperDefaultTheme,
Provider as PaperProvider,
} from 'react-native-paper';

const CombinedDefaultTheme = {
...PaperDefaultTheme,
...NavigationDefaultTheme,
};
const CombinedDarkTheme = { ...PaperDarkTheme, ...NavigationDarkTheme };

export default function Main() {
const [isDarkTheme, setIsDarkTheme] = React.useState(false);

const theme = isDarkTheme ? CombinedDarkTheme : CombinedDefaultTheme; // Use Light/Dark theme based on a state

function toggleTheme() {
// We will pass this function to Drawer and invoke it on theme switch press
setIsDarkTheme((isDark) => !isDark);
}

return (
<PaperProvider theme={theme}>
<NavigationContainer theme={theme}>{/* content */}</NavigationContainer>
</PaperProvider>
);
}

如您所记,我们已经在抽屉导航中渲染了一个开关,但我们还没有实现在按下开关时的任何逻辑。现在让我们处理它

import React from 'react';
import { View } from 'react-native';
import { DrawerContentScrollView } from '@react-navigation/drawer';
import {
useTheme,
Avatar,
Drawer,
Text,
TouchableRipple,
Switch,
} from 'react-native-paper';

export function DrawerContent(props) {
const paperTheme = useTheme();

return (
<DrawerContentScrollView {...props}>
/* {...other - content} */
<Drawer.Section title="Preferences">
<TouchableRipple onPress={props.toggleTheme}>
<View style={styles.preference}>
<Text>Dark Theme</Text>
<View pointerEvents="none">
<Switch value={theme.dark} />
</View>
</View>
</TouchableRipple>
</Drawer.Section>
</DrawerContentScrollView>
);
}

首先,我们使用来自 Paper 的 useTheme hook 获取当前主题。这意味着我们可以检查它的 dark 属性并将正确的值传递给 Switch
其次,我们将 toggleTheme 函数传递给 TouchableRipple,以便在用户按下开关时切换主题。

您现在应该可以切换开关了,Paper 的 Provider 和 React Navigation 的 NativeNavigationContainer 都将自动将正确的颜色应用于组件。


Theming with React Navigation and React Native Paper

总结

我们都知道像 Paper 这样的 UI 组件库可以加速开发,但将其与导航集成有时可能不是很直接。我希望在本指南中向您展示了这个过程中最重要的方面。阅读本文后,将 Paper 的 BottomNavigation、Appbar、Drawer、FAB 或 Portal 与 React Navigation 一起使用对您来说应该不是问题。

您有任何问题吗?在 Twitter 上给我发推文 @trensik

最后,我要感谢 @satya164 和整个 Callstack 团队对本文的帮助。