├── .npmignore ├── .gitignore ├── DisplayLabels.js ├── android ├── .gitignore ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ │ └── values │ │ │ │ ├── attributes.xml │ │ │ │ └── constants.xml │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── orhan │ │ │ └── bottomnavigation │ │ │ ├── widget │ │ │ └── BNTouchableView.java │ │ │ ├── BottomNavigationPackage.java │ │ │ └── BNTouchableViewManager.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── github │ │ └── orhan │ │ └── bottomnavigation │ │ └── ApplicationTest.java ├── build.gradle ├── gradle.properties ├── gradlew.bat └── gradlew ├── iOS ├── RCTBottomNavigation.xcodeproj │ ├── project.xcworkspace │ │ ├── xcuserdata │ │ │ └── orhan.xcuserdatad │ │ │ │ └── UserInterfaceState.xcuserstate │ │ └── contents.xcworkspacedata │ ├── xcuserdata │ │ └── orhan.xcuserdatad │ │ │ └── xcschemes │ │ │ ├── xcschememanagement.plist │ │ │ └── RCTBottomNavigation.xcscheme │ ├── BNTouchableView.h │ ├── BNTouchableView.m │ ├── BNTouchableViewManager.m │ └── project.pbxproj ├── BNTouchableView.h ├── BNTouchableView.m └── BNTouchableViewManager.m ├── Button.js ├── package.json ├── NativeTouchable.js ├── README.md ├── Ripple.js ├── .eslintrc ├── index.js └── BottomTabBar.js /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | demo_images 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | npm-debug.log 3 | node_modules/ 4 | .idea/ 5 | .reploy 6 | -------------------------------------------------------------------------------- /DisplayLabels.js: -------------------------------------------------------------------------------- 1 | export default DisplayLabels = { 2 | DEFAULT: 0, 3 | ALWAYS: 1, 4 | NEVER: 2, 5 | ACTIVE_TAB_ONLY: 3, 6 | }; -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | # 3 | build 4 | .gradle 5 | 6 | # WebStorm 7 | # 8 | .idea 9 | *.iml 10 | 11 | local.properties 12 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orhan/react-native-bottom-navigation/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /iOS/RCTBottomNavigation.xcodeproj/project.xcworkspace/xcuserdata/orhan.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orhan/react-native-bottom-navigation/HEAD/iOS/RCTBottomNavigation.xcodeproj/project.xcworkspace/xcuserdata/orhan.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Sep 20 12:14:41 CST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-bin.zip 7 | -------------------------------------------------------------------------------- /iOS/RCTBottomNavigation.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/src/androidTest/java/com/github/orhan/bottomnavigation/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.orhan.bottomnavigation; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /iOS/RCTBottomNavigation.xcodeproj/xcuserdata/orhan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | RCTBottomNavigation.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 8A1B8E761B22E4E300DB45C2 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /android/src/main/res/values/attributes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.3.1' 8 | } 9 | } 10 | 11 | apply plugin: 'com.android.library' 12 | 13 | android { 14 | compileSdkVersion 23 15 | buildToolsVersion '23.0.1' 16 | 17 | defaultConfig { 18 | minSdkVersion 16 19 | } 20 | compileOptions { 21 | sourceCompatibility JavaVersion.VERSION_1_7 22 | targetCompatibility JavaVersion.VERSION_1_7 23 | } 24 | } 25 | 26 | repositories { 27 | mavenLocal() 28 | jcenter() 29 | mavenCentral() 30 | } 31 | 32 | dependencies { 33 | provided 'com.facebook.react:react-native:0.+' 34 | } 35 | -------------------------------------------------------------------------------- /Button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Button. 3 | * Tab bar button implementation for the Bottom-Navigation-View. 4 | */ 5 | 'use strict'; 6 | 7 | /* --- Imports --- */ 8 | 9 | import React, {Component} from 'react'; 10 | import {TouchableWithoutFeedback, View} from 'react-native'; 11 | import Ripple from './Ripple'; 12 | 13 | 14 | /* --- Class methods --- */ 15 | 16 | export default class Button extends Component { 17 | render() { 18 | return ( 19 | 20 | 27 | {this.props.children} 28 | 29 | 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useDeprecatedNdk=true 21 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/orhan/bottomnavigation/widget/BNTouchableView.java: -------------------------------------------------------------------------------- 1 | package com.github.orhan.bottomnavigation.widget; 2 | 3 | import com.facebook.react.views.view.ReactViewGroup; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.content.Context; 7 | import android.view.MotionEvent; 8 | 9 | import javax.annotation.Nonnull; 10 | 11 | /** 12 | * Touchable view, for listening to touch events, but not intercept them. 13 | */ 14 | @SuppressLint("ViewConstructor") 15 | public class BNTouchableView extends ReactViewGroup { 16 | @Nonnull 17 | private final OnTouchListener onTouchListener; 18 | 19 | public BNTouchableView(Context context, 20 | @Nonnull OnTouchListener onTouchListener) { 21 | super(context); 22 | this.onTouchListener = onTouchListener; 23 | } 24 | 25 | @Override 26 | public boolean onTouchEvent(MotionEvent ev) { 27 | return this.onTouchListener.onTouch(this, ev); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /iOS/BNTouchableView.h: -------------------------------------------------------------------------------- 1 | /** 2 | * BNTouchableView.h 3 | * Native component used for the Ripple effect. 4 | */ 5 | 6 | #ifndef BNTouchableView_h 7 | #define BNTouchableView_h 8 | 9 | #import 10 | 11 | @class BNTouchableView; 12 | @protocol BNTouchableViewDelegate; 13 | 14 | /* 15 | * The BNTouchable component 16 | */ 17 | @interface BNTouchableView : RCTView 18 | 19 | @property (nonatomic, weak) id delegate; 20 | 21 | @end 22 | 23 | /* 24 | * Touche events delegate 25 | */ 26 | @protocol BNTouchableViewDelegate 27 | 28 | @required 29 | - (void)BNTouchable:(BNTouchableView*)view touchesBegan:(UITouch*)touch; 30 | 31 | @required 32 | - (void)BNTouchable:(BNTouchableView *)view touchesMoved:(UITouch *)touch; 33 | 34 | @required 35 | - (void)BNTouchable:(BNTouchableView *)view touchesEnded:(UITouch *)touch; 36 | 37 | @required 38 | - (void)BNTouchable:(BNTouchableView *)view touchesCancelled:(UITouch *)touch; 39 | 40 | @end 41 | 42 | #endif /* BNTouchableView_h */ 43 | -------------------------------------------------------------------------------- /iOS/RCTBottomNavigation.xcodeproj/BNTouchableView.h: -------------------------------------------------------------------------------- 1 | /** 2 | * BNTouchableView.h 3 | * Native component used for the Ripple effect. 4 | */ 5 | 6 | #ifndef BNTouchableView_h 7 | #define BNTouchableView_h 8 | 9 | #import "RCTView.h" 10 | 11 | @class BNTouchableView; 12 | @protocol BNTouchableViewDelegate; 13 | 14 | /* 15 | * The BNTouchable component 16 | */ 17 | @interface BNTouchableView : RCTView 18 | 19 | @property (nonatomic, weak) id delegate; 20 | 21 | @end 22 | 23 | /* 24 | * Touche events delegate 25 | */ 26 | @protocol BNTouchableViewDelegate 27 | 28 | @required 29 | - (void)BNTouchable:(BNTouchableView*)view touchesBegan:(UITouch*)touch; 30 | 31 | @required 32 | - (void)BNTouchable:(BNTouchableView *)view touchesMoved:(UITouch *)touch; 33 | 34 | @required 35 | - (void)BNTouchable:(BNTouchableView *)view touchesEnded:(UITouch *)touch; 36 | 37 | @required 38 | - (void)BNTouchable:(BNTouchableView *)view touchesCancelled:(UITouch *)touch; 39 | 40 | @end 41 | 42 | #endif /* BNTouchableView_h */ 43 | -------------------------------------------------------------------------------- /android/src/main/res/values/constants.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #3F51B5 5 | 6 | #42a5f5 7 | #f44336 8 | #fdd835 9 | #4caf50 10 | 11 | 12 | 28dp 13 | 28dp 14 | 1333 15 | 3dp 16 | 17 | @color/mdl_palette_blue_400 18 | @color/mdl_palette_red_500 19 | @color/mdl_palette_yellow_600 20 | @color/mdl_palette_green_500 21 | 22 | 23 | 24 | 1dp 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-bottom-navigation", 3 | "version": "0.7.6", 4 | "description": "A top-level component following the 'Bottom navigation' Material Design spec.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/orhan/react-native-bottom-navigation" 12 | }, 13 | "keywords": [ 14 | "react-native-component", 15 | "react-component", 16 | "react-native", 17 | "ios", 18 | "android", 19 | "tabs", 20 | "tab", 21 | "material-design", 22 | "bottom", 23 | "navigation" 24 | ], 25 | "author": "Orhan Sönmez", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/orhan/react-native-bottom-navigation/issues" 29 | }, 30 | "peerDependencies": { 31 | "react-native": ">=0.20.0" 32 | }, 33 | "dependencies": { 34 | "parse-color": "^1.0.0", 35 | "prop-types": "^15.5.10", 36 | "ramda": "^0.21.0" 37 | }, 38 | "homepage": "https://github.com/orhan/react-native-bottom-navigation" 39 | } 40 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/orhan/bottomnavigation/BottomNavigationPackage.java: -------------------------------------------------------------------------------- 1 | package com.github.orhan.bottomnavigation; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.JavaScriptModule; 5 | import com.facebook.react.bridge.NativeModule; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.uimanager.ViewManager; 8 | 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | /** 14 | * Bottom Navigation Package. 15 | */ 16 | public class BottomNavigationPackage implements ReactPackage { 17 | 18 | @Override 19 | public List createNativeModules(ReactApplicationContext reactApplicationContext) { 20 | return Collections.emptyList(); 21 | } 22 | 23 | public List> createJSModules() { 24 | return Collections.emptyList(); 25 | } 26 | 27 | @Override 28 | public List createViewManagers(ReactApplicationContext reactApplicationContext) { 29 | return Arrays.asList( 30 | new BNTouchableViewManager() 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /iOS/BNTouchableView.m: -------------------------------------------------------------------------------- 1 | /** 2 | * BNTouchableView.m 3 | * Native component used for the Ripple effect. 4 | */ 5 | 6 | #import "BNTouchableView.h" 7 | 8 | @implementation BNTouchableView 9 | 10 | #pragma mark - Touch event handling 11 | 12 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 13 | { 14 | UITouch *touch = [touches anyObject]; 15 | if (self.delegate) { 16 | [self.delegate BNTouchable:self touchesBegan:touch]; 17 | } 18 | [super touchesBegan:touches withEvent:event]; 19 | } 20 | 21 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 22 | { 23 | UITouch *touch = [touches anyObject]; 24 | if (self.delegate) { 25 | [self.delegate BNTouchable:self touchesEnded:touch]; 26 | } 27 | [super touchesEnded:touches withEvent:event]; 28 | } 29 | 30 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 31 | { 32 | UITouch *touch = [touches anyObject]; 33 | if (self.delegate) { 34 | [self.delegate BNTouchable:self touchesMoved:touch]; 35 | } 36 | [super touchesMoved:touches withEvent:event]; 37 | } 38 | 39 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 40 | { 41 | UITouch *touch = [touches anyObject]; 42 | if (self.delegate) { 43 | [self.delegate BNTouchable:self touchesCancelled:touch]; 44 | } 45 | [super touchesCancelled:touches withEvent:event]; 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /iOS/RCTBottomNavigation.xcodeproj/BNTouchableView.m: -------------------------------------------------------------------------------- 1 | /** 2 | * BNTouchableView.m 3 | * Native component used for the Ripple effect. 4 | */ 5 | 6 | #import "BNTouchableView.h" 7 | 8 | @implementation BNTouchableView 9 | 10 | #pragma mark - Touch event handling 11 | 12 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 13 | { 14 | UITouch *touch = [touches anyObject]; 15 | if (self.delegate) { 16 | [self.delegate BNTouchable:self touchesBegan:touch]; 17 | } 18 | [super touchesBegan:touches withEvent:event]; 19 | } 20 | 21 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 22 | { 23 | UITouch *touch = [touches anyObject]; 24 | if (self.delegate) { 25 | [self.delegate BNTouchable:self touchesEnded:touch]; 26 | } 27 | [super touchesEnded:touches withEvent:event]; 28 | } 29 | 30 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 31 | { 32 | UITouch *touch = [touches anyObject]; 33 | if (self.delegate) { 34 | [self.delegate BNTouchable:self touchesMoved:touch]; 35 | } 36 | [super touchesMoved:touches withEvent:event]; 37 | } 38 | 39 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 40 | { 41 | UITouch *touch = [touches anyObject]; 42 | if (self.delegate) { 43 | [self.delegate BNTouchable:self touchesCancelled:touch]; 44 | } 45 | [super touchesCancelled:touches withEvent:event]; 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /iOS/BNTouchableViewManager.m: -------------------------------------------------------------------------------- 1 | /** 2 | * BNTouchableViewManager.m 3 | * Native component used for the Ripple effect. 4 | */ 5 | 6 | #import 7 | #import 8 | #import 9 | #import "BNTouchableView.h" 10 | 11 | @interface BNTouchableViewManager : RCTViewManager 12 | @end 13 | 14 | @implementation BNTouchableViewManager 15 | 16 | RCT_EXPORT_MODULE() 17 | 18 | - (UIView*)view 19 | { 20 | BNTouchableView *view = [[BNTouchableView alloc] init]; 21 | view.delegate = self; 22 | return view; 23 | } 24 | 25 | #pragma mark - BNTouchableViewDelegate 26 | 27 | - (void)BNTouchable:(BNTouchableView *)view touchesBegan:(UITouch *)touch 28 | { 29 | [self sendTouchEvent:@"TOUCH_DOWN" touch:touch source:view]; 30 | } 31 | 32 | - (void)BNTouchable:(BNTouchableView *)view touchesMoved:(UITouch *)touch 33 | { 34 | [self sendTouchEvent:@"TOUCH_MOVE" touch:touch source:view]; 35 | } 36 | 37 | - (void)BNTouchable:(BNTouchableView *)view touchesEnded:(UITouch *)touch 38 | { 39 | [self sendTouchEvent:@"TOUCH_UP" touch:touch source:view]; 40 | } 41 | 42 | - (void)BNTouchable:(BNTouchableView *)view touchesCancelled:(UITouch *)touch 43 | { 44 | [self sendTouchEvent:@"TOUCH_CANCEL" touch:touch source:view]; 45 | } 46 | 47 | - (void)sendTouchEvent:(NSString*)type touch:(UITouch*)touch source:(BNTouchableView*)source 48 | { 49 | CGPoint location = [touch locationInView:source]; 50 | NSDictionary *dict = @{ 51 | @"target": source.reactTag, 52 | @"type": type, 53 | @"x": [NSNumber numberWithFloat:location.x], 54 | @"y": [NSNumber numberWithFloat:location.y], 55 | }; 56 | [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:dict]; 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /iOS/RCTBottomNavigation.xcodeproj/BNTouchableViewManager.m: -------------------------------------------------------------------------------- 1 | /** 2 | * BNTouchableViewManager.m 3 | * Native component used for the Ripple effect. 4 | */ 5 | 6 | #import "RCTViewManager.h" 7 | #import "RCTEventDispatcher.h" 8 | #import "UIView+React.h" 9 | #import "BNTouchableView.h" 10 | 11 | @interface BNTouchableViewManager : RCTViewManager 12 | @end 13 | 14 | @implementation BNTouchableViewManager 15 | 16 | RCT_EXPORT_MODULE() 17 | 18 | - (UIView*)view 19 | { 20 | BNTouchableView *view = [[BNTouchableView alloc] init]; 21 | view.delegate = self; 22 | return view; 23 | } 24 | 25 | #pragma mark - BNTouchableViewDelegate 26 | 27 | - (void)BNTouchable:(BNTouchableView *)view touchesBegan:(UITouch *)touch 28 | { 29 | [self sendTouchEvent:@"TOUCH_DOWN" touch:touch source:view]; 30 | } 31 | 32 | - (void)BNTouchable:(BNTouchableView *)view touchesMoved:(UITouch *)touch 33 | { 34 | [self sendTouchEvent:@"TOUCH_MOVE" touch:touch source:view]; 35 | } 36 | 37 | - (void)BNTouchable:(BNTouchableView *)view touchesEnded:(UITouch *)touch 38 | { 39 | [self sendTouchEvent:@"TOUCH_UP" touch:touch source:view]; 40 | } 41 | 42 | - (void)BNTouchable:(BNTouchableView *)view touchesCancelled:(UITouch *)touch 43 | { 44 | [self sendTouchEvent:@"TOUCH_CANCEL" touch:touch source:view]; 45 | } 46 | 47 | - (void)sendTouchEvent:(NSString*)type touch:(UITouch*)touch source:(BNTouchableView*)source 48 | { 49 | CGPoint location = [touch locationInView:source]; 50 | NSDictionary *dict = @{ 51 | @"target": source.reactTag, 52 | @"type": type, 53 | @"x": [NSNumber numberWithFloat:location.x], 54 | @"y": [NSNumber numberWithFloat:location.y], 55 | }; 56 | [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:dict]; 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /NativeTouchable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Native Touchable. 3 | * Native component used for the Ripple effect. 4 | */ 5 | 6 | /* --- Imports --- */ 7 | 8 | import React, { 9 | Component, 10 | } from 'react'; 11 | 12 | import { 13 | requireNativeComponent, 14 | Platform, 15 | PixelRatio, 16 | View, 17 | } from 'react-native'; 18 | 19 | import PropTypes from 'prop-types'; 20 | 21 | 22 | /* --- Component setup --- */ 23 | 24 | const NativeTouchableView = requireNativeComponent('BNTouchableView', { 25 | name: 'NativeTouchable', 26 | propTypes: { 27 | ...View.propTypes, 28 | 29 | // Touch events callback 30 | onTouch: PropTypes.func, 31 | }, 32 | }, { 33 | nativeOnly: { 34 | nativeBackgroundAndroid: true, 35 | nativeForegroundAndroid: true, 36 | }, 37 | }); 38 | 39 | 40 | /* --- Class methods --- */ 41 | 42 | export default class NativeTouchable extends Component { 43 | 44 | static propTypes = { 45 | ...View.propTypes, 46 | 47 | // Touch events callback 48 | onTouch: PropTypes.func, 49 | }; 50 | 51 | /* --- Private methods --- */ 52 | 53 | _onTouchEvent(event) { 54 | if (this.props.onTouch) { 55 | const evt = event.nativeEvent; 56 | evt.x = Platform.OS === 'android' ? evt.x / PixelRatio.get() : evt.x; 57 | evt.x = Platform.OS === 'android' ? evt.x / PixelRatio.get() : evt.x; 58 | this.props.onTouch(evt); 59 | } 60 | } 61 | 62 | 63 | /* --- Public methods --- */ 64 | 65 | measure(cb) { 66 | return this.refs.node.measure(cb); 67 | } 68 | 69 | 70 | /* --- Rendering methods --- */ 71 | 72 | render() { 73 | return ( 74 | 81 | {this.props.children} 82 | 83 | ); 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## react-native-bottom-navigation 3 | 4 | This is a top-level component following the ['Bottom navigation' Material Design](https://material.google.com/components/bottom-navigation.html#) specifications. 5 | 6 | 7 | ## Installation 8 | 9 | `npm install --save react-native-bottom-navigation` 10 | 11 | 12 | ### Using RNPM (React-Native Package Manager): 13 | 14 | `rnpm link react-native-bottom-navigation` 15 | 16 | 17 | ### Manually (iOS): 18 | 19 | 1. Add node_modules/react-native-bottom-navigation/iOS/RCTBottomNavigation.xcodeproj to your xcode project, usually under the **Libraries** group 20 | 21 | 2. Add libRCTBottomNavigation.a (from Products under RCTBottomNavigation.xcodeproj) to build target's **Linked Frameworks and Libraries** list 22 | 23 | 24 | ### Manually (Android): 25 | 26 | 1. Add the following snippet to your `android/settings.gradle`: 27 | ``` 28 | include ':RNBottomNavigation' 29 | project(':RNBottomNavigation').projectDir = file('../node_modules/react-native-bottom-navigation/android') 30 | ``` 31 | 32 | 2. Declare the dependency in your `android/app/build.gradle` 33 | ``` 34 | dependencies { 35 | ... 36 | compile project(':RNBottomNavigation') 37 | } 38 | ``` 39 | 40 | 3. In your `MainActivity.java`, make the following changes: 41 | ``` 42 | import com.github.orhan.bottomnavigation.ReactBottomNavigationPackage; <-- Import this! 43 | 44 | ... 45 | 46 | @Override protected 47 | List getPackages() { 48 | return Arrays.asList( 49 | new MainReactPackage(), 50 | new ReactBottomNavigationPackage() <-- Add this! 51 | ); 52 | } 53 | ``` 54 | 55 | ## Usage 56 | 57 | `const BottomNavigation = require('react-native-bottom-navigation');` 58 | 59 | ```html 60 | 65 | 70 | 71 | 76 | 77 | 82 | 83 | 88 | 89 | ``` 90 | 91 | ## Example Project 92 | 93 | You can check out the [Example Project](https://github.com/orhan/react-native-bottom-navigation-example) to get a better understanding of this library. 94 | 95 | ## Props Reference 96 | 97 | `TODO` 98 | 99 | ## Acknowledgements 100 | 101 | This library is based on the fantastic work of the [React-Native Material-Kit](https://github.com/xinthink/react-native-material-kit) by [xinthink](https://github.com/xinthink). So if you are interested in having the ripple effect in other areas of your app, you can check that library out. 102 | 103 | --- 104 | 105 | **MIT Licensed** 106 | -------------------------------------------------------------------------------- /iOS/RCTBottomNavigation.xcodeproj/xcuserdata/orhan.xcuserdatad/xcschemes/RCTBottomNavigation.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/orhan/bottomnavigation/BNTouchableViewManager.java: -------------------------------------------------------------------------------- 1 | package com.github.orhan.bottomnavigation; 2 | 3 | import com.facebook.react.bridge.Arguments; 4 | import com.facebook.react.bridge.WritableMap; 5 | import com.facebook.react.uimanager.ThemedReactContext; 6 | import com.facebook.react.uimanager.events.RCTEventEmitter; 7 | import com.facebook.react.views.view.ReactViewManager; 8 | import com.github.orhan.bottomnavigation.widget.BNTouchableView; 9 | 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | 13 | /** 14 | * BNTouchable View Manager, forwarding touch events to JS module 15 | */ 16 | public class BNTouchableViewManager extends ReactViewManager { 17 | 18 | @Override 19 | public String getName() { 20 | return "BNTouchableView"; 21 | } 22 | 23 | @Override 24 | public BNTouchableView createViewInstance(ThemedReactContext context) { 25 | final RCTEventEmitter emitter = context.getJSModule(RCTEventEmitter.class); 26 | return new BNTouchableView(context, new View.OnTouchListener() { 27 | private int prevAction; 28 | private long prevEventTime; 29 | 30 | @Override 31 | public boolean onTouch(View view, MotionEvent event) { 32 | String type = isInBounds(view, event) ? getEventType(event) : "TOUCH_CANCEL"; 33 | 34 | if (type != null && isValid(event)) { 35 | WritableMap body = Arguments.createMap(); 36 | body.putString("type", type); 37 | body.putDouble("x", event.getX()); 38 | body.putDouble("y", event.getY()); 39 | 40 | emitter.receiveEvent(view.getId(), "topChange", body); 41 | } 42 | return true; 43 | } 44 | 45 | private String getEventType(MotionEvent evt) { 46 | String type = null; 47 | switch (evt.getAction()) { 48 | case MotionEvent.ACTION_DOWN: 49 | type = "TOUCH_DOWN"; 50 | break; 51 | case MotionEvent.ACTION_UP: 52 | type = "TOUCH_UP"; 53 | break; 54 | case MotionEvent.ACTION_CANCEL: 55 | type = "TOUCH_CANCEL"; 56 | break; 57 | case MotionEvent.ACTION_MOVE: 58 | type = "TOUCH_MOVE"; 59 | break; 60 | } 61 | return type; 62 | } 63 | 64 | private boolean isInBounds(View v, MotionEvent evt) { 65 | float x = evt.getX(), y = evt.getY(); 66 | return x >= 0 && x <= v.getWidth() && y >= 0 && y <= v.getHeight(); 67 | } 68 | 69 | private boolean isValid(MotionEvent evt) { 70 | final long now = System.currentTimeMillis(); 71 | boolean valid; 72 | if (prevEventTime == 0) { 73 | valid = true; 74 | } else { 75 | long elapsed = now - prevEventTime; 76 | valid = evt.getAction() != prevAction || elapsed > 80; 77 | } 78 | 79 | if (valid) { 80 | prevAction = evt.getAction(); 81 | prevEventTime = now; 82 | } 83 | 84 | return valid; 85 | } 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /Ripple.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ripple. 3 | * Ripple effect for the button used in the Bottom-Navigation-View. 4 | */ 5 | 'use strict'; 6 | 7 | /* --- Imports --- */ 8 | 9 | import React, {Component} from 'react'; 10 | import { 11 | Animated, 12 | Easing, 13 | Platform, 14 | View, 15 | } from 'react-native'; 16 | 17 | import PropTypes from 'prop-types'; 18 | import NativeTouchable from './NativeTouchable'; 19 | 20 | 21 | /* --- Class methods --- */ 22 | 23 | class Ripple extends Component { 24 | 25 | /* --- Lifecycle methods --- */ 26 | 27 | constructor(props) { 28 | super(props); 29 | this._animatedAlpha = new Animated.Value(0); 30 | this._animatedRippleScale = new Animated.Value(0); 31 | 32 | this.state = { 33 | width: 1, 34 | height: 1, 35 | maskBorderRadius: 0, 36 | shadowOffsetY: 1, 37 | ripple: { radii: 0, dia: 0, offset: { top: 0, left: 0 } }, 38 | }; 39 | } 40 | 41 | 42 | /* --- Private methods --- */ 43 | 44 | _onLayout = (evt) => { 45 | this._onLayoutChange(evt.nativeEvent.layout); 46 | 47 | if (this.props.onLayout) { 48 | this.props.onLayout(evt); 49 | } 50 | }; 51 | 52 | _onTouchEvent = (evt) => { 53 | switch (evt.type) { 54 | case 'TOUCH_DOWN': 55 | this._onPointerDown(evt); 56 | break; 57 | case 'TOUCH_UP': 58 | case 'TOUCH_CANCEL': 59 | this._onPointerUp(); 60 | break; 61 | default: 62 | break; 63 | } 64 | 65 | if (this.props.onTouch) { 66 | this.props.onTouch(evt); 67 | } 68 | }; 69 | 70 | _onLayoutChange({ width, height }) { 71 | if (width === this.state.width && height === this.state.height) { 72 | return; 73 | } 74 | 75 | this.setState({ 76 | width, 77 | height, 78 | ...this._calcMaskLayer(width, height), 79 | }); 80 | } 81 | 82 | _calcMaskLayer(width, height) { 83 | const maskRadiiPercent = this.props.maskBorderRadiusInPercent; 84 | let maskBorderRadius = this.props.maskBorderRadius; 85 | 86 | if (maskRadiiPercent) { 87 | maskBorderRadius = Math.min(width, height) * maskRadiiPercent / 100; 88 | } 89 | 90 | return { maskBorderRadius }; 91 | } 92 | 93 | _calcRippleLayer(x0, y0) { 94 | const { width, height, maskBorderRadius } = this.state; 95 | const { maskBorderRadiusInPercent } = this.props; 96 | let radii; 97 | let hotSpotX = x0; 98 | let hotSpotY = y0; 99 | 100 | if (this.props.rippleLocation === 'center') { 101 | hotSpotX = width / 2; 102 | hotSpotY = height / 2; 103 | } 104 | const offsetX = Math.max(hotSpotX, (width - hotSpotX)); 105 | const offsetY = Math.max(hotSpotY, (height - hotSpotY)); 106 | 107 | // FIXME Workaround for Android not respect `overflow` 108 | // @see https://github.com/facebook/react-native/issues/3198 109 | if (Platform.OS === 'android' 110 | && this.props.rippleLocation === 'center' 111 | && this.props.maskEnabled && maskBorderRadiusInPercent > 0) { 112 | // limit ripple to the bounds of mask 113 | radii = maskBorderRadius; 114 | } else { 115 | radii = Math.sqrt(offsetX * offsetX + offsetY * offsetY); 116 | } 117 | 118 | return { 119 | ripple: { 120 | radii, 121 | dia: radii * 2, 122 | offset: { 123 | top: hotSpotY - radii, 124 | left: hotSpotX - radii, 125 | }, 126 | }, 127 | }; 128 | } 129 | 130 | _onPointerDown(evt) { 131 | this.setState({ 132 | ...this._calcRippleLayer(evt.x, evt.y), 133 | }); 134 | this.showRipple(); 135 | } 136 | 137 | _onPointerUp() { 138 | this.hideRipple(); 139 | } 140 | 141 | /* --- Public methods --- */ 142 | 143 | measure(cb) { 144 | return this.refs.container.measure(cb); 145 | } 146 | 147 | setCoordinates(x, y) { 148 | this.setState({ 149 | ...this._calcRippleLayer(x, y), 150 | }); 151 | } 152 | 153 | setColors(maskColor, rippleColor) { 154 | this.setState({maskColor, rippleColor}); 155 | } 156 | 157 | showRipple() { 158 | this._animatedAlpha.setValue(1); 159 | this._animatedRippleScale.setValue(0.3); 160 | 161 | // scaling up the ripple layer 162 | this._rippleAni = Animated.timing(this._animatedRippleScale, { 163 | toValue: 1, 164 | duration: this.props.rippleDuration || 200, 165 | easing: Easing.ease, 166 | }); 167 | 168 | // enlarge the shadow, if enabled 169 | if (this.props.shadowAniEnabled) { 170 | this.setState({ shadowOffsetY: 1.5 }); 171 | } 172 | 173 | this._rippleAni.start(() => { 174 | this._rippleAni = undefined; 175 | 176 | // if any pending animation, do it 177 | if (this._pendingRippleAni) { 178 | this._pendingRippleAni(); 179 | } 180 | }); 181 | } 182 | 183 | hideRipple() { 184 | this._pendingRippleAni = () => { 185 | // hide the ripple layer 186 | Animated.timing(this._animatedAlpha, { 187 | toValue: 0, 188 | duration: this.props.maskDuration || 200, 189 | }).start(); 190 | 191 | // scale down the shadow 192 | if (this.props.shadowAniEnabled) { 193 | this.setState({ shadowOffsetY: 1 }); 194 | } 195 | 196 | this._pendingRippleAni = undefined; 197 | }; 198 | 199 | if (!this._rippleAni) { 200 | // previous ripple animation is done, good to go 201 | this._pendingRippleAni(); 202 | } 203 | } 204 | 205 | 206 | /* --- Rendering methods --- */ 207 | 208 | render() { 209 | const shadowStyle = {}; 210 | if (this.props.shadowAniEnabled) { 211 | shadowStyle.shadowOffset = { 212 | width: 0, 213 | height: this.state.shadowOffsetY, 214 | }; 215 | } 216 | 217 | return ( 218 | 224 | 238 | 252 | 253 | 254 | {this.props.children} 255 | 256 | ); 257 | } 258 | } 259 | 260 | 261 | /* --- PropTypes --- */ 262 | 263 | Ripple.propTypes = { 264 | ...View.propTypes, 265 | 266 | // Color of the `Ripple` layer 267 | rippleColor: PropTypes.string, 268 | 269 | // Duration of the ripple effect, in milliseconds 270 | rippleDuration: PropTypes.number, 271 | 272 | // Hot-spot position of the ripple effect 273 | rippleLocation: PropTypes.oneOf([ 274 | 'tapLocation', 275 | 'center', 276 | // 'left', 277 | // 'right', 278 | ]), 279 | 280 | // Whether a `Mask` layer should be used, to clip the ripple to the container’s bounds, 281 | // default is `true` 282 | maskEnabled: PropTypes.bool, 283 | 284 | // Color of the `Mask` layer 285 | maskColor: PropTypes.string, 286 | 287 | // Border radius of the `Mask` layer 288 | maskBorderRadius: PropTypes.number, 289 | 290 | // Border radius of the `Mask` layer, in percentage (of min(width, height)) 291 | maskBorderRadiusInPercent: PropTypes.number, 292 | 293 | // Duration of the mask effect (alpha), in milliseconds 294 | maskDuration: PropTypes.number, 295 | 296 | // Animating the shadow (on pressed/released) or not 297 | shadowAniEnabled: PropTypes.bool, 298 | 299 | // Touch events callback 300 | onTouch: PropTypes.func, 301 | 302 | onLayout: PropTypes.func, 303 | }; 304 | 305 | Ripple.defaultProps = { 306 | rippleColor: 'rgba(255,255,255,0.2)', 307 | rippleDuration: 200, 308 | rippleLocation: 'tapLocation', 309 | maskEnabled: true, 310 | maskColor: 'rgba(255,255,255,0.15)', 311 | maskBorderRadius: 2, 312 | maskDuration: 200, 313 | shadowAniEnabled: true, 314 | }; 315 | 316 | 317 | /* --- Module exports --- */ 318 | 319 | module.exports = Ripple; 320 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "jasmine": true 7 | }, 8 | "ecmaFeatures": { 9 | "arrowFunctions": true, 10 | "blockBindings": true, 11 | "classes": true, 12 | "defaultParams": true, 13 | "destructuring": true, 14 | "forOf": true, 15 | "generators": false, 16 | "modules": true, 17 | "objectLiteralComputedProperties": true, 18 | "objectLiteralDuplicateProperties": false, 19 | "objectLiteralShorthandMethods": true, 20 | "objectLiteralShorthandProperties": true, 21 | "spread": true, 22 | "superInFunctions": true, 23 | "templateStrings": true, 24 | "jsx": true 25 | }, 26 | "rules": { 27 | /** 28 | * Strict mode 29 | */ 30 | // babel inserts "use strict"; for us 31 | // http://eslint.org/docs/rules/strict 32 | "strict": [2, "never"], 33 | 34 | /** 35 | * ES6 36 | */ 37 | "no-var": 2, // http://eslint.org/docs/rules/no-var 38 | 39 | /** 40 | * Variables 41 | */ 42 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 43 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 44 | "no-unused-vars": [0, { // http://eslint.org/docs/rules/no-unused-vars 45 | "vars": "local", 46 | "args": "after-used" 47 | }], 48 | 49 | /** 50 | * Possible errors 51 | */ 52 | "comma-dangle": [2, "always"], // http://eslint.org/docs/rules/comma-dangle 53 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 54 | "no-console": 1, // http://eslint.org/docs/rules/no-console 55 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 56 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 57 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 58 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 59 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 60 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 61 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 62 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 63 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 64 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 65 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 66 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 67 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 68 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 69 | "no-reserved-keys": 0, // http://eslint.org/docs/rules/no-reserved-keys 70 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 71 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 72 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 73 | "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var 74 | 75 | /** 76 | * Best practices 77 | */ 78 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 79 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 80 | "default-case": 2, // http://eslint.org/docs/rules/default-case 81 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 82 | "allowKeywords": true 83 | }], 84 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 85 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 86 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 87 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 88 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 89 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 90 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 91 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 92 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 93 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 94 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 95 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 96 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 97 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 98 | "no-new": 2, // http://eslint.org/docs/rules/no-new 99 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 100 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 101 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 102 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 103 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 104 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 105 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 106 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 107 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 108 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 109 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 110 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 111 | "no-with": 2, // http://eslint.org/docs/rules/no-with 112 | "radix": 2, // http://eslint.org/docs/rules/radix 113 | "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top 114 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 115 | "yoda": 2, // http://eslint.org/docs/rules/yoda 116 | 117 | /** 118 | * Style 119 | */ 120 | "indent": [2, 2], // http://eslint.org/docs/rules/ 121 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 122 | "1tbs", { 123 | "allowSingleLine": true 124 | }], 125 | "quotes": [ 126 | 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes 127 | ], 128 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 129 | "properties": "never" 130 | }], 131 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 132 | "before": false, 133 | "after": true 134 | }], 135 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 136 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 137 | "func-names": 1, // http://eslint.org/docs/rules/func-names 138 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 139 | "beforeColon": false, 140 | "afterColon": true 141 | }], 142 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 143 | "newIsCap": true 144 | }], 145 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 146 | "max": 2 147 | }], 148 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 149 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 150 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 151 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 152 | "no-extra-parens": 0, // http://eslint.org/docs/rules/no-extra-parens 153 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 154 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var 155 | "padded-blocks": 0, // http://eslint.org/docs/rules/padded-blocks 156 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 157 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 158 | "before": false, 159 | "after": true 160 | }], 161 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords 162 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 163 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 164 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 165 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Native Bottom Navigation. 3 | * A top-level component following the 'Bottom navigation' Material Design spec. 4 | */ 5 | 6 | 7 | /* --- Imports --- */ 8 | 9 | import React, {Component, } from 'react'; 10 | import { 11 | Dimensions, 12 | View, 13 | Animated, 14 | Easing, 15 | StyleSheet, 16 | ViewPropTypes, 17 | } from 'react-native'; 18 | 19 | import PropTypes from 'prop-types'; 20 | import BottomTabBar from './BottomTabBar'; 21 | import DisplayLabels from './DisplayLabels'; 22 | 23 | 24 | /* --- Member variables --- */ 25 | 26 | let overlayTabs; 27 | let tabBarProps = {}; 28 | 29 | 30 | /* --- Class methods --- */ 31 | 32 | export default class BottomNavigation extends Component { 33 | 34 | /* --- Component setup -- */ 35 | 36 | static propTypes = { 37 | style: (ViewPropTypes || View.propTypes).style, 38 | initialPage: PropTypes.number, 39 | page: PropTypes.number, 40 | animated: PropTypes.bool, 41 | animatedTabSwitch: PropTypes.bool, 42 | animatedTabSwitchDuration: PropTypes.number, 43 | onChangeTab: PropTypes.func, 44 | onScroll: PropTypes.func, 45 | renderTabBarBackground: PropTypes.any, 46 | translucent: PropTypes.bool, 47 | }; 48 | 49 | static defaultProps = { 50 | initialPage: 0, 51 | page: -1, 52 | animated: false, 53 | animatedTabSwitch: true, 54 | animatedTabSwitchDuration: 100, 55 | translucent: false, 56 | onChangeTab: () => {}, 57 | onScroll: () => {}, 58 | }; 59 | 60 | static DisplayLabels = DisplayLabels; 61 | 62 | 63 | /* --- Lifecycle methods --- */ 64 | 65 | constructor(props) { 66 | super(props); 67 | 68 | this.state = { 69 | currentPage: this.props.initialPage || 0, 70 | scrollValue: new Animated.Value(this.props.initialPage), 71 | containerWidth: Dimensions.get('window').width, 72 | 73 | animationValue: new Animated.Value(1), 74 | bottomBarAnimation: new Animated.Value(0), 75 | bottomBarHidden: false, 76 | }; 77 | } 78 | 79 | componentWillReceiveProps(props) { 80 | if (props.page >= 0 && props.page !== this.state.currentPage) { 81 | this.goToPage(props.page); 82 | } 83 | } 84 | 85 | 86 | /* --- Public methods --- */ 87 | 88 | toggleBottomBar() { 89 | const { bottomBarHidden } = this.state; 90 | 91 | if (bottomBarHidden) { 92 | this.showBottomBar(); 93 | } else { 94 | this.hideBottomBar(); 95 | } 96 | } 97 | 98 | hideBottomBar() { 99 | const { bottomBarAnimation } = this.state; 100 | 101 | Animated.timing(bottomBarAnimation, { 102 | toValue: 1, 103 | duration: 200, 104 | easing: Easing.ease, 105 | }).start(() => { 106 | this.setState({ bottomBarHidden: true }); 107 | }) 108 | } 109 | 110 | showBottomBar() { 111 | const { bottomBarAnimation } = this.state; 112 | 113 | Animated.timing(bottomBarAnimation, { 114 | toValue: 0, 115 | duration: 100, 116 | easing: Easing.ease, 117 | }).start(() => { 118 | this.setState({ bottomBarHidden: false }); 119 | }) 120 | } 121 | 122 | scrollToTop(pageNumber) { 123 | if (this.props.onScrollToTop) { 124 | this.props.onScrollToTop({ i: pageNumber, ref: this._children()[pageNumber], }); 125 | } 126 | } 127 | 128 | goToPage(pageNumber) { 129 | if (this.props.onChangeTab) { 130 | this.props.onChangeTab({ i: pageNumber, ref: this._children()[pageNumber], }); 131 | } 132 | 133 | this.state.animationValue.setValue(0); 134 | this.setState({currentPage: pageNumber}, () => { 135 | Animated.timing(this.state.animationValue, { 136 | fromValue: 0, 137 | toValue: 1, 138 | duration: this.props.animatedTabSwitchDuration, 139 | easing: Easing.ease, 140 | }).start(); 141 | }); 142 | } 143 | 144 | 145 | /* --- Private methods --- */ 146 | 147 | _updateTabBarProps() { 148 | const { 149 | renderTabBarBackground, 150 | tabBarColor, 151 | tabBarBorderWidth, 152 | tabBarBorderColor, 153 | displayLabels, 154 | tabStyle, 155 | labelStyle, 156 | activeColor, 157 | inactiveColor, 158 | inactiveFontSize, 159 | activeFontSize, 160 | rippleColor, 161 | maskColor 162 | } = this.props; 163 | 164 | const { currentPage, scrollValue, containerWidth } = this.state; 165 | 166 | tabBarProps = { 167 | scrollToTop: this.scrollToTop.bind(this), 168 | goToPage: this.goToPage.bind(this), 169 | tabs: this._children().map((child) => { 170 | return { 171 | enabled: child.props.enabled !== undefined ? child.props.enabled : true, 172 | icon: child.props.tabIcon, 173 | name: child.props.tabLabel, 174 | maskColor: child.props.tabMaskColor, 175 | rippleColor: child.props.tabRippleColor, 176 | activeColor: child.props.tabActiveColor, 177 | backgroundColor: child.props.tabBackgroundColor, 178 | animationValue: new Animated.Value(0), 179 | badgeValue: child.props.badgeValue, 180 | badgeStyle: child.props.badgeStyle, 181 | renderBadge: child.props.renderBadge, 182 | }; 183 | }), 184 | activeTab: currentPage, 185 | renderBackground: renderTabBarBackground, 186 | backgroundColor: tabBarColor, 187 | borderWidth: tabBarBorderWidth, 188 | borderColor: tabBarBorderColor, 189 | displayLabels: displayLabels || DisplayLabels.DEFAULT, 190 | tabStyle: tabStyle, 191 | labelStyle: labelStyle, 192 | activeColor: activeColor, 193 | inactiveColor: inactiveColor, 194 | inactiveFontSize: inactiveFontSize || 12, 195 | activeFontSize: activeFontSize || 14, 196 | scrollValue: scrollValue, 197 | containerWidth: containerWidth, 198 | rippleColor: rippleColor || maskColor, 199 | maskColor: maskColor || rippleColor, 200 | }; 201 | } 202 | 203 | _handleLayout(e) { 204 | const { width, } = e.nativeEvent.layout; 205 | 206 | if (width !== this.state.containerWidth) { 207 | this.setState({ containerWidth: width, }); 208 | if (this.requestAnimationFrame) { 209 | this.requestAnimationFrame(() => { 210 | this.goToPage(this.state.currentPage); 211 | }); 212 | } 213 | } 214 | } 215 | 216 | _children() { 217 | return React.Children.map(this.props.children, (child) => child); 218 | } 219 | 220 | /* --- Rendering methods --- */ 221 | 222 | /** 223 | * Renders the component itself. 224 | */ 225 | render() { 226 | const { translucent, animatedTabSwitch } = this.props; 227 | const { currentPage, animationValue, bottomBarAnimation } = this.state; 228 | 229 | this._updateTabBarProps(); 230 | 231 | return ( 232 | 235 | 248 | { 249 | this._children().map((child, index) => { 250 | const isCurrentPage = (index === currentPage); 251 | 252 | return ( 253 | 260 | {child} 261 | 262 | ) 263 | }) 264 | } 265 | 266 | 267 | 280 | 281 | 282 | 283 | ); 284 | } 285 | } 286 | 287 | 288 | /* --- Stylesheet --- */ 289 | 290 | const styles = StyleSheet.create({ 291 | container: { 292 | flex: 1, 293 | overflow: 'hidden', 294 | }, 295 | 296 | scrollableContentContainerIOS: { 297 | flex: 1, 298 | }, 299 | 300 | scrollableContentIOS: { 301 | flexDirection: 'column', 302 | }, 303 | 304 | scrollableContentAndroid: { 305 | flex: 1, 306 | }, 307 | 308 | bottomBar: { 309 | position: 'absolute', 310 | height: 56, 311 | left: 0, 312 | right: 0, 313 | bottom: 0, 314 | } 315 | }); 316 | -------------------------------------------------------------------------------- /iOS/RCTBottomNavigation.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E0179A0F1D26842200E35B8F /* BNTouchableView.m in Sources */ = {isa = PBXBuildFile; fileRef = E0179A0D1D26842200E35B8F /* BNTouchableView.m */; }; 11 | E0179A101D26842200E35B8F /* BNTouchableViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E0179A0E1D26842200E35B8F /* BNTouchableViewManager.m */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXCopyFilesBuildPhase section */ 15 | 8A1B8E751B22E4E300DB45C2 /* CopyFiles */ = { 16 | isa = PBXCopyFilesBuildPhase; 17 | buildActionMask = 2147483647; 18 | dstPath = "include/$(PRODUCT_NAME)"; 19 | dstSubfolderSpec = 16; 20 | files = ( 21 | ); 22 | runOnlyForDeploymentPostprocessing = 0; 23 | }; 24 | /* End PBXCopyFilesBuildPhase section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 8A1B8E771B22E4E300DB45C2 /* libRCTBottomNavigation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTBottomNavigation.a; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | E0179A0C1D26842200E35B8F /* BNTouchableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNTouchableView.h; sourceTree = ""; }; 29 | E0179A0D1D26842200E35B8F /* BNTouchableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNTouchableView.m; sourceTree = ""; }; 30 | E0179A0E1D26842200E35B8F /* BNTouchableViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNTouchableViewManager.m; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 8A1B8E741B22E4E300DB45C2 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 8A1B8E6E1B22E4E300DB45C2 = { 45 | isa = PBXGroup; 46 | children = ( 47 | E0DDEA2D1CFF347800CD98CC /* RCTBottomNavigation */, 48 | 8A1B8E781B22E4E300DB45C2 /* Products */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | 8A1B8E781B22E4E300DB45C2 /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 8A1B8E771B22E4E300DB45C2 /* libRCTBottomNavigation.a */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | E0DDEA2D1CFF347800CD98CC /* RCTBottomNavigation */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | E0179A0C1D26842200E35B8F /* BNTouchableView.h */, 64 | E0179A0D1D26842200E35B8F /* BNTouchableView.m */, 65 | E0179A0E1D26842200E35B8F /* BNTouchableViewManager.m */, 66 | ); 67 | name = RCTBottomNavigation; 68 | sourceTree = ""; 69 | }; 70 | /* End PBXGroup section */ 71 | 72 | /* Begin PBXNativeTarget section */ 73 | 8A1B8E761B22E4E300DB45C2 /* RCTBottomNavigation */ = { 74 | isa = PBXNativeTarget; 75 | buildConfigurationList = 8A1B8E8B1B22E4E300DB45C2 /* Build configuration list for PBXNativeTarget "RCTBottomNavigation" */; 76 | buildPhases = ( 77 | 8A1B8E731B22E4E300DB45C2 /* Sources */, 78 | 8A1B8E741B22E4E300DB45C2 /* Frameworks */, 79 | 8A1B8E751B22E4E300DB45C2 /* CopyFiles */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | name = RCTBottomNavigation; 86 | productName = RCTMaterialKit; 87 | productReference = 8A1B8E771B22E4E300DB45C2 /* libRCTBottomNavigation.a */; 88 | productType = "com.apple.product-type.library.static"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | 8A1B8E6F1B22E4E300DB45C2 /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | LastUpgradeCheck = 0630; 97 | ORGANIZATIONNAME = ""; 98 | TargetAttributes = { 99 | 8A1B8E761B22E4E300DB45C2 = { 100 | CreatedOnToolsVersion = 6.3.2; 101 | }; 102 | }; 103 | }; 104 | buildConfigurationList = 8A1B8E721B22E4E300DB45C2 /* Build configuration list for PBXProject "RCTBottomNavigation" */; 105 | compatibilityVersion = "Xcode 3.2"; 106 | developmentRegion = English; 107 | hasScannedForEncodings = 0; 108 | knownRegions = ( 109 | en, 110 | ); 111 | mainGroup = 8A1B8E6E1B22E4E300DB45C2; 112 | productRefGroup = 8A1B8E781B22E4E300DB45C2 /* Products */; 113 | projectDirPath = ""; 114 | projectRoot = ""; 115 | targets = ( 116 | 8A1B8E761B22E4E300DB45C2 /* RCTBottomNavigation */, 117 | ); 118 | }; 119 | /* End PBXProject section */ 120 | 121 | /* Begin PBXSourcesBuildPhase section */ 122 | 8A1B8E731B22E4E300DB45C2 /* Sources */ = { 123 | isa = PBXSourcesBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | E0179A101D26842200E35B8F /* BNTouchableViewManager.m in Sources */, 127 | E0179A0F1D26842200E35B8F /* BNTouchableView.m in Sources */, 128 | ); 129 | runOnlyForDeploymentPostprocessing = 0; 130 | }; 131 | /* End PBXSourcesBuildPhase section */ 132 | 133 | /* Begin XCBuildConfiguration section */ 134 | 8A1B8E891B22E4E300DB45C2 /* Debug */ = { 135 | isa = XCBuildConfiguration; 136 | buildSettings = { 137 | ALWAYS_SEARCH_USER_PATHS = NO; 138 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 139 | CLANG_CXX_LIBRARY = "libc++"; 140 | CLANG_ENABLE_MODULES = YES; 141 | CLANG_ENABLE_OBJC_ARC = YES; 142 | CLANG_WARN_BOOL_CONVERSION = YES; 143 | CLANG_WARN_CONSTANT_CONVERSION = YES; 144 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 145 | CLANG_WARN_EMPTY_BODY = YES; 146 | CLANG_WARN_ENUM_CONVERSION = YES; 147 | CLANG_WARN_INT_CONVERSION = YES; 148 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 149 | CLANG_WARN_UNREACHABLE_CODE = YES; 150 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 151 | COPY_PHASE_STRIP = NO; 152 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 153 | ENABLE_STRICT_OBJC_MSGSEND = YES; 154 | GCC_C_LANGUAGE_STANDARD = gnu99; 155 | GCC_DYNAMIC_NO_PIC = NO; 156 | GCC_NO_COMMON_BLOCKS = YES; 157 | GCC_OPTIMIZATION_LEVEL = 0; 158 | GCC_PREPROCESSOR_DEFINITIONS = ( 159 | "DEBUG=1", 160 | "$(inherited)", 161 | ); 162 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 163 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 164 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 165 | GCC_WARN_UNDECLARED_SELECTOR = YES; 166 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 167 | GCC_WARN_UNUSED_FUNCTION = YES; 168 | GCC_WARN_UNUSED_VARIABLE = YES; 169 | HEADER_SEARCH_PATHS = ( 170 | "$(inherited)", 171 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 172 | "$(SRCROOT)/../../react-native/React/**", 173 | "$(SRCROOT)/../node_modules/react-native/React/**", 174 | ); 175 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 176 | MTL_ENABLE_DEBUG_INFO = YES; 177 | ONLY_ACTIVE_ARCH = YES; 178 | SDKROOT = iphoneos; 179 | }; 180 | name = Debug; 181 | }; 182 | 8A1B8E8A1B22E4E300DB45C2 /* Release */ = { 183 | isa = XCBuildConfiguration; 184 | buildSettings = { 185 | ALWAYS_SEARCH_USER_PATHS = NO; 186 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 187 | CLANG_CXX_LIBRARY = "libc++"; 188 | CLANG_ENABLE_MODULES = YES; 189 | CLANG_ENABLE_OBJC_ARC = YES; 190 | CLANG_WARN_BOOL_CONVERSION = YES; 191 | CLANG_WARN_CONSTANT_CONVERSION = YES; 192 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 193 | CLANG_WARN_EMPTY_BODY = YES; 194 | CLANG_WARN_ENUM_CONVERSION = YES; 195 | CLANG_WARN_INT_CONVERSION = YES; 196 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 197 | CLANG_WARN_UNREACHABLE_CODE = YES; 198 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 199 | COPY_PHASE_STRIP = NO; 200 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 201 | ENABLE_NS_ASSERTIONS = NO; 202 | ENABLE_STRICT_OBJC_MSGSEND = YES; 203 | GCC_C_LANGUAGE_STANDARD = gnu99; 204 | GCC_NO_COMMON_BLOCKS = YES; 205 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 206 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 207 | GCC_WARN_UNDECLARED_SELECTOR = YES; 208 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 209 | GCC_WARN_UNUSED_FUNCTION = YES; 210 | GCC_WARN_UNUSED_VARIABLE = YES; 211 | HEADER_SEARCH_PATHS = ( 212 | "$(inherited)", 213 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 214 | "$(SRCROOT)/../../react-native/React/**", 215 | "$(SRCROOT)/../node_modules/react-native/React/**", 216 | ); 217 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 218 | MTL_ENABLE_DEBUG_INFO = NO; 219 | SDKROOT = iphoneos; 220 | VALIDATE_PRODUCT = YES; 221 | }; 222 | name = Release; 223 | }; 224 | 8A1B8E8C1B22E4E300DB45C2 /* Debug */ = { 225 | isa = XCBuildConfiguration; 226 | buildSettings = { 227 | HEADER_SEARCH_PATHS = ""; 228 | OTHER_LDFLAGS = "-ObjC"; 229 | PRODUCT_NAME = RCTBottomNavigation; 230 | SKIP_INSTALL = YES; 231 | }; 232 | name = Debug; 233 | }; 234 | 8A1B8E8D1B22E4E300DB45C2 /* Release */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | HEADER_SEARCH_PATHS = ""; 238 | OTHER_LDFLAGS = "-ObjC"; 239 | PRODUCT_NAME = RCTBottomNavigation; 240 | SKIP_INSTALL = YES; 241 | }; 242 | name = Release; 243 | }; 244 | /* End XCBuildConfiguration section */ 245 | 246 | /* Begin XCConfigurationList section */ 247 | 8A1B8E721B22E4E300DB45C2 /* Build configuration list for PBXProject "RCTBottomNavigation" */ = { 248 | isa = XCConfigurationList; 249 | buildConfigurations = ( 250 | 8A1B8E891B22E4E300DB45C2 /* Debug */, 251 | 8A1B8E8A1B22E4E300DB45C2 /* Release */, 252 | ); 253 | defaultConfigurationIsVisible = 0; 254 | defaultConfigurationName = Release; 255 | }; 256 | 8A1B8E8B1B22E4E300DB45C2 /* Build configuration list for PBXNativeTarget "RCTBottomNavigation" */ = { 257 | isa = XCConfigurationList; 258 | buildConfigurations = ( 259 | 8A1B8E8C1B22E4E300DB45C2 /* Debug */, 260 | 8A1B8E8D1B22E4E300DB45C2 /* Release */, 261 | ); 262 | defaultConfigurationIsVisible = 0; 263 | defaultConfigurationName = Release; 264 | }; 265 | /* End XCConfigurationList section */ 266 | }; 267 | rootObject = 8A1B8E6F1B22E4E300DB45C2 /* Project object */; 268 | } 269 | -------------------------------------------------------------------------------- /BottomTabBar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bottom Tab Bar. 3 | * Tab bar implementation for the Bottom-Navigation-View. 4 | */ 5 | 'use strict'; 6 | 7 | /* --- Imports --- */ 8 | 9 | import React, {Component} from 'react'; 10 | import { 11 | Platform, 12 | Dimensions, 13 | StyleSheet, 14 | Animated, 15 | Easing, 16 | View, 17 | Image, 18 | } from 'react-native'; 19 | 20 | import PropTypes from 'prop-types'; 21 | 22 | import DisplayLabels from './DisplayLabels'; 23 | import Button from './Button'; 24 | import Ripple from './Ripple'; 25 | 26 | import parseColor from 'parse-color'; 27 | 28 | 29 | /* --- Member variables --- */ 30 | 31 | let tabPositions = {}; 32 | let backgroundColor; 33 | let maskColor; 34 | let rippleColor; 35 | 36 | 37 | /* --- Class methods --- */ 38 | 39 | export default class BottomTabBar extends Component { 40 | 41 | /* --- Component setup --- */ 42 | 43 | static propTypes = { 44 | goToPage: PropTypes.func, 45 | activeTab: PropTypes.number, 46 | tabs: PropTypes.array, 47 | underlineColor: PropTypes.string, 48 | backgroundColor: PropTypes.string, 49 | activeColor: PropTypes.string, 50 | inactiveColor: PropTypes.string, 51 | }; 52 | 53 | 54 | /* --- Lifecycle methods --- */ 55 | 56 | constructor(props) { 57 | super(props); 58 | 59 | let tabWidths = this.setTabWidth(this.props.tabs.length); 60 | let nextBackgroundColor = this.props.backgroundColor || 'rgba(0, 0, 0, 0)'; 61 | let activeTab = this.props.activeTab || 0; 62 | let animationValue = 0; 63 | 64 | if (this.props.tabs && this.props.tabs.length > 0 && this.props.tabs[activeTab].backgroundColor) { 65 | nextBackgroundColor = this.props.tabs[activeTab].backgroundColor; 66 | animationValue = 1; 67 | } 68 | 69 | let numberOfTabs = this.props.tabs.length; 70 | let screenWidth = Dimensions.get('window').width; 71 | let maxTabWidth = numberOfTabs <= 3 ? (3 * 168) : 168 + (numberOfTabs - 1) * 96; 72 | let justifyTabs = maxTabWidth < screenWidth ? 'center' : 'space-around'; 73 | 74 | this.state = { 75 | lastTab: activeTab, 76 | inactiveTabWidth: tabWidths.inactiveTabWidth, 77 | activeTabWidth: tabWidths.activeTabWidth, 78 | backgroundColor: this.props.backgroundColor || '#FFFFFF', 79 | nextBackgroundColor: nextBackgroundColor, 80 | animationValue: new Animated.Value(1), 81 | screenWidth, 82 | maxTabWidth, 83 | justifyTabs 84 | } 85 | } 86 | 87 | componentDidMount() { 88 | this.props.tabs[this.state.lastTab].animationValue.setValue(1); 89 | } 90 | 91 | componentWillReceiveProps(nextProps) { 92 | let tabWidths = this.setTabWidth(nextProps.tabs.length); 93 | 94 | let numberOfTabs = nextProps.tabs.length; 95 | let maxTabWidth = numberOfTabs <= 3 ? (3 * 168) : 168 + (numberOfTabs - 1) * 96; 96 | let justifyTabs = maxTabWidth < this.state.screenWidth ? 'center' : 'space-around'; 97 | 98 | this.setState({ 99 | lastTab: this.props.activeTab, 100 | inactiveTabWidth: tabWidths.inactiveTabWidth, 101 | activeTabWidth: tabWidths.activeTabWidth, 102 | backgroundColor: nextProps.backgroundColor || '#FFFFFF', 103 | nextBackgroundColor: nextProps.tabs[nextProps.activeTab || 0].backgroundColor, 104 | maxTabWidth, 105 | justifyTabs 106 | }); 107 | } 108 | 109 | 110 | /* --- Private methods --- */ 111 | 112 | setTabWidth(tabCount) { 113 | let screenWidth = Dimensions.get('window').width; 114 | 115 | // We have three tabs or less, distribute them evenly. 116 | if (tabCount <= 3 || this.props.displayLabels === DisplayLabels.ALWAYS) { 117 | let tabWidth = screenWidth / tabCount; 118 | 119 | if (tabWidth > 168) { 120 | tabWidth = 168; 121 | } 122 | 123 | return {inactiveTabWidth: tabWidth, activeTabWidth: tabWidth}; 124 | } 125 | 126 | // We have more than three tabs, calculate active and inactive tab width. 127 | else { 128 | let activeTabWidth = screenWidth / tabCount; 129 | 130 | if (activeTabWidth > 168) { 131 | activeTabWidth = 168; 132 | } else if (activeTabWidth < 96) { 133 | activeTabWidth = 96; 134 | } 135 | 136 | let inactiveTabWidth = activeTabWidth / 1.75; 137 | 138 | if (inactiveTabWidth > 96) { 139 | inactiveTabWidth = 96; 140 | } else if (inactiveTabWidth < 56) { 141 | inactiveTabWidth = 56; 142 | } 143 | 144 | return {inactiveTabWidth: inactiveTabWidth, activeTabWidth: activeTabWidth}; 145 | } 146 | } 147 | 148 | 149 | /* --- Rendering methods --- */ 150 | 151 | renderTabOption(tab, page) { 152 | const isTabActive = this.props.activeTab === page; 153 | const activeColor = this.props.activeColor || 'black'; 154 | const inactiveColor = this.props.inactiveColor || 'grey'; 155 | const badgeContainerStyle = {flex: 1, alignItems: 'center', justifyContent: 'center', position: 'absolute', 156 | left: 0, right: 0, top: 0, bottom: 0, paddingLeft: 16, paddingBottom: 18}; 157 | const badgeStyle = {height: 14, padding: 2.5, borderRadius: 7, backgroundColor: this.props.backgroundColor}; 158 | const iconStyle = {alignSelf: 'center', height: 24}; 159 | 160 | tab.animationValue.setValue(this.state.lastTab === page ? 1 : 0); 161 | 162 | Animated.timing(tab.animationValue, { 163 | toValue: isTabActive ? 1 : 0, 164 | duration: 150, 165 | }).start(); 166 | 167 | const hideLabels = this.props.displayLabels === DisplayLabels.NEVER; 168 | const showAllLabels = (this.props.tabs.length <= 3 && this.props.displayLabels !== DisplayLabels.ACTIVE_TAB_ONLY) || this.props.displayLabels === DisplayLabels.ALWAYS; 169 | 170 | return ( 171 | { 184 | let left = layoutEvent.nativeEvent.layout.x; 185 | let top = layoutEvent.nativeEvent.layout.y; 186 | 187 | tabPositions[tab.name] = {x: left, y: top}; 188 | }} 189 | > 190 | 349 | 350 | ); 351 | } 352 | 353 | render() { 354 | return ( 355 | 356 | { 357 | this.props.renderBackground ? 358 | 368 | {this.props.renderBackground()} 369 | 370 | : 371 | 383 | 391 | 392 | } 393 | 394 | 405 | 419 | 420 | 427 | 428 | {this.props.tabs.map((tab, i) => this.renderTabOption(tab, i))} 429 | 430 | 431 | ); 432 | } 433 | } 434 | 435 | 436 | /* --- Stylesheet --- */ 437 | 438 | const styles = StyleSheet.create({ 439 | container: { 440 | alignSelf: 'stretch', 441 | alignItems: 'center', 442 | height: 56, 443 | borderLeftWidth: 0, 444 | borderRightWidth: 0, 445 | borderBottomWidth: 0, 446 | }, 447 | 448 | tabs: { 449 | flex: 1, 450 | flexDirection: 'row', 451 | alignSelf: 'stretch', 452 | backgroundColor: 'rgba(0, 0, 0, 0)' 453 | }, 454 | 455 | ripple: { 456 | position: 'absolute', 457 | left: 0, 458 | right: 0, 459 | top: 0, 460 | bottom: 0, 461 | } 462 | }); 463 | --------------------------------------------------------------------------------