├── .gitignore
├── .watchmanconfig
├── App.js
├── README.md
├── app.json
├── assets
├── icon.png
└── splash.png
├── babel.config.js
├── custom-tab-bar.gif
├── package.json
├── src
├── AppEntry.tsx
├── components
│ ├── Icon.tsx
│ ├── TabBar.tsx
│ └── index.tsx
├── router
│ └── index.tsx
└── screens
│ ├── ScreenTemplate.tsx
│ └── index.tsx
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p12
6 | *.key
7 | *.mobileprovision
8 | yarn-error.log
9 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import AppEntry from "./src/AppEntry";
2 |
3 | export default AppEntry;
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-custom-tab-bar
2 | Custom animated Tab Bar component that works well with React Navigation
3 |
4 | 
5 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "ExpoPlayground",
4 | "slug": "ExpoPlayground",
5 | "privacy": "public",
6 | "sdkVersion": "32.0.0",
7 | "platforms": [
8 | "ios",
9 | "android"
10 | ],
11 | "version": "1.0.0",
12 | "orientation": "portrait",
13 | "icon": "./assets/icon.png",
14 | "splash": {
15 | "image": "./assets/splash.png",
16 | "resizeMode": "contain",
17 | "backgroundColor": "#ffffff"
18 | },
19 | "updates": {
20 | "fallbackToCacheTimeout": 0
21 | },
22 | "assetBundlePatterns": [
23 | "**/*"
24 | ],
25 | "ios": {
26 | "supportsTablet": true
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrastnik/react-native-custom-tab-bar/be1d02b8dd799730811c4d6c15a55f44530b1018/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrastnik/react-native-custom-tab-bar/be1d02b8dd799730811c4d6c15a55f44530b1018/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/custom-tab-bar.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrastnik/react-native-custom-tab-bar/be1d02b8dd799730811c4d6c15a55f44530b1018/custom-tab-bar.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "eject": "expo eject"
8 | },
9 | "dependencies": {
10 | "expo": "^32.0.0",
11 | "react": "16.5.0",
12 | "react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz",
13 | "react-native-pose": "^0.9.0",
14 | "react-navigation": "^3.1.2"
15 | },
16 | "devDependencies": {
17 | "@types/expo": "31.0.9",
18 | "@types/expo__vector-icons": "9.0.0",
19 | "@types/react": "16.8.1",
20 | "@types/react-native": "0.57.32",
21 | "@types/react-navigation": "3.0.1",
22 | "babel-preset-expo": "^5.0.0"
23 | },
24 | "private": true
25 | }
26 |
--------------------------------------------------------------------------------
/src/AppEntry.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Router from "./router";
4 |
5 | export default () => ;
6 |
--------------------------------------------------------------------------------
/src/components/Icon.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC } from "react";
2 | import { Text, TextProps } from "react-native";
3 |
4 | import { Ionicons } from "@expo/vector-icons";
5 |
6 | const nameMap = {
7 | A: "md-home",
8 | B: "logo-rss",
9 | C: "md-alarm",
10 | D: "md-basket",
11 | E: "md-build"
12 | };
13 |
14 | const Icon: SFC<
15 | {
16 | name: string;
17 | color: string;
18 | } & TextProps
19 | > = ({ name, color, style, ...props }) => {
20 | return (
21 |
28 | );
29 | };
30 |
31 | export default Icon;
32 |
--------------------------------------------------------------------------------
/src/components/TabBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC } from "react";
2 | import { View, StyleSheet, TouchableOpacity, Dimensions } from "react-native";
3 | import posed from "react-native-pose";
4 |
5 | const windowWidth = Dimensions.get("window").width;
6 |
7 | const Scaler = posed.View({
8 | active: { scale: 1.25 },
9 | inactive: { scale: 1 }
10 | });
11 |
12 | const S = StyleSheet.create({
13 | container: {
14 | flexDirection: "row",
15 | height: 52,
16 | elevation: 2,
17 | alignItems: "center"
18 | },
19 | spotlightInner: { width: 48, height: 48, borderRadius: 24 },
20 | tabButton: { flex: 1 },
21 | scaler: { flex: 1, alignItems: "center", justifyContent: "center" }
22 | });
23 |
24 | type Props = {
25 | tabColors: string[];
26 | renderIcon: SFC<{
27 | route: any;
28 | focused: boolean;
29 | tintColor: string;
30 | }>;
31 | activeTintColor: string;
32 | inactiveTintColor: string;
33 | onTabPress: ({ route: any }) => void;
34 | onTabLongPress: ({ route: any }) => void;
35 | getAccessibilityLabel: ({ route: any }) => string;
36 | navigation: any;
37 | };
38 |
39 | class TabBar extends React.Component {
40 | SpotLight = undefined;
41 | spotlightStyle = undefined;
42 | Inner = undefined;
43 |
44 | constructor(props) {
45 | super(props);
46 | this.init();
47 | }
48 |
49 | componentDidUpdate(prevProps) {
50 | const numTabs = this.props.navigation.state.routes.length;
51 | const prevNumTabs = prevProps.navigation.state.routes.length;
52 | if (numTabs !== prevNumTabs) {
53 | this.init();
54 | }
55 | }
56 |
57 | init() {
58 | const numTabs = this.props.navigation.state.routes.length;
59 |
60 | const tabWidth = windowWidth / numTabs;
61 |
62 | const poses = Array.from({ length: numTabs }).reduce((poses, _, index) => {
63 | return { ...poses, [`route${index}`]: { x: tabWidth * index } };
64 | }, {});
65 |
66 | const styles = StyleSheet.create({
67 | spotlight: {
68 | width: tabWidth,
69 | height: "100%",
70 | justifyContent: "center",
71 | alignItems: "center"
72 | }
73 | });
74 |
75 | this.SpotLight = posed.View(poses);
76 | this.spotlightStyle = styles.spotlight;
77 |
78 | const { tabColors } = this.props;
79 |
80 | this.Inner = posed.View({
81 | passive: {
82 | backgroundColor: [
83 | "x",
84 | {
85 | inputRange: Array.from({ length: numTabs }).map(
86 | (_, i) => i * tabWidth
87 | ),
88 | outputRange: tabColors
89 | },
90 | true
91 | ]
92 | }
93 | });
94 | }
95 |
96 | render() {
97 | const {
98 | renderIcon,
99 | activeTintColor,
100 | inactiveTintColor,
101 | onTabPress,
102 | onTabLongPress,
103 | getAccessibilityLabel,
104 | navigation
105 | } = this.props;
106 |
107 | const { routes, index: activeRouteIndex } = navigation.state;
108 |
109 | const { SpotLight, spotlightStyle, Inner } = this;
110 |
111 | return (
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | {routes.map((route, routeIndex) => {
120 | const isRouteActive = routeIndex === activeRouteIndex;
121 | const tintColor = isRouteActive ? activeTintColor : inactiveTintColor;
122 |
123 | return (
124 | {
128 | onTabPress({ route });
129 | }}
130 | onLongPress={() => {
131 | onTabLongPress({ route });
132 | }}
133 | accessibilityLabel={getAccessibilityLabel({ route })}
134 | >
135 |
139 | {renderIcon({ route, focused: isRouteActive, tintColor })}
140 |
141 |
142 | );
143 | })}
144 |
145 | );
146 | }
147 | }
148 |
149 | export default TabBar;
150 |
--------------------------------------------------------------------------------
/src/components/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as Icon } from "./Icon";
2 | export { default as TabBar } from "./TabBar";
3 |
--------------------------------------------------------------------------------
/src/router/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { createAppContainer, createBottomTabNavigator } from "react-navigation";
4 |
5 | import { Icon, TabBar } from "../components";
6 | import * as Screens from "../screens";
7 |
8 | const TabNavigator = createBottomTabNavigator(
9 | {
10 | HomeScreen: {
11 | screen: Screens.ScreenA,
12 | navigationOptions: {
13 | tabBarIcon: ({ tintColor }) =>
14 | }
15 | },
16 | SearchScreen: {
17 | screen: Screens.ScreenB,
18 | navigationOptions: {
19 | tabBarIcon: ({ tintColor }) =>
20 | }
21 | },
22 | FavoritesScreen: {
23 | screen: Screens.ScreenC,
24 | navigationOptions: {
25 | tabBarIcon: ({ tintColor }) =>
26 | }
27 | },
28 | ProfileScreen: {
29 | screen: Screens.ScreenD,
30 | navigationOptions: {
31 | tabBarIcon: ({ tintColor }) =>
32 | }
33 | },
34 | ProfileScreen2: {
35 | screen: Screens.ScreenE,
36 | navigationOptions: {
37 | tabBarIcon: ({ tintColor }) =>
38 | }
39 | }
40 | },
41 | {
42 | tabBarComponent: props => (
43 |
47 | ),
48 | tabBarOptions: {
49 | activeTintColor: "#eeeeee",
50 | inactiveTintColor: "#222222"
51 | }
52 | }
53 | );
54 |
55 | export default createAppContainer(TabNavigator);
56 |
--------------------------------------------------------------------------------
/src/screens/ScreenTemplate.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC } from "react";
2 | import { Text, View, StyleSheet } from "react-native";
3 |
4 | const S = StyleSheet.create({
5 | container: {
6 | flex: 1,
7 | backgroundColor: "#bbbbbb",
8 | justifyContent: "center",
9 | alignItems: "center"
10 | },
11 | text: { fontSize: 28, color: "#222222", textAlign: "center" }
12 | });
13 |
14 | const ScreenTemplate: SFC<{ name: string; color: string }> = ({
15 | name,
16 | color
17 | }) => (
18 |
19 | This is the "{name}" screen
20 |
21 | );
22 |
23 | export default ScreenTemplate;
24 |
--------------------------------------------------------------------------------
/src/screens/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Screen from "./ScreenTemplate";
4 |
5 | export const ScreenA = () => ;
6 | export const ScreenB = () => ;
7 | export const ScreenC = () => ;
8 | export const ScreenD = () => ;
9 | export const ScreenE = () => ;
10 |
--------------------------------------------------------------------------------