├── .github ├── demo-3tabs.gif └── demo-4tabs.gif ├── LICENSE ├── README.md ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── de │ └── timomeh │ └── bottomnavigation │ ├── RNBottomNavigation.java │ ├── RNBottomNavigationManager.java │ └── RNBottomNavigationPackage.java ├── index.android.js └── package.json /.github/demo-3tabs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timomeh/react-native-android-bottom-navigation/1ff943072c211fbec047f4830af8f3e7833fb856/.github/demo-3tabs.gif -------------------------------------------------------------------------------- /.github/demo-4tabs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timomeh/react-native-android-bottom-navigation/1ff943072c211fbec047f4830af8f3e7833fb856/.github/demo-4tabs.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Timo Mämecke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Native BottomNavigation for react-native 2 | 3 | ### Note: Development switched to [timomeh/react-native-material-bottom-navigation](https://github.com/timomeh/react-native-material-bottom-navigation)** 4 | 5 | This is a bridge to Android's native [BottomNavigation](https://material.io/guidelines/components/bottom-navigation.html). Because this is a Native Component, it only works with Android, not with iOS. 6 | 7 | Heavily inspired by react-native's implementation of ToolbarAndroid. 8 | 9 | ![with 3 tabs](.github/demo-3tabs.gif) ![with 4 tabs](.github/demo-4tabs.gif) 10 | 11 | 12 | ## Example 13 | 14 | ```js 15 | console.log(activeTab)} 34 | /> 35 | ``` 36 | 37 | ## Options 38 | 39 | All currently available options are used in the example above. 40 | 41 | - `activeTab` sets the active Tab. 42 | - `tabs` is the configuration for the rendered Tabs. 43 | - Up to 5 Tabs possible. 44 | - `labelColors` sets the color of the Tab's Label for the given state. 45 | - `iconTint` sets the tintColor of the Tab's Icon for the given state. (DOESN'T WORK CURRENTLY) 46 | - `onTabSelected` is a click listener with one parameter, which returns the index of the pressed Tab. 47 | 48 | ## TODO 49 | 50 | - Get `iconTint` to work. Maybe it has something to do with Fresco? Don't know. 51 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.1.3' 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 | targetSdkVersion 22 20 | versionCode 1 21 | versionName "1.0" 22 | } 23 | lintOptions { 24 | abortOnError false 25 | } 26 | } 27 | 28 | repositories { 29 | mavenCentral() 30 | } 31 | 32 | dependencies { 33 | compile "com.facebook.react:react-native:+" 34 | compile "com.android.support:design:25.0.1" 35 | } 36 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/src/main/java/de/timomeh/bottomnavigation/RNBottomNavigation.java: -------------------------------------------------------------------------------- 1 | package de.timomeh.bottomnavigation; 2 | 3 | import android.content.Context; 4 | import android.graphics.Color; 5 | import android.graphics.drawable.Animatable; 6 | import android.graphics.drawable.Drawable; 7 | import android.net.Uri; 8 | import android.support.design.widget.BottomNavigationView; 9 | import android.util.Log; 10 | import android.view.Menu; 11 | import android.view.MenuItem; 12 | import android.widget.Toast; 13 | 14 | import com.facebook.drawee.backends.pipeline.Fresco; 15 | import com.facebook.drawee.controller.BaseControllerListener; 16 | import com.facebook.drawee.drawable.ScalingUtils; 17 | import com.facebook.drawee.generic.GenericDraweeHierarchy; 18 | import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; 19 | import com.facebook.drawee.interfaces.DraweeController; 20 | import com.facebook.drawee.view.DraweeHolder; 21 | import com.facebook.drawee.view.MultiDraweeHolder; 22 | import com.facebook.imagepipeline.image.ImageInfo; 23 | import com.facebook.imagepipeline.image.QualityInfo; 24 | import com.facebook.react.bridge.ReadableArray; 25 | import com.facebook.react.bridge.ReadableMap; 26 | import com.facebook.react.uimanager.PixelUtil; 27 | import com.facebook.react.views.toolbar.DrawableWithIntrinsicSize; 28 | 29 | import javax.annotation.Nullable; 30 | 31 | /** 32 | * Created by @timomeh on 19/02/2017. 33 | */ 34 | 35 | public class RNBottomNavigation extends BottomNavigationView { 36 | 37 | private final MultiDraweeHolder mActionsHolder = 38 | new MultiDraweeHolder<>(); 39 | 40 | private abstract class IconControllerListener extends BaseControllerListener { 41 | 42 | private final DraweeHolder mHolder; 43 | private IconImageInfo mIconImageInfo; 44 | 45 | public IconControllerListener(DraweeHolder holder) { 46 | mHolder = holder; 47 | } 48 | 49 | public void setIconImageInfo(IconImageInfo iconImageInfo) { 50 | mIconImageInfo = iconImageInfo; 51 | } 52 | 53 | @Override 54 | public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) { 55 | super.onFinalImageSet(id, imageInfo, animatable); 56 | 57 | final ImageInfo info = mIconImageInfo != null ? mIconImageInfo : imageInfo; 58 | setDrawable(new DrawableWithIntrinsicSize(mHolder.getTopLevelDrawable(), info)); 59 | } 60 | 61 | protected abstract void setDrawable(Drawable d); 62 | } 63 | 64 | private class ActionIconControllerListener extends IconControllerListener { 65 | private final MenuItem mItem; 66 | 67 | ActionIconControllerListener(MenuItem item, DraweeHolder holder) { 68 | super(holder); 69 | mItem = item; 70 | } 71 | 72 | @Override 73 | protected void setDrawable(Drawable d) { 74 | mItem.setIcon(d); 75 | } 76 | } 77 | 78 | private static class IconImageInfo implements ImageInfo { 79 | private int mWidth; 80 | private int mHeight; 81 | 82 | public IconImageInfo(int width, int height) { 83 | mWidth = width; 84 | mHeight = height; 85 | } 86 | 87 | @Override 88 | public int getWidth() { 89 | return mWidth; 90 | } 91 | 92 | @Override 93 | public int getHeight() { 94 | return mHeight; 95 | } 96 | 97 | @Override 98 | public QualityInfo getQualityInfo() { 99 | return null; 100 | } 101 | } 102 | 103 | public RNBottomNavigation(Context context) { 104 | super(context); 105 | } 106 | 107 | private final Runnable mLayoutRunnable = new Runnable() { 108 | @Override 109 | public void run() { 110 | measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY), 111 | MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY)); 112 | layout(getLeft(), getTop(), getRight(), getBottom()); 113 | } 114 | }; 115 | 116 | @Override 117 | public void requestLayout() { 118 | super.requestLayout(); 119 | post(mLayoutRunnable); 120 | } 121 | 122 | @Override 123 | protected void onDetachedFromWindow() { 124 | super.onDetachedFromWindow(); 125 | detachDraweeHolders(); 126 | } 127 | 128 | @Override 129 | public void onStartTemporaryDetach() { 130 | super.onStartTemporaryDetach(); 131 | detachDraweeHolders(); 132 | } 133 | 134 | @Override 135 | protected void onAttachedToWindow() { 136 | super.onAttachedToWindow(); 137 | attachDraweeHolders(); 138 | } 139 | 140 | @Override 141 | public void onFinishTemporaryDetach() { 142 | super.onFinishTemporaryDetach(); 143 | attachDraweeHolders(); 144 | } 145 | 146 | private void detachDraweeHolders() { 147 | mActionsHolder.onDetach(); 148 | } 149 | 150 | private void attachDraweeHolders() { 151 | mActionsHolder.onAttach(); 152 | } 153 | 154 | public void setActions(@Nullable ReadableArray actions) { 155 | Menu menu = getMenu(); 156 | menu.clear(); 157 | mActionsHolder.clear(); 158 | if (actions != null) { 159 | for (int i = 0; i < actions.size(); i++) { 160 | ReadableMap action = actions.getMap(i); 161 | 162 | MenuItem item = menu.add(Menu.NONE, Menu.NONE, i, action.getString("title")); 163 | setMenuItemIcon(item, action.getMap("icon")); 164 | if (action.getBoolean("disabled")) { 165 | item.setEnabled(false); 166 | } 167 | } 168 | } 169 | } 170 | 171 | private void setMenuItemIcon(final MenuItem item, ReadableMap iconSource) { 172 | DraweeHolder holder = 173 | DraweeHolder.create(createDraweeHierarchy(), getContext()); 174 | ActionIconControllerListener controllerListener = 175 | new ActionIconControllerListener(item, holder); 176 | controllerListener.setIconImageInfo(getIconImageInfo(iconSource)); 177 | 178 | setIconSource(iconSource, controllerListener, holder); 179 | 180 | mActionsHolder.add(holder); 181 | } 182 | 183 | private void setIconSource(ReadableMap source, IconControllerListener controllerListener, 184 | DraweeHolder holder) { 185 | String uri = source != null ? source.getString("uri") : null; 186 | 187 | if (uri == null) { 188 | controllerListener.setIconImageInfo(null); 189 | controllerListener.setDrawable(null); 190 | } else if (uri.startsWith("http://") || uri.startsWith("https://") || 191 | uri.startsWith("file://")) { 192 | controllerListener.setIconImageInfo(getIconImageInfo(source)); 193 | DraweeController controller = Fresco.newDraweeControllerBuilder() 194 | .setUri(Uri.parse(uri)) 195 | .setControllerListener(controllerListener) 196 | .setOldController(holder.getController()) 197 | .build(); 198 | holder.setController(controller); 199 | holder.getTopLevelDrawable().setVisible(true, true); 200 | } else { 201 | controllerListener.setDrawable(getDrawableByName(uri)); 202 | } 203 | } 204 | 205 | private GenericDraweeHierarchy createDraweeHierarchy() { 206 | return new GenericDraweeHierarchyBuilder(getResources()) 207 | .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) 208 | .setFadeDuration(0) 209 | .build(); 210 | } 211 | 212 | private int getDrawableResourceByName(String name) { 213 | return getResources().getIdentifier( 214 | name, 215 | "drawable", 216 | getContext().getPackageName()); 217 | } 218 | 219 | private Drawable getDrawableByName(String name) { 220 | int drawableResId = getDrawableResourceByName(name); 221 | if (drawableResId != 0) { 222 | return getResources().getDrawable(getDrawableResourceByName(name)); 223 | } else { 224 | return null; 225 | } 226 | } 227 | 228 | private IconImageInfo getIconImageInfo(ReadableMap source) { 229 | if (source.hasKey("width") && source.hasKey("height")) { 230 | final int width = Math.round(PixelUtil.toPixelFromDIP(source.getInt("width"))); 231 | final int height = Math.round(PixelUtil.toPixelFromDIP(source.getInt("height"))); 232 | return new IconImageInfo(width, height); 233 | } else { 234 | return null; 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /android/src/main/java/de/timomeh/bottomnavigation/RNBottomNavigationManager.java: -------------------------------------------------------------------------------- 1 | package de.timomeh.bottomnavigation; 2 | 3 | import android.content.res.ColorStateList; 4 | import android.graphics.Color; 5 | import android.support.annotation.NonNull; 6 | import android.support.design.widget.BottomNavigationView; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | 10 | import com.facebook.react.bridge.Arguments; 11 | import com.facebook.react.bridge.ReadableArray; 12 | import com.facebook.react.bridge.ReadableMap; 13 | import com.facebook.react.bridge.WritableMap; 14 | import com.facebook.react.uimanager.ThemedReactContext; 15 | import com.facebook.react.uimanager.ViewGroupManager; 16 | import com.facebook.react.uimanager.annotations.ReactProp; 17 | import com.facebook.react.uimanager.events.RCTEventEmitter; 18 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | import javax.annotation.Nullable; 23 | 24 | /** 25 | * Created by @timomeh on 19/02/2017. 26 | */ 27 | 28 | public class RNBottomNavigationManager extends ViewGroupManager { 29 | 30 | private static final String REACT_CLASS = "RNBottomNavigation"; 31 | 32 | @Override 33 | public String getName() { 34 | return REACT_CLASS; 35 | } 36 | 37 | @Override 38 | protected RNBottomNavigation createViewInstance(final ThemedReactContext reactContext) { 39 | final RNBottomNavigation bottomNavigation = new RNBottomNavigation(reactContext); 40 | 41 | bottomNavigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { 42 | @Override 43 | public boolean onNavigationItemSelected(@NonNull MenuItem item) { 44 | WritableMap event = Arguments.createMap(); 45 | event.putInt("selectedPosition", item.getOrder()); 46 | reactContext.getJSModule(RCTEventEmitter.class).receiveEvent( 47 | bottomNavigation.getId(), 48 | "topChange", 49 | event 50 | ); 51 | 52 | return true; 53 | } 54 | }); 55 | 56 | return bottomNavigation; 57 | } 58 | 59 | @ReactProp(name = "tabs") 60 | public void setTabs(RNBottomNavigation view, @Nullable ReadableArray actions) { 61 | view.setActions(actions); 62 | } 63 | 64 | @ReactProp(name = "labelColors") 65 | public void setItemTextColor(RNBottomNavigation view, ReadableMap colorMap) { 66 | view.setItemTextColor(buildColorStateList(colorMap)); 67 | } 68 | 69 | @ReactProp(name = "iconTint") 70 | public void setItemIconTintColor(RNBottomNavigation view, ReadableMap colorMap) { 71 | view.setItemIconTintList(buildColorStateList(colorMap)); 72 | } 73 | 74 | @ReactProp(name = "activeTab", defaultInt = 0) 75 | public void setActiveTab(RNBottomNavigation view, int activeTab) { 76 | Menu menu = view.getMenu(); 77 | 78 | for (int i = 0; i < menu.size(); i++) { 79 | MenuItem menuItem = menu.getItem(i); 80 | if (i == activeTab) { 81 | menuItem.setChecked(true); 82 | } else { 83 | menuItem.setChecked(false); 84 | } 85 | } 86 | } 87 | 88 | private HashMap unifyColors(ReadableMap colorMap) { 89 | String normal = colorMap.getString("default"); 90 | String active = colorMap.hasKey("active") ? colorMap.getString("active") : normal; 91 | String disabled = colorMap.hasKey("disabled") ? colorMap.getString("disabled") : normal; 92 | 93 | HashMap map = new HashMap<>(); 94 | map.put("default", normal); 95 | map.put("active", active); 96 | map.put("disabled", disabled); 97 | 98 | return map; 99 | } 100 | 101 | private ColorStateList buildColorStateList(ReadableMap colorMap) { 102 | int[][] states = new int[][] { 103 | new int[] { android.R.attr.state_checked }, 104 | new int[] {-android.R.attr.state_enabled }, 105 | new int[] { }, 106 | }; 107 | 108 | Map colorStates = unifyColors(colorMap); 109 | 110 | int[] colors = new int[] { 111 | Color.parseColor(colorStates.get("active")), 112 | Color.parseColor(colorStates.get("disabled")), 113 | Color.parseColor(colorStates.get("default")) 114 | }; 115 | 116 | return new ColorStateList(states, colors); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /android/src/main/java/de/timomeh/bottomnavigation/RNBottomNavigationPackage.java: -------------------------------------------------------------------------------- 1 | package de.timomeh.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 | * Created by @timomeh on 19/02/2017. 15 | */ 16 | 17 | public class RNBottomNavigationPackage implements ReactPackage { 18 | @Override 19 | public List createNativeModules(ReactApplicationContext reactContext) { 20 | return Collections.emptyList(); 21 | } 22 | 23 | @Override 24 | public List> createJSModules() { 25 | return Collections.emptyList(); 26 | } 27 | 28 | @Override 29 | public List createViewManagers(ReactApplicationContext reactContext) { 30 | return Arrays.asList( 31 | new RNBottomNavigationManager() 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /index.android.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { requireNativeComponent } from 'react-native' 3 | import resolveAssetSource from 'resolveAssetSource' 4 | 5 | const RNBottomNavigation = requireNativeComponent('RNBottomNavigation', null, { 6 | nativeOnly: { 7 | onChange: true, 8 | accessibilityLabel: true, 9 | testID: true, 10 | importantForAccessibility: true, 11 | renderToHardwareTextureAndroid: true, 12 | onLayout: true, 13 | accessibilityLiveRegion: true, 14 | accessibilityComponentType: true, 15 | } 16 | }) 17 | 18 | class BottomNavigation extends Component { 19 | _onChange = (event) => { 20 | if (this.props.onTabSelected) { 21 | this.props.onTabSelected(event.nativeEvent.selectedPosition) 22 | } 23 | } 24 | 25 | render() { 26 | const nativeProps = { ...this.props } 27 | if (this.props.tabs) { 28 | const tabs = [] 29 | for (let i = 0; i < this.props.tabs.length; i++) { 30 | const tab = { 31 | ...this.props.tabs[i] 32 | } 33 | if (tab.icon) { 34 | tab.icon = resolveAssetSource(tab.icon) 35 | } 36 | if (tab.disabled == null) { 37 | tab.disabled = false 38 | } 39 | tabs.push(tab); 40 | } 41 | nativeProps.tabs = tabs 42 | } 43 | 44 | return 45 | } 46 | } 47 | 48 | const colorList = React.PropTypes.shape({ 49 | default: React.PropTypes.string, 50 | active: React.PropTypes.string 51 | }) 52 | 53 | BottomNavigation.propTypes = { 54 | tabs: React.PropTypes.arrayOf(React.PropTypes.shape({ 55 | title: React.PropTypes.string.isRequired, 56 | icon: React.PropTypes.any.isRequired, 57 | disabled: React.PropTypes.bool 58 | })).isRequired, 59 | labelColors: colorList, 60 | iconTint: colorList, 61 | activeTab: React.PropTypes.number, 62 | onTabSelected: React.PropTypes.func 63 | } 64 | 65 | export default BottomNavigation 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-android-bottom-navigation", 3 | "version": "0.1.0", 4 | "description": "react-native bridge to Android's native component BottomNavigation", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Timo Mämecke", 8 | "email": "maemecketimo@gmail.com" 9 | }, 10 | "homepage": "https://github.com/timomeh/react-native-android-bottom-navigation", 11 | "bugs": { 12 | "url": "https://github.com/timomeh/react-native-android-bottom-navigation/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/timomeh/react-native-android-bottom-navigation.git" 17 | } 18 | } 19 | --------------------------------------------------------------------------------