├── .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 | ![with 3 tabs in white](.github/white-3-tab.gif) ![with 3 tabs in color](.github/color-3-tab.gif) 17 | 18 | **Shifting Bottom Navigation** 19 | 20 | ![with 4 tabs in white](.github/white-4-tab.gif) ![with 4 tabs in color](.github/color-4-tab.gif) 21 | 22 | **Behind the Android System Navigation Bar** 23 | 24 | ![behind navigation bar](.github/behind-nav-bar.gif) 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 | --------------------------------------------------------------------------------