├── .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 |
--------------------------------------------------------------------------------