手把手教你如何自定义 React Native 底部导航栏

6次阅读

共计 9590 个字符,预计需要花费 24 分钟才能阅读完成。

如果你觉得 React Navigation 默认 Tab 组件看起来太平淡,或者想创造一些更现代的东西,那么你想法就和我一样。在本指南中,我将向你演示如何创建自定义标签栏以并与 React Navigation 一起使用。
源码已发布到 github, 如果有需要,请点击这里。
这是最终完成的样子:

首先,让我们初始化一个新项目并安装几个依赖项。在终端运行如下命令:
$ react-native init CustomTabBar
$ cd CustomTabBar
$ npm install react-navigation react-native-gesture-handler react-native-pose

React Navigation 从 V3 开始需要依赖 react-native-gesture-handler 库,react-native-pose 是一个很棒的库,我们将用它来制作非常简单的动画。
react-native-gesture-handler 需要通过 link 命令将一些配置自动关联到原生中。
react-native link react-native-gesture-handler

现在我们可以启动应用程序了。
首先——我们创建如下一个目录结构,方便代码管理:
/android
/ios

/src
/AppEntry.js
/router
/router.js
/index.js
/components
/screens
/index.js

首先,我们将创建一个 src 目录,将我们的代码与项目根目录中的其他文件 (package.json,app.json,.gitignore 等) 分开。screens,components 和 router 目录是知名其意的。
我们从项目的根目录中删除默认的 App.js 文件,并在 index.js 中写入 import /src/AppEntry.js
/* /index.js */

import {AppRegistry} from “react-native”;
import App from “./src/AppEntry”;
import {name as appName} from “./app.json”;

AppRegistry.registerComponent(appName, () => App);

现在我们想要使用 react-navigation 创建路由器,但是首先我们需要创建一些 screen(就是页面)。我们将创建一个通用的 Screen 组件,它接受一个名称并显示它来模拟多个 Screen。
在 /src/screens/index.js 添加如下内容:
/* /src/screens/index.js */
import React from “react”

import Screen from “./Screen”

export const HomeScreen = () => <Screen name=”Home”/>
export const SearchScreen = () => <Screen name=”Search” />
export const FavoritesScreen = () => <Screen name=”Favorites” />
export const ProfileScreen = () => <Screen name=”Profile” />;

现在我们创建 Screen 组件。
/* /src/screens/Screen.js */

import React from “react”;
import {Text, View, StyleSheet} from “react-native”;

