├── .babelrc
├── .editorconfig
├── .github
├── ISSUE_TEMPLATE.md
├── behind-nav-bar.gif
├── color-3-tab.gif
├── color-4-tab.gif
├── white-3-tab.gif
└── white-4-tab.gif
├── .npmignore
├── LICENSE.md
├── README.md
├── example
├── ReactNavigationBottomNavigation.js
├── SimpleBottomNavigation.js
└── StatefulBottomNavigation.js
├── index.js
├── lib
├── BottomNavigation.js
├── NavigationComponent.js
├── PressRipple.js
├── RippleBackgroundTransition.js
├── Tab.js
└── utils
│ └── easing.js
└── package.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react-native"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.md]
11 | trim_trailing_whitespace = false
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
19 |
20 | ### What kind of Issue is this?
21 |
22 |
23 |
24 | - () Bug Report
25 | - () Question / Problem
26 | - () Discussion / Feature Request
27 |
28 |
32 |
33 | ### How are you using the Bottom Navigation?
34 |
35 |
50 |
51 | - () I use the Bottom Navigation together with react-navigation; the Issue is not appearing when I'm using react-navigation's TabBar instead.
52 | - () I use the standalone version.
53 |
54 | Related Libraries: {If you use the standalone version together with other libraries, please list them here. If not, delete this line.}
55 |
56 | ### Expected behavior
57 |
58 | {Write down what you expected to happen. Add code snippets later.}
59 |
60 | ### Actual behavior
61 |
62 | {Write down what is actually happening. Add code snippets later.}
63 |
64 | ### Additional description and resources
65 |
66 | {Write down everything else you want to say.}
67 |
68 | {Place a reproducible example here.
69 | The best thing you can do is to provide a minimal example which reproduces your Issue in Expo's Snack: https://snack.expo.io
70 | Otherwise: Add all relevant Code Snippets here. Add screenshots/gifs showing your issue in action.}
71 |
72 | ### What did you do to find a solution?
73 |
74 |
87 |
88 | {List all the things you've done and/or places and search-terms you used to find a solution.}
89 |
90 | ### Environment
91 |
92 |
96 |
97 | - **React Native versions:** {Insert here.}
98 | - **react-native-material-bottom-navigation version:** {Insert here.}
99 | - **react-navigation version:** {Insert here.}
100 | - **I tested this with:** {Please choose one or more: Android | iOS | Web | Windows}
101 |
102 |
108 |
--------------------------------------------------------------------------------
/.github/behind-nav-bar.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomzaku/react-native-material-bottom-navigation-performance/7bc2930b59000cfb017df8635f0215e336263ae4/.github/behind-nav-bar.gif
--------------------------------------------------------------------------------
/.github/color-3-tab.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomzaku/react-native-material-bottom-navigation-performance/7bc2930b59000cfb017df8635f0215e336263ae4/.github/color-3-tab.gif
--------------------------------------------------------------------------------
/.github/color-4-tab.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomzaku/react-native-material-bottom-navigation-performance/7bc2930b59000cfb017df8635f0215e336263ae4/.github/color-4-tab.gif
--------------------------------------------------------------------------------
/.github/white-3-tab.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomzaku/react-native-material-bottom-navigation-performance/7bc2930b59000cfb017df8635f0215e336263ae4/.github/white-3-tab.gif
--------------------------------------------------------------------------------
/.github/white-4-tab.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomzaku/react-native-material-bottom-navigation-performance/7bc2930b59000cfb017df8635f0215e336263ae4/.github/white-4-tab.gif
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github/
2 | .editorconfig
3 | example
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2017 Timo Mämecke
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Material Design Bottom Navigation for react-native
2 |
3 | A highly accurate Bottom Navigation Component for react-native, based on [Material Guidelines' Bottom Navigation](https://material.io/guidelines/components/bottom-navigation.html).
4 |
5 | * Support for iOS and Android (it's programmed only in JavaScript)
6 | * Uses those dope Ripple Transitions between two background colors
7 | * Follows the Material Design Guidelines
8 | * Switches automatically between Fixed Navigation (up to 3 tabs) and Shifting Navigation (3 - 5 tabs)
9 | * No dependencies
10 | * Support for [react-navigation](https://reactnavigation.org)
11 |
12 | The Bottom navigation looks lovely. That's probably the reason why you're here. Using a Bottom Navigation is a good choice. More and more apps are switching from a Burger Menu and/or [Tabs](https://material.io/guidelines/components/tabs.html) to a Bottom Navigation, including Google Apps.
13 |
14 | **Fixed Bottom Navigation**
15 |
16 |  
17 |
18 | **Shifting Bottom Navigation**
19 |
20 |  
21 |
22 | **Behind the Android System Navigation Bar**
23 |
24 | 
25 |
26 |
27 | - [Install](#install)
28 | - [But how? (Usage)](#but-how)
29 | - [Configuration](#configuration)
30 | - [Behind the Navigation Bar](#behind-the-navigation-bar)
31 | - [Usage for react-navigation](#usage-for-react-navigation)
32 | - [Roadmap](#roadmap)
33 | - [LICENSE](#license)
34 |
35 | ## Install
36 |
37 | ```sh
38 | # via npm
39 | $ npm install react-native-material-bottom-navigation-performance --save
40 |
41 | # via yarn
42 | $ yarn add react-native-material-bottom-navigation-performance
43 | ```
44 |
45 |
46 | ## But how?
47 |
48 | This is an example for a Bottom Navigation with 4 Tabs, each Tab has its own background color.
49 |
50 | In this example, I used [react-native-vector-icons](https://github.com/oblador/react-native-vector-icons) as Icon Components. You can use whatever Component you want.
51 |
52 | ```jsx
53 | import React, { Component } from 'react'
54 | import BottomNavigation, { Tab } from 'react-native-material-bottom-navigation'
55 | import Icon from 'react-native-vector-icons/MaterialIcons'
56 |
57 | class MyComponent extends Component {
58 | render() {
59 | return (
60 | alert(`New Tab at position ${newTabIndex}`)}
65 | >
66 | }
70 | />
71 | }
75 | />
76 | }
80 | />
81 | }
85 | />
86 |
87 | )
88 | }
89 | }
90 | ```
91 |
92 | ## Configuration
93 |
94 | Don't skip this part. You will be happy to know about all the good stuff you can configure here.
95 |
96 | **Note:** If you are searching for more customization options, like label styles for fonts/positioning/..., they are *intentionally* not supported. More and more customizations would be actively against the Material Design Guidelines, and I want to encourage you to follow the Guidelines.
97 |
98 | ### BottomNavigation
99 |
100 | | Prop | Description | Type | Default |
101 | |------|--------------|------|--------|
102 | | **`activeTab`** | Index of the preselected Tab, starting from 0. | `number` | `0` |
103 | | **`labelColor`** | Text Color of the Tab's Label. Can be overwritten by the Tab itself. | `string` | `rgba(0, 0, 0, 0.54)` |
104 | | **`activeLabelColor`** | Text Color of the active Tab's Label. Can be overwritten by the Tab itself. | `string` | `labelColor` |
105 | | **`rippleColor`** | Color of the small Ripple Effect when the Tab will be pressed. Has opacity of `0.12`. | `string` | `black` |
106 | | **`backgroundColor`** | Background color of the Bottom Navigation. Can be overwritten by the Tab itself, to achieve different background colors for each active Tab. | `string` | `white` |
107 | | **`onTabChange`** | Function to be called when a Tab was pressed and changes into active state. Will be called with parameters `(newTabIndex, oldTabIndex) => {}`. | `function` | `noop` |
108 | | **`style`** | **Required.** Style will be directly applied to the component. Use this to set the height of the BottomNavigation (should be 56), to position it, to add shadow and border. The only pre-set rule is `overflow: hidden`. | `object` | **Required.** |
109 | | **`innerStyle`** | All tabs are wrapped in another container. Use this to add styles to this container. The main reason why you would want to use this is to put the Navigation behind the Android System Navigation Bar. See below for an example on how to achieve this. | `object` | – |
110 | | **`shifting`** | Turn manually on/off shifting mode. | `boolean` | `true` if > 3 Tabs, otherwise `false` |
111 |
112 | **Hints:**
113 |
114 | - Elevation should be `8`
115 | - Height should be `56`
116 | - Width should be 100%
117 | - Follow all specs defined in the [Official Guidelines](https://material.io/guidelines/components/bottom-navigation.html#bottom-navigation-specs)
118 |
119 |
120 | ### Tab
121 |
122 | | Prop | Description | Type | Default |
123 | |------|--------------|------|--------|
124 | | **`icon`** | **Required.** Component to render as icon. Should have height and width of `24`. | `ReactElement<*>` | **Required.** |
125 | | **`activeIcon`** | Component to render as icon when the Tab is active. Should have height and width of `24`. Use this to change the color of the icon. | `ReactElement<*>` | `icon` |
126 | | **`label`** | **Required.** Text of the Label. | `string` | **Required.** |
127 | | **`labelColor`** | Text Color of the Label. | `string` | `labelColor` of BottomNavigation |
128 | | **`activeLabelColor`** | Text Color of the Label when the Tab is active. | `string` | `activeLabelColor` of BottomNavigation |
129 | | **`barBackgroundColor`** | Background color for the whole component, if the tab is active. | `string` | `backgroundColor` of BottomNavigation |
130 | | **`onPress`** | Function to be called when the Tab was pressed. **When you use this, the pressed tab won't be active automatically. You need to set it to active by updating `BottomNavigation.activeTab`.** This function will be called with the parameter `(newTabIndex) => {}` | `function` | – |
131 |
132 |
133 | ## Behind the Navigation Bar
134 |
135 | In the Material Design Guidelines you can see examples with the Bottom Navigation behind the Software Navigation Bar. That looks pretty sweet. In theory, that's pretty simple. In practice there's a problem: Not every device has a visible Navigation Bar. If someone has hardware buttons on his phone, the Navigation Bar is usually hidden. As of now, we can't simply detect if it's visible. If you don't detect it and just add the following code, the BottomNavigation will have a huge padding-bottom on devices without a Navigation Bar.
136 |
137 | See [Issue #28](https://github.com/timomeh/react-native-material-bottom-navigation/issues/28) for more informations with an initial proposal by @keeleycarrigan.
138 |
139 | However, if you know what you're doing, you only need to adjust a few things:
140 |
141 | **Step 1.** In order to make the System Navigation translucent, you have to add this to `android/app/src/main/res/values/styles.xml`:
142 |
143 | ```xml
144 |
145 | - @android:color/transparent
146 | - true
147 | ```
148 |
149 | **Step 2.** The System Navigation has a height of 48dp. The Bottom Navigation should be 56dp tall. This makes a total height of 104. Use `innerStyle` to push the tabs above the System Navigation without pushing the whole Bottom Navigation above it.
150 |
151 | ```jsx
152 |
156 | ```
157 |
158 | **Step 3.** You're done!
159 |
160 |
161 | ## Usage for [react-navigation](https://reactnavigation.org)
162 |
163 | This package includes a Component to plug into react-navigation. It is as configurable as the standalone version. To achieve this, it uses a separate configuration inside `tabBarOptions`. You can only set those configurations for the Bottom Navigation inside the `TabNavigatorConfig` of `TabNavigator()` – **not inside `static navigationOptions` or inside the `RouteConfigs`**.
164 |
165 | The following example will explain everything you need to get started.
166 |
167 | ```jsx
168 |
169 | import React from 'react'
170 | import { NavigationComponent } from 'react-native-material-bottom-navigation'
171 | import { TabNavigator } from 'react-navigation'
172 | import { AppRegistry } from 'react-native';
173 |
174 | class MoviesAndTV extends React.Component {
175 | static navigationOptions = {
176 | tabBarLabel: 'Movies & TV',
177 | tabBarIcon: () => ()
178 | }
179 |
180 | render() { ... }
181 | }
182 |
183 | class Music extends React.Component {
184 | static navigationOptions = {
185 | tabBarLabel: 'Music',
186 | tabBarIcon: () => ()
187 | }
188 |
189 | render() { ... }
190 | }
191 |
192 | class Newsstand extends React.Component {
193 | static navigationOptions = {
194 | tabBarLabel: 'Newsstand',
195 | tabBarIcon: () => ()
196 | }
197 |
198 | render() { ... }
199 | }
200 |
201 | const MyApp = TabNavigator({
202 | MoviesAndTV: { screen: MoviesAndTV },
203 | Music: { screen: Music },
204 | Newsstand: { screen: Newsstand }
205 | }, {
206 | tabBarComponent: NavigationComponent,
207 | tabBarPosition: 'bottom',
208 | tabBarOptions: {
209 | bottomNavigationOptions: {
210 | labelColor: 'white',
211 | rippleColor: 'white',
212 | tabs: {
213 | MoviesAndTV: {
214 | barBackgroundColor: '#37474F'
215 | },
216 | Music: {
217 | barBackgroundColor: '#00796B'
218 | },
219 | Newsstand: {
220 | barBackgroundColor: '#EEEEEE',
221 | labelColor: '#434343', // like in the standalone version, this will override the already specified `labelColor` for this tab
222 | activeLabelColor: '#212121',
223 | activeIcon:
224 | }
225 | }
226 | }
227 | }
228 | })
229 |
230 | AppRegistry.registerComponent('MyApp', () => MyApp)
231 | ```
232 |
233 | ### [TabNavigatorConfig](https://reactnavigation.org/docs/navigators/tab#TabNavigatorConfig)
234 |
235 | - `tabBarComponent`: Use `NavigationComponent` provided by `react-native-material-bottom-navigation`.
236 | - `tabBarPosition`: Use `bottom`.
237 | - `tabBarOptions`: react-navigation's configuration of the tab bar.
238 |
239 |
240 | ### tabBarOptions
241 |
242 | The only options, which will affect the Bottom Navigation, are the following:
243 |
244 | - `style`: Corresponds to the `style` prop of [`BottomNavigation`](#BottomNavigation). If no height is specified, it will use `height: 56`. This way you don't need any styling in most cases.
245 | - `bottomNavigationOptions`: The options for the Bottom Navigation, see below.
246 |
247 |
248 | ### bottomNavigationOptions
249 |
250 | All options of [`BottomNavigation`](#BottomNavigation) are available. They behave like the options in the standalone version, including fallback- and default-behaviour.
251 |
252 | - **`labelColor`**
253 | - **`activeLabelColor`**
254 | - **`rippleColor`**
255 | - **`backgroundColor`**
256 | - **`style`**: If specified, `tabBarOptions.style` won't be used.
257 | - **`innerStyle`**
258 | - **`shifting`**
259 | - **`tabs`**: Configuration for the tabs, see below.
260 |
261 | *Note: `activeTab` and `onTabChange` don't have any effect, since this is handled by react-navigation.*
262 |
263 |
264 | ### tabs
265 |
266 | Each tab can be configured by its key from `RouteConfigs`. *If you take a look at the example, you will see that `MoviesAndTV`, `Music` and `Newsstand` correspond to each other.*
267 |
268 | - **`tab`** is an object with `{ [routeKey]: tabOptions }`
269 |
270 | ### tabOptions
271 |
272 | All options of [`Tab`](#Tab) are available. They behave like the options in the standalone version, including fallback- and default-behaviour.
273 |
274 | - **`icon`**: If not specified, the icon inside `static navigationOptions.tabBar` of the scene will be used.
275 | - **`activeIcon`**
276 | - **`label`**: If not specified, the label inside `static navigationOptions.tabBar` of the scene will be used.
277 | - **`labelColor`**
278 | - **`activeLabelColor`**
279 | - **`barBackgroundColor`**
280 |
281 |
282 | ### Why don't you use all the options provided by react-navigation?
283 |
284 | At the time I developed this, react-navigation was in an early beta stage. It wasn't easy to get those options and add new options. I could only access the configs inside `tabBarOptions`, hence everything is stored there.
285 |
286 | ## Roadmap
287 |
288 | Check if they are any new features announced in the [Issues](https://github.com/timomeh/react-native-material-bottom-navigation/issues).
289 |
290 | ## [LICENSE](LICENSE.md)
291 |
292 | MIT
293 |
--------------------------------------------------------------------------------
/example/ReactNavigationBottomNavigation.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { AppRegistry, StyleSheet, Text, View, Button } from 'react-native'
3 | import { NavigationComponent } from 'react-native-material-bottom-navigation'
4 | import { TabNavigator } from 'react-navigation'
5 | import Icon from 'react-native-vector-icons/MaterialIcons'
6 |
7 |
8 | /**
9 | * Screen for first tab.
10 | * You usually will have this in a separate file.
11 | */
12 | class MoviesAndTV extends Component {
13 | static navigationOptions = {
14 | tabBarLabel: "Movies & TV",
15 | tabBarIcon: () =>
16 | }
17 |
18 | render() {
19 | return Movies & TV
20 | }
21 | }
22 |
23 | /**
24 | * Screen for second tab.
25 | * You usually will have this in a separate file.
26 | */
27 | class Music extends Component {
28 | static navigationOptions = {
29 | tabBarLabel: "Music",
30 | tabBarIcon: () =>
31 | }
32 |
33 | render() {
34 | return Music
35 | }
36 | }
37 |
38 | /**
39 | * Screen for third tab.
40 | * You usually will have this in a separate file.
41 | */
42 | class Books extends Component {
43 | static navigationOptions = {
44 | tabBarLabel: "Books",
45 | tabBarIcon: () =>
46 | }
47 |
48 | render() {
49 | return Books
50 | }
51 | }
52 |
53 | /**
54 | * react-navigation's TabNavigator.
55 | */
56 | const MyApp = TabNavigator({
57 | MoviesAndTV: { screen: MoviesAndTV },
58 | Music: { screen: Music },
59 | Books: { screen: Books }
60 | }, {
61 | tabBarComponent: NavigationComponent,
62 | tabBarPosition: 'bottom',
63 | tabBarOptions: {
64 | bottomNavigationOptions: {
65 | labelColor: 'white',
66 | rippleColor: 'white',
67 | tabs: {
68 | MoviesAndTV: {
69 | barBackgroundColor: '#37474F'
70 | },
71 | Music: {
72 | barBackgroundColor: '#00796B'
73 | },
74 | Books: {
75 | barBackgroundColor: '#5D4037'
76 | }
77 | }
78 | }
79 | }
80 | })
81 |
82 | AppRegistry.registerComponent('MyApp', () => MyApp)
83 |
--------------------------------------------------------------------------------
/example/SimpleBottomNavigation.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { View, StyleSheet } from 'react-native'
3 | import BottomNavigation, { Tab } from 'react-native-material-bottom-navigation'
4 | import Icon from 'react-native-vector-icons/MaterialIcons'
5 |
6 |
7 | export default class SimpleBottomNavigation extends Component {
8 | render() {
9 | return (
10 |
11 |
16 | }
20 | />
21 | }
25 | />
26 | }
30 | />
31 | }
35 | />
36 |
37 |
38 | )
39 | }
40 | }
41 |
42 | const styles = StyleSheet.create({
43 | bottomNavigation: {
44 | position: 'absolute',
45 | left: 0,
46 | right: 0,
47 | bottom: 0,
48 | height: 56
49 | }
50 | })
51 |
--------------------------------------------------------------------------------
/example/StatefulBottomNavigation.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { View, StyleSheet } from 'react-native'
3 | import BottomNavigation, { Tab } from 'react-native-material-bottom-navigation'
4 | import Icon from 'react-native-vector-icons/MaterialIcons'
5 |
6 | /**
7 | * In this Example, the active Tab will be stored in the state.
8 | */
9 |
10 | export default class StatefulBottomNavigation extends Component {
11 | constructor(props) {
12 | super(props)
13 |
14 | this.state = { activeTab: 0 }
15 | this.handleTabChange = this.handleTabChange.bind(this)
16 | }
17 |
18 | handleTabChange(newTabIndex, oldTabIndex) {
19 | this.setState({ activeTab: newTabIndex })
20 | }
21 |
22 | render() {
23 | return (
24 |
25 |
32 | }
36 | />
37 | }
41 | />
42 | }
46 | />
47 | }
51 | />
52 |
53 |
54 | )
55 | }
56 | }
57 |
58 | const styles = StyleSheet.create({
59 | bottomNavigation: {
60 | position: 'absolute',
61 | left: 0,
62 | right: 0,
63 | bottom: 0,
64 | height: 56
65 | }
66 | })
67 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // Hi!
2 | //
3 | // Timo Mämecke, 2017, @timomeh
4 | // https://twitter.com/timomeh
5 | // https://github.com/timomeh
6 | //
7 |
8 | export { default } from './lib/BottomNavigation'
9 | export { default as Tab } from './lib/Tab'
10 | export { default as NavigationComponent } from './lib/NavigationComponent'
11 |
--------------------------------------------------------------------------------
/lib/BottomNavigation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Bottom Navigation like Material Design Guidelines.
3 | * As best as I can. Everybody freak out.
4 | * @flow
5 | */
6 |
7 | import React, { Component } from 'react'
8 | import {
9 | View,
10 | Platform,
11 | UIManager,
12 | StyleSheet,
13 | findNodeHandle,
14 | LayoutAnimation,
15 | TouchableWithoutFeedback
16 | } from 'react-native'
17 | import RippleBackgroundTransition from './RippleBackgroundTransition'
18 | import PressRipple from './PressRipple'
19 | import Tab from './Tab'
20 |
21 |
22 | type BottomNavigationProps = {
23 | activeTab: number,
24 | labelColor: string,
25 | activeLabelColor: string,
26 | rippleColor: string,
27 | backgroundColor: string,
28 | shifting?: ?boolean,
29 | onTabChange: () => void,
30 | style: any,
31 | innerStyle: any,
32 | children: Array>
33 | }
34 |
35 | type BottomNavigationState = {
36 | activeTab: number,
37 | backgroundColor: string,
38 | pressRippleColor: string,
39 | rippleX: number,
40 | rippleY: number
41 | }
42 |
43 | const defaultProps = {
44 | activeTab: 0,
45 | labelColor: 'rgba(0, 0, 0, 0.54)',
46 | rippleColor: 'black',
47 | backgroundColor: 'white',
48 | onTabChange: () => {}
49 | }
50 |
51 | export default class BottomNavigation extends Component {
52 |
53 | static defaultProps: typeof defaultProps
54 | props: BottomNavigationProps
55 | state: BottomNavigationState
56 | layoutWillChange: boolean
57 | nextActiveTab: number
58 | iconPositions: Array<{ x: number, y: number }>
59 | dimensions: { width: number, height: number }
60 |
61 | static defaultProps = defaultProps
62 |
63 | constructor(props: BottomNavigationProps) {
64 | super(props)
65 |
66 | // Default values
67 | this.layoutWillChange = false
68 | this.lastTabChangeDate = -1
69 | this.dimensions = { width: -1, height: -1 }
70 | this.nextActiveTab = props.activeTab
71 | this.state = {
72 | activeTab: props.activeTab,
73 | backgroundColor: props.backgroundColor,
74 | pressRippleColor: 'transparent',
75 | rippleX: 0,
76 | rippleY: 0
77 | }
78 |
79 | if (props.activeLabelColor == null) {
80 | this.props.activeLabelColor = this.props.labelColor
81 | }
82 |
83 | // Enable LayoutAnimations on Android
84 | if (Platform.OS === 'android') {
85 | UIManager.setLayoutAnimationEnabledExperimental &&
86 | UIManager.setLayoutAnimationEnabledExperimental(true)
87 | }
88 | }
89 |
90 | componentWillMount() {
91 | const { children } = this.props
92 | const { activeTab } = this.state
93 | const { barBackgroundColor } = children[activeTab].props
94 |
95 | this.iconPositions = new Array(children.length).fill({ x: 0, y: 0 })
96 |
97 | if (children.length > 5) {
98 | if (__DEV__) {
99 | console.warn('You shouldn\'t put more than 5 Tabs in the ' +
100 | 'BottomNavigation. Styling may break and it\'s against the specs ' +
101 | 'in the Material Design Guidelines.')
102 | }
103 | }
104 |
105 | // Set Initial Bar backgroundColor, if Tab has any
106 | if (barBackgroundColor) {
107 | this.setState({
108 | backgroundColor: barBackgroundColor
109 | })
110 | }
111 | }
112 |
113 | componentDidMount() {
114 | // Measure all icons in order to display Ripples correctly
115 | setTimeout(() => this._measureIcons())
116 | }
117 |
118 | componentDidUpdate() {
119 | // `this.layoutWillChange` will be set to true right before state.activeTab
120 | // is updated. Then, and only then, we had a true layout change, and thus
121 | // we want to measure the icons.
122 | if (this.layoutWillChange) {
123 | setTimeout(() => this._measureIcons())
124 | this.layoutWillChange = false
125 | }
126 | }
127 |
128 | componentWillReceiveProps(nextProps: BottomNavigationProps) {
129 | const { activeTab: newTabIndex } = nextProps
130 | const { activeTab: oldTabIndex } = this.state
131 | const { nextActiveTab } = this
132 | const tabAmount = this.props.children.length
133 |
134 | // Change active tab when activeTab-prop changes
135 | if (newTabIndex !== oldTabIndex && newTabIndex !== nextActiveTab) {
136 | // Test index out of bounce
137 | if (newTabIndex < 0 && newTabIndex >= tabAmount) {
138 | if (__DEV__) console.error(`${newTabIndex} is not a valid tabIndex`)
139 | } else {
140 | this.refs[`tab_${newTabIndex}`].setTabActive({ forceAnimation: true })
141 | }
142 | }
143 | }
144 | render() {
145 | const {
146 | backgroundColor,
147 | pressRippleColor,
148 | rippleX,
149 | rippleY,
150 | activeTab
151 | } = this.state
152 |
153 | var shifting = this.props.shifting != null
154 | ? this.props.shifting
155 | : this.props.children.length > 3
156 | return (
157 |
162 |
167 |
173 |
179 | {React.Children.map(this.props.children, (child, tabIndex) => (
180 | React.cloneElement(child, {
181 | shifting,
182 | active: tabIndex === activeTab,
183 | tabIndex: tabIndex,
184 | onTabPress: this._handleTabChange,
185 | ref: `tab_${tabIndex}`,
186 |
187 | // Pass setted props, or inherited props by parent component
188 | labelColor: child.props.labelColor || this.props.labelColor,
189 | activeLabelColor: child.props.activeLabelColor ||
190 | this.props.activeLabelColor,
191 | barBackgroundColor: child.props.barBackgroundColor ||
192 | this.props.backgroundColor,
193 | tabStyle: child.props.tabStyle ||
194 | this.props.tabStyle
195 | })
196 | ))}
197 |
198 |
199 | )
200 | }
201 |
202 | _canChangeTabs() {
203 | // Ignore tab taps that are less than 500ms apart. Blocks repetitive or
204 | // super fast tab switches. Fixes Issue #32.
205 | const { delay } = this.props;
206 | const TAB_BLOCK_DELAY_MS = delay || 100
207 |
208 | if (this.lastTabChangeDate < 0) {
209 | return true
210 | }
211 |
212 | const tabChangeDiff = new Date() - this.lastTabChangeDate
213 | return tabChangeDiff > TAB_BLOCK_DELAY_MS
214 | }
215 |
216 | _handleTabChange = (args, opts) => {
217 | const { tabIndex, barBackgroundColor } = args
218 | const { updateActiveTab, forceAnimation = false } = opts
219 |
220 | if (!this._canChangeTabs() && !forceAnimation) {
221 | return
222 | } else {
223 | this.lastTabChangeDate = new Date()
224 | }
225 |
226 | const { x, y } = this.iconPositions[tabIndex]
227 |
228 | // Directly save the active tab index, but not in the state.
229 | // This way we can block any componentUpdate when the activeTab prop is
230 | // set to a value which the BottomNavigation already has.
231 | // (see componentWillReceiveProps)
232 | if (updateActiveTab) this.nextActiveTab = tabIndex
233 |
234 | // Delegation to next tick will cause smoother animations
235 | setTimeout(() => {
236 | // Call onTabChange Event Callback
237 | if (updateActiveTab) {
238 | this.props.onTabChange(tabIndex, this.state.activeTab)
239 | }
240 |
241 | // Checks in general if component is still mounted.
242 | // Abort further execution if unmounted.
243 | if (this.refs.pressRipple == null) return
244 |
245 | // Prepare Ripple Background Animation
246 | this.setState({
247 | pressRippleColor: barBackgroundColor,
248 | rippleX: x + 12, // + 12 because icon has size 24
249 | rippleY: 28 // 56/2, vertical middle of component
250 | })
251 |
252 | // Show the PressRipple Animation
253 | this.refs.pressRipple.run()
254 |
255 | // If color changes, run RippleBackground Animation
256 | if (this.state.backgroundColor !== barBackgroundColor) {
257 | this.refs.backgroundRipple.run(() => {
258 | // After that, set the new bar background color
259 | this.setState({ backgroundColor: barBackgroundColor })
260 | })
261 | }
262 |
263 | // Delegation to next tick, so that LayoutAnimation doesn't apply to
264 | // Ripple animations
265 | setTimeout(() => {
266 | // Make magic LayoutAnimation for next Layout Change
267 | LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
268 |
269 | // Announce that the layout will change. This will cause, that
270 | // `this._measure()` will be executed in `componentDidUpdate`
271 | this.layoutWillChange = true
272 |
273 | // Finally, update tab and set it active
274 | if (updateActiveTab) this.setState({ activeTab: tabIndex })
275 | })
276 | })
277 | }
278 |
279 | _handleOnLayout = ({ nativeEvent }) => {
280 | const { width, height } = nativeEvent.layout
281 |
282 | // Set layout initial
283 | if (this.dimensions.width === -1 && this.dimensions.height === -1) {
284 | this.dimensions = { width, height }
285 | }
286 |
287 | // Measure Icons, if dimensions differ
288 | if (this.dimensions.width !== width || this.dimensions.height !== height) {
289 | setTimeout(() => this._measureIcons())
290 | this.dimensions = { width, height }
291 | }
292 | }
293 |
294 | _measureIcons = () => {
295 | const navHandle = findNodeHandle(this.refs.navigation)
296 |
297 | this.props.children.forEach((child, tabIndex) => {
298 | // If Component was unmounted meanwhile, stop measuring
299 | if (this.refs[`tab_${tabIndex}`] == null) return
300 |
301 | this.refs[`tab_${tabIndex}`]
302 | .getIconRef()
303 | .measureLayout(navHandle, (x, y) => {
304 | // Save current icon position
305 | this.iconPositions[tabIndex] = { x, y }
306 | })
307 | })
308 | }
309 | }
310 |
311 | const styles = StyleSheet.create({
312 | container: {
313 | flex: 1,
314 | flexDirection: 'row',
315 | justifyContent: 'center',
316 | alignItems: 'center'
317 | }
318 | })
319 |
--------------------------------------------------------------------------------
/lib/NavigationComponent.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Pluggable Version for [react-navigation](https://reactnavigation.org/).
3 | * The BottomNavigation gets its options from a separate namespace in
4 | * `tabBarOptions` inside TabNavigatorConfig, namely `bottomNavigationOptions`.
5 | * @flow
6 | */
7 |
8 | import React, { Component, PureComponent } from 'react'
9 | import BottomNavigation, { Tab } from '../'
10 |
11 |
12 | type NCProps = {
13 | // I could use react-navigation's type definitions, but I don't want to
14 | // include it as a dependency or risk throwing an error because the package
15 | // is not found. So: `any`
16 | navigationState: any,
17 | jumpToIndex: (index: number) => void,
18 | getLabel: (scene: any) => ?(React.Element<*> | string),
19 | getLabelText?: (scene: any) => ?(React.Element<*> | string),
20 | renderIcon: (scene: any) => React.Element<*>,
21 | style?: any,
22 | bottomNavigationOptions: any,
23 | activeTintColor?: string,
24 | inactiveTintColor?: string
25 | }
26 |
27 | export default
28 | class NavigationComponent extends PureComponent {
29 |
30 |
31 | render() {
32 | // react-navigation passed props
33 | const {
34 | activeTintColor,
35 | inactiveTintColor,
36 | navigationState,
37 | bottomNavigationOptions,
38 | getLabel: navigationGetLabel,
39 | getLabelText: navigationGetLabelOld,
40 | getOnPress: navigationGetOnPress,
41 | renderIcon,
42 | jumpToIndex,
43 | style
44 | } = this.props
45 |
46 | const bnOptions = bottomNavigationOptions || {}
47 |
48 | // Support for earlier version of react-navigation (up to 1.0.0-beta5)
49 | const getLabel = navigationGetLabel || navigationGetLabelOld
50 |
51 | // BottomNavigation's style
52 | const { style: bnStyle } = bnOptions
53 |
54 | // Builded props for BottomNavigation
55 | const bnProps = {
56 | labelColor: bnOptions.labelColor,
57 | innerStyle: bnOptions.innerStyle,
58 | activeLabelColor: bnOptions.activeLabelColor,
59 | rippleColor: bnOptions.rippleColor,
60 | backgroundColor: bnOptions.backgroundColor,
61 | shifting: bnOptions.shifting,
62 | tabStyle: bnOptions.tabStyle,
63 | delay: bnOptions.delay
64 | }
65 | const previousScene = navigationState.routes[navigationState.index]
66 |
67 | return (
68 | jumpToIndex(index)}
76 | {...bnProps}
77 | >
78 | {navigationState.routes.map((route, index) => {
79 | const focused = index === navigationState.index
80 |
81 | // scene object for `getLabel` and `renderIcon`
82 | const scene = {
83 | route,
84 | index,
85 | focused,
86 | tintColor: focused ? activeTintColor : inactiveTintColor
87 | }
88 | const onPress = navigationGetOnPress(previousScene, scene)
89 | const label = getLabel(scene)
90 | const icon = renderIcon(scene)
91 |
92 | // Prepare props for the tabs
93 | const tabs = bnOptions.tabs || {}
94 | const tabOptions = tabs[route.key] || {}
95 | const tabProps = {
96 | icon: tabOptions.icon || icon,
97 | activeIcon: tabOptions.activeIcon,
98 | label: tabOptions.label || label,
99 | labelColor: tabOptions.labelColor,
100 | activeLabelColor: tabOptions.activeLabelColor,
101 | barBackgroundColor: tabOptions.barBackgroundColor,
102 | tabStyle: bnProps.tabStyle,
103 | }
104 | return { onPress(scene, jumpToIndex) } : null}
107 | {...tabProps}
108 | />
109 | })}
110 |
111 | )
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/lib/PressRipple.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A normal Ripple, like TouchFeedbackNative.
3 | * We can't use TouchFeedbackNative, since it only works for Android.
4 | * We can't use the Ripple inside the Tab, because `overflow: visible` doesn't
5 | * work in Android. So this Component has a x and y value, to be absolutely
6 | * positioned in a container.
7 | * @flow
8 | */
9 |
10 | import React, { Component } from 'react'
11 | import {
12 | View,
13 | Animated,
14 | Platform
15 | } from 'react-native'
16 | import { easeOut } from './utils/easing'
17 |
18 |
19 | type PressRippleProps = {
20 | color: string,
21 | x: number,
22 | y: number
23 | }
24 |
25 | type PressRippleState = {
26 | opacity: Animated.Value,
27 | scale: Animated.Value,
28 | animating: boolean
29 | }
30 |
31 | const defaultProps = {
32 | color: 'black',
33 | x: 0,
34 | y: 0
35 | }
36 |
37 | export default class PressRipple extends Component {
38 |
39 | static defaultProps: typeof defaultProps
40 | props: PressRippleProps
41 | state: PressRippleState
42 | maxRippleOpacity: number
43 | size: number
44 |
45 | static defaultProps = defaultProps
46 |
47 | constructor(props: PressRippleProps) {
48 | super(props)
49 |
50 | this.maxRippleOpacity = 0.12
51 | this.size = 100
52 |
53 | this.state = {
54 | opacity: new Animated.Value(0),
55 | scale: new Animated.Value(0.01),
56 | animating: false
57 | }
58 | }
59 |
60 | render() {
61 | const { color, x, y } = this.props
62 | const { scale, opacity, animating } = this.state
63 | const { size } = this
64 |
65 | if (!animating) return null
66 |
67 | return (
68 |
81 | )
82 | }
83 |
84 | run = () => {
85 | const useNativeDriver = Platform.OS === 'android'
86 |
87 | // Render the Component
88 | this.setState({ animating: true })
89 | this.state.opacity.setValue(this.maxRippleOpacity)
90 |
91 | // GET TO THE CHOPPA
92 | Animated.parallel([
93 | Animated.timing(this.state.scale,
94 | { toValue: 1, duration: 200, easing: easeOut, useNativeDriver }),
95 | Animated.timing(this.state.opacity,
96 | { toValue: 0, duration: 300, easing: easeOut, useNativeDriver })
97 | ]).start(() => {
98 | // Initial values
99 | this.state.scale.setValue(0.01)
100 | this.state.opacity.setValue(0)
101 |
102 | // Don't render the Component anymore
103 | this.setState({ animating: true })
104 | })
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/lib/RippleBackgroundTransition.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Transition from one background color to another using a Ripple Effect.
3 | * Doesn't end with background color, only makes transition and resets.
4 | * @flow
5 | */
6 |
7 | import React, { Component } from 'react'
8 | import {
9 | View,
10 | Animated,
11 | Platform
12 | } from 'react-native'
13 | import { easeOut } from './utils/easing'
14 |
15 | type RBTProps = {
16 | color: string,
17 | posX: number,
18 | posY: number
19 | }
20 |
21 | type RBTState = {
22 | animating: boolean,
23 | distance: number,
24 | scale: Animated.Value
25 | }
26 |
27 | export default class RippleBackgroundTransition extends Component {
28 |
29 | props: RBTProps
30 | state: RBTState
31 | layout: { x: number, y: number, width: number, height: number }
32 | scaleInit: number
33 |
34 | constructor(props: RBTProps) {
35 | super(props)
36 |
37 | this.scaleInit = Platform.OS === 'android' ? 0.01 : 0
38 | this.layout = { x: 0, y: 0, width: 0, height: 0 }
39 | this.state = this._initState()
40 | }
41 |
42 | _initState = () => {
43 | return {
44 | animating: false,
45 | scale: new Animated.Value(this.scaleInit),
46 | distance: 0
47 | }
48 | }
49 |
50 | render() {
51 | return (
52 |
56 | {this.state.animating && this._renderRipple()}
57 |
58 | )
59 | }
60 |
61 | _renderRipple = () => {
62 | const { distance, scale } = this.state
63 | const { posX, posY } = this.props
64 |
65 | // Distance is from press position to furthest position on view is the
66 | // radius of the circle.
67 | const size = distance * 2
68 |
69 | return (
70 |
82 | )
83 | }
84 |
85 | // Calculate the longest distance between click position and bounds
86 | _getLargestDistanceToBounds = (x: number, y: number): number => {
87 | // Method: the longest distance is always to a corner.
88 | // So we measure the distances to all 4 corners using Pythagoras
89 | // and return the biggest of them.
90 |
91 | let biggestDistance = 0
92 |
93 | const testVectors = [
94 | [ 0, 0 ],
95 | [ this.layout.width, 0 ],
96 | [ this.layout.width, this.layout.height ],
97 | [ 0, this.layout.height ]
98 | ]
99 | const refVector = [ x, y ]
100 |
101 | testVectors.forEach((vector, i) => {
102 | const dX = vector[0] - refVector[0] // distance on x axis
103 | const dY = vector[1] - refVector[1] // distance on y axis
104 |
105 | // Pythagoras: a^2 + b^2 = c^2
106 | // Note: d is now a squared value
107 | const d = dX*dX + dY*dY
108 |
109 | if (d > biggestDistance) biggestDistance = d
110 | })
111 |
112 | // Since we only have the c^2 part of Pythagoras, we need to sqrt it
113 | return Math.sqrt(biggestDistance)
114 | }
115 |
116 | // Save Layout for later measurement
117 | _handleOnLayout = ({ nativeEvent }: any) => {
118 | this.layout = { ...nativeEvent.layout }
119 | }
120 |
121 | // Public accessible method to run Ripple Animation
122 | run = (callback?: Function = () => {}) => {
123 | const { posX, posY } = this.props
124 | const distance = this._getLargestDistanceToBounds(posX, posY)
125 |
126 | this.setState({
127 | animating: true,
128 | distance
129 | })
130 |
131 | Animated.timing(this.state.scale, {
132 | toValue: 1,
133 | duration: 349,
134 | easing: easeOut,
135 | useNativeDriver: Platform.OS === 'android'
136 | }).start(() => {
137 | // Call callback to tell callee that we are finished
138 | callback(this.props.color)
139 |
140 | // Reset everything
141 | this.setState(this._initState())
142 | })
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/lib/Tab.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Tab in the Bottom Navigation. Consists of Label + Icon.
3 | * Handles all the fancy animations.
4 | * @flow
5 | */
6 |
7 | import React, { Component, PureComponent } from 'react'
8 | import {
9 | View,
10 | Text,
11 | Easing,
12 | Animated,
13 | Platform,
14 | StyleSheet,
15 | TouchableWithoutFeedback
16 | } from 'react-native'
17 | import { easeInOut } from './utils/easing'
18 |
19 |
20 | const useNativeDriver = Platform.OS === 'android'
21 |
22 | type TabProps = {
23 | active: boolean,
24 | shifting: boolean,
25 | tabIndex: number,
26 | barBackgroundColor: string,
27 | icon: ReactElement<*>,
28 | activeIcon?: ReactElement<*>,
29 | label: string,
30 | labelColor: string,
31 | activeLabelColor?: string,
32 | onTabPress: () => void,
33 | onPress: () => void
34 | }
35 |
36 | type TabState = {
37 | fixed: {
38 | labelScale: Animated.Value,
39 | labelY: Animated.Value,
40 | iconY: Animated.Value,
41 | iconOpacity: Animated.Value
42 | },
43 | shifting: {
44 | labelOpacity: Animated.Value,
45 | labelScale: Animated.Value,
46 | iconY: Animated.Value,
47 | iconOpacity: Animated.Value
48 | }
49 | }
50 |
51 | export default class Tab extends Component {
52 |
53 | props: TabProps
54 | state: TabState
55 | didOnceBecameActive: boolean
56 |
57 | constructor(props: TabProps) {
58 | super(props)
59 | const { active } = props
60 |
61 | // HACK: In shifting mode, after the first animation from active to
62 | // inactive, the icon jumps down before animating to the active state
63 | // again. In order to fix this, we need to store, if it already was
64 | // active. Then we can catch that case and manually move it up before
65 | // animating. This only happens in Android, not iOS.
66 | // Is this a bug in react-native or somewhere here?
67 | this.didOnceBecameActive = props.active ? true : false
68 |
69 | this.state = {
70 | fixed: {
71 | iconY: active ? new Animated.Value(-2) : new Animated.Value(0),
72 | labelScale: active ? new Animated.Value(1) : new Animated.Value(0.857),
73 | labelY: active ? new Animated.Value(0) : new Animated.Value(2),
74 | iconOpacity: active ? new Animated.Value(1) : new Animated.Value(0.8)
75 | },
76 | shifting: {
77 | labelOpacity: active ? new Animated.Value(1) : new Animated.Value(0),
78 | labelScale: active ? new Animated.Value(1) : new Animated.Value(0.857),
79 | iconY: active ? new Animated.Value(0) : new Animated.Value(8),
80 | iconOpacity: active ? new Animated.Value(1) : new Animated.Value(0.8)
81 | }
82 | }
83 | }
84 |
85 | // Animations will start as soon as new props are passed through
86 | componentWillReceiveProps(nextProps: TabProps) {
87 | const { props } = this
88 |
89 | const fixedMode = !props.shifting
90 | const shiftingMode = props.shifting
91 | const willBeActive = !props.active && nextProps.active
92 | const willBeInactive = props.active && !nextProps.active
93 |
94 | if (fixedMode && willBeActive) {
95 | this._animateFixedInactiveToActive()
96 | } else if (fixedMode && willBeInactive) {
97 | this._animateFixedActiveToInactive()
98 | } else if (shiftingMode && willBeActive) {
99 | this._animateShiftingInactiveToActive()
100 | } else if (shiftingMode && willBeInactive) {
101 | this._animateShiftingActiveToInactive()
102 | }
103 | }
104 |
105 | render() {
106 | const { icon, label, active, tabStyle } = this.props
107 | return (
108 |
109 |
117 | {this._renderIcon()}
118 | {this._renderLabel()}
119 |
120 |
121 | )
122 | }
123 |
124 | _renderIcon = () => {
125 | const mode = this._getModeString()
126 | const { active, icon, activeIcon } = this.props
127 |
128 | return (
129 |
136 |
137 | {active && activeIcon ? activeIcon : icon}
138 |
139 |
140 | )
141 | }
142 |
143 | _renderLabel = () => {
144 | const { active, labelColor, activeLabelColor, label } = this.props
145 | return (
146 |
160 | {label}
161 |
162 | )
163 | }
164 | shouldComponentUpdate = (nextProps, nextState) => {
165 | return nextProps.active != this.props.active
166 | }
167 |
168 | _animateFixedInactiveToActive = () => {
169 | const duration = 266
170 | const easing = easeInOut
171 |
172 | Animated.parallel([
173 | Animated.timing(this.state.fixed.iconY,
174 | { toValue: -2, duration, easing, useNativeDriver }),
175 | Animated.timing(this.state.fixed.labelScale,
176 | { toValue: 1, duration, easing, useNativeDriver }),
177 | Animated.timing(this.state.fixed.labelY,
178 | { toValue: 0, duration, easing, useNativeDriver }),
179 | Animated.timing(this.state.fixed.iconOpacity,
180 | { toValue: 1, duration, easing, useNativeDriver })
181 | ]).start()
182 | }
183 |
184 | _animateFixedActiveToInactive = () => {
185 | const duration = 266
186 | const easing = easeInOut
187 |
188 | Animated.parallel([
189 | Animated.timing(this.state.fixed.iconY,
190 | { toValue: 0, duration, easing, useNativeDriver }),
191 | Animated.timing(this.state.fixed.labelScale,
192 | { toValue: 0.857, duration, easing, useNativeDriver }),
193 | Animated.timing(this.state.fixed.labelY,
194 | { toValue: 2, duration, easing, useNativeDriver }),
195 | Animated.timing(this.state.fixed.iconOpacity,
196 | { toValue: 0.8, duration, easing, useNativeDriver })
197 | ]).start()
198 | }
199 |
200 | _animateShiftingInactiveToActive = () => {
201 | const easing = easeInOut
202 |
203 | // HACK: See above "didOnceBecameActive"
204 | if (Platform.OS === 'android') {
205 | if (this.didOnceBecameActive) this.state.shifting.iconY.setValue(0)
206 | this.didOnceBecameActive = true
207 | }
208 |
209 | Animated.parallel([
210 | Animated.timing(this.state.shifting.iconY,
211 | { toValue: 0, duration: 266, easing, useNativeDriver }),
212 | Animated.timing(this.state.shifting.iconOpacity,
213 | { toValue: 1, duration: 266, easing, useNativeDriver }),
214 | Animated.timing(this.state.shifting.labelOpacity,
215 | { toValue: 1, duration: 183, delay: 83, easing, useNativeDriver }),
216 | Animated.timing(this.state.shifting.labelScale,
217 | { toValue: 1, duration: 183, delay: 83, easing, useNativeDriver })
218 | ]).start()
219 | }
220 |
221 | _animateShiftingActiveToInactive = () => {
222 | const easing = easeInOut
223 |
224 | Animated.parallel([
225 | Animated.timing(this.state.shifting.iconY,
226 | { toValue: 8, duration: 266, easing, useNativeDriver }),
227 | Animated.timing(this.state.shifting.labelOpacity,
228 | { toValue: 0, duration: 83, easing, useNativeDriver }),
229 | Animated.timing(this.state.shifting.labelScale,
230 | { toValue: 0.857, duration: 83, easing, useNativeDriver }),
231 | Animated.timing(this.state.shifting.iconOpacity,
232 | { toValue: 0.8, duration: 266, easing, useNativeDriver })
233 | ]).start()
234 | }
235 |
236 | _handleTabPress = () => {
237 | this.props.onPress && this.props.onPress(this.props.tabIndex)
238 |
239 | // if onPress is set, only run the (ripple) animation, don't set it active
240 | this.setTabActive({ updateActiveTab: !this.props.onPress })
241 | }
242 |
243 | setTabActive = ({ updateActiveTab = true, forceAnimation = false }) => {
244 | // Setting the tab active is job of the BottomNavigation Component,
245 | // so call it's function to handle that.
246 | this.props.onTabPress({
247 | tabIndex: this.props.tabIndex,
248 | barBackgroundColor: this.props.barBackgroundColor,
249 | iconRef: this.refs._bnic
250 | }, { updateActiveTab, forceAnimation })
251 | }
252 |
253 | _getModeString = () => {
254 | if (this.props.shifting) return 'shifting'
255 | return 'fixed'
256 | }
257 |
258 | _isShifting = () => {
259 | return !!this.props.shifting
260 | }
261 |
262 | _isFixed = () => {
263 | return !this.props.shifting
264 | }
265 |
266 | getIconRef = () => {
267 | return this.refs._bnic
268 | }
269 | }
270 |
271 |
272 | const styles = StyleSheet.create({
273 | container: {
274 | height: 56,
275 | flex: 1,
276 | alignItems: 'center',
277 | paddingTop: 8,
278 | paddingBottom: 10,
279 | paddingLeft: 12,
280 | paddingRight: 12,
281 | backgroundColor: 'transparent'
282 | },
283 | shiftingInactiveContainer: {
284 | maxWidth: 96,
285 | flex: 1
286 | },
287 | shiftingActiveContainer: {
288 | maxWidth: 168,
289 | flex: 1.75
290 | },
291 | icon: {
292 | width: 24,
293 | height: 24,
294 | backgroundColor: 'transparent'
295 | },
296 | label: {
297 | fontSize: 14,
298 | width: 168,
299 | textAlign: 'center',
300 | includeFontPadding: false,
301 | textAlignVertical: 'center',
302 | justifyContent: 'flex-end',
303 | backgroundColor: 'transparent'
304 | }
305 | })
306 |
--------------------------------------------------------------------------------
/lib/utils/easing.js:
--------------------------------------------------------------------------------
1 | import { Easing } from 'react-native'
2 |
3 | export const easeInOut = new Easing.bezier(0.4, 0.0, 0.2, 1)
4 | export const easeOut = new Easing.bezier(0, 0, 0.2, 1)
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-material-bottom-navigation-performance",
3 | "version": "0.7.7",
4 | "description": "JS Implementation of the Material Design Guidelines' Bottom Navigation for react-native",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/tomzaku/react-native-material-bottom-navigation-performance.git"
9 | },
10 | "bugs": {
11 | "url": "https://github.com/tomzaku/react-native-material-bottom-navigation-performance/issues"
12 | },
13 | "author": "Timo Mämecke ",
14 | "license": "MIT",
15 | "keywords": [
16 | "react-native",
17 | "material",
18 | "bottomnavigation",
19 | "bottom navigation",
20 | "ios",
21 | "android",
22 | "react-component",
23 | "react-navigation"
24 | ],
25 | "devDependencies": {
26 | "babel-jest": "18.0.0",
27 | "babel-preset-react-native": "1.9.1",
28 | "jest": "18.1.0",
29 | "react-test-renderer": "~15.4.0"
30 | },
31 | "jest": {
32 | "preset": "react-native"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------