const S = StyleSheet.create({
container: {
flex: 1,
backgroundColor: “#bbbbbb”,
justifyContent: “center”,
alignItems: “center”
},
text: {fontSize: 28, color: “#222222”, textAlign: “center”}
});

const Screen = ({name}) => (
<View style={S.container}>
<Text style={S.text}>This is the “{name}” screen</Text>
</View>
);

export default Screen;

接着创建路由,首先在 /src/router/index.js 在添加如下内容:
/* /src/router/index.js */

export {default as Router} from “./router”;

现在让我们在 router.js 中创建基本的 BottomTabNavigator。我们将导入 screens 并使用 createBottomTabNavigator 创建默认选项卡导航器。
/* /src/router/router.js */

import {createAppContainer, createBottomTabNavigator} from “react-navigation”;

import {
HomeScreen,
SearchScreen,
FavoritesScreen,
ProfileScreen
} from “../screens”;

const TabNavigator = createBottomTabNavigator({
HomeScreen,
SearchScreen,
FavoritesScreen,
ProfileScreen
});

export default createAppContainer(TabNavigator);

现在我们在 AppEntry.js 中渲染路由:
/* /src/AppEntry.js */

import React from “react”;

import {Router} from “./router”;

export default () => <Router />;

当我们重新加载应用程序时,应该会如下内容:

默认标签栏支持图标,我们将在本教程中使用 ascii 字符,当然在实际应用中可以使用 react-native-vector-icons 或自定义图标字体。
让我们创建一个 Icon 组件,接受参数为 name 和 color 并返回图标。
/* /src/components/index.js */

export {default as Icon} from “./Icon”;

/ /src/components/Icon.js /
import React from “react”;
import {Text} from “react-native”;

const iconMap = {
home: “♡”,
search: “♢”,
favorites: “♧”,
profile: “♤”
};

const Icon = ({name, color, style, …props}) => {
const icon = iconMap[name];

return <Text style={[{fontSize: 26, color}, style]}>{icon}</Text>;
};

export default Icon;

现在我们可以在路由器中使用这个组件。我们在 router.js 中更改 screens,以接受带有 navigationOptions 配置的对象。默认选项卡栏将 tintColor 传递给图标组件,因此我们使用它来设置图标颜色。
/* /src/router/router.js */

import {createAppContainer, createBottomTabNavigator} from “react-navigation”;
import React from “react”;
import {
HomeScreen,
SearchScreen,
FavoritesScreen,
ProfileScreen
} from “../screens”;

import {Icon} from ‘../components’

const TabNavigator = createBottomTabNavigator({
HomeScreen: {
screen: HomeScreen,
navigationOptions: {
tabBarIcon: ({tintColor}) => <Icon name=”home” color={tintColor} />
}
},
SearchScreen: {
screen: SearchScreen,
navigationOptions: {
tabBarIcon: ({tintColor}) => <Icon name=”search” color={tintColor} />
}
},
FavoritesScreen: {
screen: FavoritesScreen,
navigationOptions: {
tabBarIcon: ({tintColor}) => <Icon name=”favorites” color={tintColor} />
}
},
ProfileScreen: {
screen: ProfileScreen,
navigationOptions: {
tabBarIcon: ({tintColor}) => <Icon name=”profile” color={tintColor} />
}
}
});
export default createAppContainer(TabNavigator);

运行效果如下:

现在我们的标签栏看起来好一点,但它仍然是 react-navigation 的默认标签栏。接下来,我们将添加实际的自定义标签栏组件。
让我们从创建一个自定义 TabBar 组件开始,该组件只渲染一些文本并打印传递过来的 props,这样我们就可以看到我们从导航器中得到了什么 props。
/* /src/components/index.js */

export {default as Icon} from “./Icon”;
export {default as TabBar} from “./TabBar”;

/* /src/components/TabBar.js */

import React from “react”;
import {Text} from “react-native”;

const TabBar = props => {
console.log(“Props”, props);

return <Text>Custom Tab Bar</Text>;
};

export default TabBar;

使用自定义标签栏需要配置 createBottomTabNavigator 第二个参数,我们可以添加以下配置作为 createBottomTabNavigator 的第二个参数。
如果我们查看标签栏打印了什么,我们会看到导航栏中有 navigation.state 状态,其中也包含路由。还有 renderIcon 函数,onTabPress 和很多我们可能需要的东西。此外,我们还注意到我们在路由器配置中 tabBarOptions 是如何被注入到组件中的。
现在重新编写 TabBar 组件。首先,让我们尝试重新创建默认选项卡栏。我们将在容器上设置一些样式,以便将选项卡按钮排成一行,并为每个路由呈现一个选项卡按钮。我们可以使用 renderIcon 函数来渲染正确的图标——通过查看源代码,该函数需要传入一个对象参数:{route, focused, tintColor}。我们添加了 onPress 处理程序、易访问性标签,这样就有了默认的选项卡栏。
/* /src/components/TabBar.js */

import React from “react”;
import {View, Text, StyleSheet, TouchableOpacity} from “react-native”;

const S = StyleSheet.create({
container: {flexDirection: “row”, height: 52, elevation: 2},
tabButton: {flex: 1, justifyContent: “center”, alignItems: “center”}
});

const TabBar = props => {
const {
renderIcon,
getLabelText,
activeTintColor,
inactiveTintColor,
onTabPress,
onTabLongPress,
getAccessibilityLabel,
navigation
} = props;

const {routes, index: activeRouteIndex} = navigation.state;

return (
<View style={S.container}>
{routes.map((route, routeIndex) => {
const isRouteActive = routeIndex === activeRouteIndex;
const tintColor = isRouteActive ? activeTintColor : inactiveTintColor;

return (
<TouchableOpacity
key={routeIndex}
style={S.tabButton}
onPress={() => {
onTabPress({route});
}}
onLongPress={() => {
onTabLongPress({route});
}}
accessibilityLabel={getAccessibilityLabel({ route})}
>
{renderIcon({ route, focused: isRouteActive, tintColor})}

<Text>{getLabelText({ route})}</Text>
</TouchableOpacity>
);
})}
</View>
);
};

export default TabBar;

运行后,效果如下:

现在我们知道我们可以灵活地创建自己的标签栏,因此我们可以开始实际扩展它。我们将使用 react-native-pose 创建一个动画视图,该视图将突出显示活动路径 – 我们将此视图称为聚光灯。
首先我们可以去掉标签。然后我们在标签栏后面添加一个绝对视图,它将显示聚光灯效果。我们使用 Dimensions API 计算聚光灯的偏移量。
/* /src/components/TabBar.js */

import React from “react”;
import {View, Text, StyleSheet, TouchableOpacity, Dimensions} from “react-native”;
import posed from “react-native-pose”;

const windowWidth = Dimensions.get(“window”).width;
const tabWidth = windowWidth / 4;

const SpotLight = posed.View({
route0: {x: 0},
route1: {x: tabWidth},
route2: {x: tabWidth * 2},
route3: {x: tabWidth * 3}
});

const S = StyleSheet.create({
container: {flexDirection: “row”, height: 52, elevation: 2},
tabButton: {flex: 1, justifyContent: “center”, alignItems: “center”},
spotLight: {
width: tabWidth,
height: “100%”,
backgroundColor: “rgba(128,128,255,0.2)”,
borderRadius: 8
}
});

const TabBar = props => {
const {
renderIcon,
getLabelText,
activeTintColor,
inactiveTintColor,
onTabPress,
onTabLongPress,
getAccessibilityLabel,
navigation
} = props;

const {routes, index: activeRouteIndex} = navigation.state;

return (
<View style={S.container}>
<View style={StyleSheet.absoluteFillObject}>
<SpotLight style={S.spotLight} pose={`route${activeRouteIndex}`} />
</View>
{routes.map((route, routeIndex) => {
const isRouteActive = routeIndex === activeRouteIndex;
const tintColor = isRouteActive ? activeTintColor : inactiveTintColor;

return (
<TouchableOpacity
key={routeIndex}
style={S.tabButton}
onPress={() => {
onTabPress({route});
}}
onLongPress={() => {
onTabLongPress({route});
}}
accessibilityLabel={getAccessibilityLabel({ route})}
>
{renderIcon({ route, focused: isRouteActive, tintColor})}

<Text>{getLabelText({ route})}</Text>
</TouchableOpacity>
);
})}
</View>
);
};

export default TabBar;

运行效果如下:

请注意,我们从未指定动画的持续时间和行为,Pos e 负责使用合理的默认值。
现在我们将为选中图标添加一些缩放:
/* /src/components/TabBar.js */

const Scaler = posed.View({
active: {scale: 1.25},
inactive: {scale: 1}
});

现在我们可以像这样将图标包装在 Scaler 组件中。
/* /src/components/TabBar.js */

<Scaler style={S.scaler} pose={isRouteActive ? “active” : “inactive”}>
{renderIcon({ route, focused: isRouteActive, tintColor})}
</Scaler>

运行效果如下:

我们的标签栏开始看起来很不错。剩下要做的就是稍微改善一下,改变配色方案,调整我们的聚光灯,我们的组件就完成了。

现在,我们可以在这里改进一些事情。例如,当前的实现假设选项卡导航器中总会有 4 个 Screen,聚光灯颜色在选项卡栏组件中是写死。样式应该通过路由器上的 tabBarOptions 配置进行动态编写的,这边不会讲这些,大家自己动手做做。
TabBar 组件的完整代码:
/* /src/components/TabBar.js */

import React from “react”;
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Dimensions
} from “react-native”;
import posed from “react-native-pose”;

const windowWidth = Dimensions.get(“window”).width;
const tabWidth = windowWidth / 4;
const SpotLight = posed.View({
route0: {x: 0},
route1: {x: tabWidth},
route2: {x: tabWidth * 2},
route3: {x: tabWidth * 3}
});

const Scaler = posed.View({
active: {scale: 1.25},
inactive: {scale: 1}
});

const S = StyleSheet.create({
container: {
flexDirection: “row”,
height: 52,
elevation: 2,
alignItems: “center”
},
tabButton: {flex: 1},
spotLight: {
width: tabWidth,
height: “100%”,
justifyContent: “center”,
alignItems: “center”
},
spotLightInner: {
width: 48,
height: 48,
backgroundColor: “#ee0000”,
borderRadius: 24
},
scaler: {flex: 1, alignItems: “center”, justifyContent: “center”}
});

const TabBar = props => {
const {
renderIcon,
activeTintColor,
inactiveTintColor,
onTabPress,
onTabLongPress,
getAccessibilityLabel,
navigation
} = props;

const {routes, index: activeRouteIndex} = navigation.state;

return (
<View style={S.container}>
<View style={StyleSheet.absoluteFillObject}>
<SpotLight style={S.spotLight} pose={`route${activeRouteIndex}`}>
<View style={S.spotLightInner} />
</SpotLight>
</View>

{routes.map((route, routeIndex) => {
const isRouteActive = routeIndex === activeRouteIndex;
const tintColor = isRouteActive ? activeTintColor : inactiveTintColor;

return (
<TouchableOpacity
key={routeIndex}
style={S.tabButton}
onPress={() => {
onTabPress({route});
}}
onLongPress={() => {
onTabLongPress({route});
}}
accessibilityLabel={getAccessibilityLabel({ route})}
>
<Scaler
pose={isRouteActive ? “active” : “inactive”}
style={S.scaler}
>
{renderIcon({ route, focused: isRouteActive, tintColor})}
</Scaler>
</TouchableOpacity>
);
})}
</View>
);
};

export default TabBar;

路由器配置如下:
/ /src/router/router.js /

const TabNavigator = createBottomTabNavigator(
/* screen config ommited */,
{
tabBarComponent: TabBar,
tabBarOptions: {
activeTintColor: “#eeeeee”,
inactiveTintColor: “#222222”
}
}
);

你的点赞是我持续分享好东西的动力,欢迎点赞!
欢迎加入前端大家庭,里面会经常分享一些技术资源。

正文完
 0