├── .eslintrc ├── .gitignore ├── README.md ├── Tab.js ├── TabLayout.js ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── xebia │ │ └── reactnative │ │ ├── ReactTabLayout.java │ │ ├── ReactTabStub.java │ │ ├── TabGravity.java │ │ ├── TabLayoutManager.java │ │ ├── TabLayoutPackage.java │ │ ├── TabManager.java │ │ ├── TabMode.java │ │ └── TabSelectedEvent.java │ └── res │ └── layout │ └── custom_tab_view.xml ├── build.gradle ├── docs └── manual_install.md ├── index.android.js ├── package.json └── src └── main └── java └── com └── xebia └── reactnative ├── GradleModifier.java ├── Installer.java ├── InstallerUtil.java └── JavaModifier.java /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "plugins": [ 5 | "react" 6 | ], 7 | 8 | "env": { 9 | "es6": true 10 | }, 11 | 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:react/recommended", 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | android/.gradle 2 | android/build 3 | .gradle 4 | build 5 | node_modules 6 | rn-cli.config.js 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Android TabLayout native component 2 | 3 | A React-Native (0.19+) wrapper for the standalone Android 4 | [TabLayout](http://developer.android.com/reference/android/support/design/widget/TabLayout.html) component. It's fully 5 | native and similar in use like the [TabBarIOS](https://facebook.github.io/react-native/docs/tabbarios.html) component. 6 | 7 | ![Animated example](https://i.imgur.com/nKFVnu4.gif) 8 | 9 | ## Example Project 10 | 11 | You can find an example project in [a separate repo](https://github.com/AlbertBrand/react-native-android-tablayout-example). 12 | 13 | ## Installation 14 | 15 | Install module with NPM: 16 | 17 | ```bash 18 | npm install --save react-native-android-tablayout 19 | ``` 20 | 21 | If you haven't installed [RNPM](https://github.com/rnpm/rnpm), run: 22 | 23 | ```bash 24 | npm install -g rnpm 25 | ``` 26 | 27 | After RNPM is installed: 28 | 29 | ```bash 30 | rnpm link react-native-android-tablayout 31 | ``` 32 | 33 | If you want to setup the project manually, see the [manual install guide](docs/manual_install.md). 34 | 35 | After setting up your project, run `react-native run-android` from the root to see if there are no compilation failures. 36 | 37 | ## Usage 38 | 39 | Make sure to import the `Tab` and `TabLayout` component in your script: 40 | 41 | ```javascript 42 | import { Tab, TabLayout } from 'react-native-android-tablayout'; 43 | ``` 44 | 45 | Then, create a tab layout as follows: 46 | 47 | ```javascript 48 | export default class MyComponent extends Component { 49 | render() { 50 | return ( 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | } 61 | ``` 62 | 63 | The `TabLayout` and `Tab` accept the following properties: 64 | 65 | ### TabLayout 66 | 67 | Prop | Type | Explanation 68 | --- | --- | --- 69 | selectedTab | number | Use for selecting the initial tab and/or connecting to state. See the [StatefulTabLayout example](https://github.com/AlbertBrand/react-native-android-tablayout-example/blob/master/app/StatefulTabLayout.js). 70 | selectedTabIndicatorColor | string | Color of indicator line. Specify in [CSS color format](https://facebook.github.io/react-native/docs/colors.html). 71 | tabGravity | string | Set tab gravity. Default 'fill', use 'center' when tabstrip needs to be centered. 72 | tabMode | string | Set tab mode. Default 'fixed', use 'scrollable' when tabstrip needs to scroll. 73 | onTabSelected | func | Provide callback function with `e:Event` as argument. When called, the selected tab position is found in `e.nativeEvent.position` (0-based). See the [StatefulTabLayout example](https://github.com/AlbertBrand/react-native-android-tablayout-example/blob/master/app/StatefulTabLayout.js). 74 | 75 | ### Tab 76 | 77 | Prop | Type | Explanation 78 | --- | --- | --- 79 | name | string | Tab name. 80 | iconResId | string | Icon resource ID. Points to a drawable, see the [IconsOnTopTabLayout example](https://github.com/AlbertBrand/react-native-android-tablayout-example/blob/master/app/IconsOnTopTabLayout.js). 81 | iconPackage | string | Icon resource package. If not provided, defaults to current package. Use 'android' for built-in icons. See the [IconsOnTopTabLayout example](https://github.com/AlbertBrand/react-native-android-tablayout-example/blob/master/app/IconsOnTopTabLayout.js). 82 | iconUri | string | Icon URI. Only allows file:// URIs. See how to combine with [react-native-vector-icons](https://github.com/oblador/react-native-vector-icons) in the [IconsOnTopTabLayout example](https://github.com/AlbertBrand/react-native-android-tablayout-example/blob/master/app/IconsOnTopTabLayout.js). 83 | iconSize | number | Icon size. 84 | textColor | string | Text color. Specify in [CSS color format](https://facebook.github.io/react-native/docs/colors.html). 85 | onTabSelected | func | Provide callback function with `e:Event` as argument. Called on the tab that was selected. When called, the selected tab position is found in `e.nativeEvent.position` (0-based). See the [StatefulTabLayout example](https://github.com/AlbertBrand/react-native-android-tablayout-example/blob/master/app/StatefulTabLayout.js). 86 | accessibilityLabel | string | Accessibility label for tab. Tabs are already set as accessible. 87 | 88 | Usage of these properties can be seen by example in [the example repo](https://github.com/AlbertBrand/react-native-android-tablayout-example). 89 | 90 | ## Custom views 91 | 92 | Since v0.2, you can define a custom view for a tab by adding child components to a `Tab` element: 93 | 94 | ```javascript 95 | export default class MyComponent extends Component { 96 | render() { 97 | return ( 98 | 99 | 100 | 101 | Tab 1 102 | Hey, multiline! 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | ); 113 | } 114 | } 115 | ``` 116 | 117 | You need to specify the width and height of the tab contents, else no contents will show up. This might be improved in the future. 118 | 119 | See the [CustomViewTabLayout example](https://github.com/AlbertBrand/react-native-android-tablayout-example/blob/master/app/CustomViewTabLayout.js) for a working example. 120 | 121 | ## Todo 122 | 123 | * add/remove tabs not implemented 124 | * custom views need a width and height to work 125 | 126 | PRs are welcome! 127 | -------------------------------------------------------------------------------- /Tab.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { 3 | Component, 4 | PropTypes 5 | } from 'react'; 6 | import { 7 | ColorPropType, 8 | processColor, 9 | requireNativeComponent, 10 | View 11 | } from 'react-native'; 12 | 13 | export default class Tab extends Component { 14 | static propTypes = { 15 | ...View.propTypes, 16 | iconPackage: PropTypes.string, 17 | iconResId: PropTypes.string, 18 | iconSize: PropTypes.number, 19 | iconUri: PropTypes.string, 20 | name: PropTypes.string, 21 | onTabSelected: PropTypes.func, 22 | textColor: ColorPropType 23 | }; 24 | 25 | onTabSelected: Function = (e) => { 26 | if (this.props.onTabSelected) { 27 | this.props.onTabSelected(e); 28 | } 29 | }; 30 | 31 | render() { 32 | const {style, children, ...otherProps} = this.props; 33 | const wrappedChildren = children ? 34 | : null; 40 | 41 | return ( 42 | 49 | ); 50 | } 51 | } 52 | 53 | const AndroidTab = requireNativeComponent('Tab', Tab); 54 | -------------------------------------------------------------------------------- /TabLayout.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { 3 | Component, 4 | PropTypes 5 | } from 'react'; 6 | import { 7 | ColorPropType, 8 | processColor, 9 | requireNativeComponent, 10 | View 11 | } from 'react-native'; 12 | 13 | export default class TabLayout extends Component { 14 | static propTypes = { 15 | ...View.propTypes, 16 | onTabSelected: PropTypes.func, 17 | selectedTab: PropTypes.number, 18 | selectedTabIndicatorColor: ColorPropType, 19 | tabGravity: PropTypes.oneOf(['fill', 'center']), 20 | tabMode: PropTypes.oneOf(['fixed', 'scrollable']) 21 | }; 22 | 23 | onTabSelected: Function = (e) => { 24 | if (this.props.onTabSelected) { 25 | this.props.onTabSelected(e); 26 | } 27 | }; 28 | 29 | render() { 30 | return ( 31 | 36 | ); 37 | } 38 | } 39 | 40 | const AndroidTabLayout = requireNativeComponent('TabLayout', TabLayout); 41 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:1.5.0' 7 | } 8 | } 9 | 10 | apply plugin: 'com.android.library' 11 | 12 | android { 13 | compileSdkVersion 23 14 | buildToolsVersion "23.0.1" 15 | 16 | defaultConfig { 17 | minSdkVersion 16 18 | targetSdkVersion 22 19 | versionCode 1 20 | versionName "1.0" 21 | } 22 | 23 | lintOptions { 24 | warning 'InvalidPackage' 25 | } 26 | } 27 | 28 | repositories { 29 | mavenCentral() 30 | } 31 | 32 | dependencies { 33 | compile 'com.facebook.react:react-native:+' 34 | compile 'com.android.support:design:23.0.1' 35 | } 36 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/java/com/xebia/reactnative/ReactTabLayout.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 2 | 3 | import android.content.Context; 4 | import android.support.design.widget.TabLayout; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class ReactTabLayout extends TabLayout { 10 | 11 | public ReactTabLayout(Context context) { 12 | super(context); 13 | } 14 | 15 | List tabStubs = new ArrayList<>(); 16 | 17 | InitialState initialState = InitialState.TAB_POSITION_UNSET; 18 | int initialTabPosition; 19 | 20 | enum InitialState { 21 | TAB_POSITION_UNSET, 22 | TAB_POSITION_SET, 23 | TAB_SELECTED 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /android/src/main/java/com/xebia/reactnative/ReactTabStub.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Color; 7 | import android.support.design.widget.TabLayout.Tab; 8 | import android.text.TextUtils; 9 | import android.util.Log; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.FrameLayout; 13 | import android.widget.ImageView; 14 | import android.widget.TextView; 15 | 16 | public class ReactTabStub extends ViewGroup { 17 | public static final String TAG = "ReactTabStub"; 18 | 19 | public ReactTabStub(Context context) { 20 | super(context); 21 | } 22 | 23 | @Override 24 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 25 | // never called, needsCustomLayoutForChildren for parent is true 26 | } 27 | 28 | Tab tab; 29 | 30 | private View customView; 31 | private TextView tabText; 32 | private ImageView tabImage; 33 | 34 | private String name; 35 | private String iconResId; 36 | private String iconPackage; 37 | private String iconUri; 38 | private int iconSize; 39 | private int textColor; 40 | 41 | public void attachCustomTabView(Tab tab) { 42 | Log.d(TAG, "attachCustomTabView"); 43 | 44 | this.tab = tab; 45 | 46 | if (customView == null) { 47 | tab.setCustomView(R.layout.custom_tab_view); 48 | customView = tab.getCustomView(); 49 | assert customView != null; 50 | 51 | tabText = (TextView) customView.findViewById(R.id.tabText); 52 | tabImage = (ImageView) customView.findViewById(R.id.tabImage); 53 | 54 | if (name != null) { 55 | nameChanged(); 56 | } 57 | if (textColor != 0) { 58 | textColorChanged(); 59 | } 60 | if (iconUri != null) { 61 | iconUriChanged(); 62 | } else if (iconResId != null) { 63 | iconResourceChanged(); 64 | } 65 | if (iconSize > 0) { 66 | iconSizeChanged(); 67 | } 68 | } else { 69 | customViewChanged(); 70 | } 71 | } 72 | 73 | public void setName(String name) { 74 | this.name = name; 75 | nameChanged(); 76 | } 77 | 78 | public void setIconResId(String iconResId) { 79 | this.iconResId = iconResId; 80 | iconResourceChanged(); 81 | } 82 | 83 | public void setIconPackage(String iconPackage) { 84 | this.iconPackage = iconPackage; 85 | iconResourceChanged(); 86 | } 87 | 88 | public void setIconUri(String iconUri) { 89 | this.iconUri = iconUri; 90 | iconUriChanged(); 91 | } 92 | 93 | public void setIconSize(int iconSize) { 94 | this.iconSize = iconSize; 95 | iconSizeChanged(); 96 | } 97 | 98 | public void setTextColor(int textColor) { 99 | this.textColor = textColor; 100 | textColorChanged(); 101 | } 102 | 103 | public void setCustomView(View customView) { 104 | this.customView = customView; 105 | customViewChanged(); 106 | } 107 | 108 | public void setAccessibilityLabel(String accessibilityLabel) { 109 | setContentDescription(accessibilityLabel); 110 | accessibilityLabelChanged(); 111 | } 112 | 113 | private void nameChanged() { 114 | if (tabText == null) return; 115 | Log.d(TAG, "nameChanged: " + name); 116 | 117 | tabText.setText(name); 118 | 119 | updateLayout(); 120 | } 121 | 122 | private void iconResourceChanged() { 123 | if (tabImage == null) return; 124 | String packageName = iconPackage != null ? iconPackage : getContext().getPackageName(); 125 | Log.d(TAG, "iconResourceChanged, id: " + iconResId + " package: " + packageName); 126 | 127 | if (!TextUtils.isEmpty(iconResId)) { 128 | try { 129 | int resId = getContext().getResources().getIdentifier(iconResId, "drawable", packageName); 130 | tabImage.setImageResource(resId); 131 | tabImage.setVisibility(View.VISIBLE); 132 | } catch (Exception e) { 133 | Log.e(TAG, "Icon resource id " + iconResId + " with package " + packageName + " not found", e); 134 | } 135 | } else { 136 | tabImage.setVisibility(View.GONE); 137 | } 138 | 139 | updateLayout(); 140 | } 141 | 142 | private void iconUriChanged() { 143 | if (tabImage == null) return; 144 | Log.d(TAG, "iconUriChanged: " + iconUri); 145 | 146 | if (iconUri.startsWith("file://")) { 147 | String pathName = iconUri.substring(7); 148 | Bitmap bm = BitmapFactory.decodeFile(pathName); 149 | tabImage.setImageBitmap(bm); 150 | tabImage.setVisibility(View.VISIBLE); 151 | } else if (TextUtils.isEmpty(iconUri)) { 152 | tabImage.setVisibility(View.GONE); 153 | } else { 154 | Log.e(TAG, "Icon uri only supports file:// for now, saw " + iconUri); 155 | } 156 | 157 | updateLayout(); 158 | } 159 | 160 | private void iconSizeChanged() { 161 | if (tabImage == null) return; 162 | Log.d(TAG, "iconSizeChanged: " + iconSize); 163 | 164 | float scale = getContext().getResources().getDisplayMetrics().density; 165 | int size = Math.round(iconSize * scale); 166 | tabImage.getLayoutParams().width = size; 167 | tabImage.getLayoutParams().height = size; 168 | 169 | updateLayout(); 170 | } 171 | 172 | private void textColorChanged() { 173 | if (tabText == null) return; 174 | Log.d(TAG, "textColorChanged: " + textColor); 175 | 176 | tabText.setTextColor(textColor); 177 | } 178 | 179 | public void accessibilityLabelChanged() { 180 | if (customView == null || customView.getParent() == null) return; 181 | CharSequence contentDescription = getContentDescription(); 182 | Log.d(TAG, "accessibilityLabelChanged: " + contentDescription); 183 | 184 | ViewGroup parent = (ViewGroup) customView.getParent(); 185 | parent.setContentDescription(contentDescription); 186 | } 187 | 188 | public void customViewChanged() { 189 | if (tab == null) return; 190 | Log.d(TAG, "customViewChanged"); 191 | 192 | FrameLayout wrapperView = new FrameLayout(getContext()) { 193 | @Override 194 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 195 | // set measurements based on the TabView dimensions 196 | int parentWidth = MeasureSpec.getSize(widthMeasureSpec); 197 | int parentHeight = MeasureSpec.getSize(heightMeasureSpec); 198 | setMeasuredDimension(parentWidth, parentHeight); 199 | } 200 | }; 201 | 202 | wrapperView.addView(customView); 203 | tab.setCustomView(wrapperView); 204 | updateLayout(); 205 | } 206 | 207 | private void updateLayout() { 208 | if (customView == null || customView.getParent() == null) return; 209 | 210 | View tabView = (View) customView.getParent(); 211 | tabView.measure( 212 | View.MeasureSpec.makeMeasureSpec(tabView.getMeasuredWidth(), View.MeasureSpec.EXACTLY), 213 | View.MeasureSpec.makeMeasureSpec(tabView.getMeasuredHeight(), View.MeasureSpec.EXACTLY)); 214 | tabView.layout(tabView.getLeft(), tabView.getTop(), tabView.getRight(), tabView.getBottom()); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /android/src/main/java/com/xebia/reactnative/TabGravity.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 2 | 3 | import android.support.design.widget.TabLayout; 4 | 5 | enum TabGravity { 6 | FILL(TabLayout.GRAVITY_FILL), 7 | CENTER(TabLayout.GRAVITY_CENTER); 8 | 9 | public int gravity; 10 | 11 | TabGravity(int gravity) { 12 | this.gravity = gravity; 13 | } 14 | 15 | public static TabGravity fromString(String text) { 16 | return text != null ? Enum.valueOf(TabGravity.class, text.trim().toUpperCase()) : null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /android/src/main/java/com/xebia/reactnative/TabLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 2 | 3 | import android.support.design.widget.TabLayout.OnTabSelectedListener; 4 | import android.support.design.widget.TabLayout.Tab; 5 | import android.util.Log; 6 | import android.view.View; 7 | import com.facebook.react.bridge.JSApplicationIllegalArgumentException; 8 | import com.facebook.react.common.MapBuilder; 9 | import com.facebook.react.uimanager.ThemedReactContext; 10 | import com.facebook.react.uimanager.UIManagerModule; 11 | import com.facebook.react.uimanager.ViewGroupManager; 12 | import com.facebook.react.uimanager.annotations.ReactProp; 13 | import com.facebook.react.uimanager.events.EventDispatcher; 14 | import com.xebia.reactnative.ReactTabLayout.InitialState; 15 | 16 | import java.util.Map; 17 | 18 | public class TabLayoutManager extends ViewGroupManager { 19 | public static final String REACT_CLASS = "TabLayout"; 20 | 21 | private EventDispatcher mEventDispatcher; 22 | 23 | @Override 24 | public String getName() { 25 | return REACT_CLASS; 26 | } 27 | 28 | @Override 29 | protected ReactTabLayout createViewInstance(ThemedReactContext themedReactContext) { 30 | Log.d(REACT_CLASS, "createViewInstance"); 31 | ReactTabLayout tabLayout = new ReactTabLayout(themedReactContext); 32 | tabLayout.setOnTabSelectedListener(new TabLayoutOnTabSelectedListener(tabLayout)); 33 | return tabLayout; 34 | } 35 | 36 | @Override 37 | public void addView(ReactTabLayout tabLayout, View child, int index) { 38 | Log.d(REACT_CLASS, "addView"); 39 | if (!(child instanceof ReactTabStub)) { 40 | throw new JSApplicationIllegalArgumentException("The TabLayout can only have Tab children"); 41 | } 42 | 43 | Tab tab = tabLayout.newTab(); 44 | tabLayout.addTab(tab); 45 | 46 | ReactTabStub tabStub = (ReactTabStub) child; 47 | tabStub.attachCustomTabView(tab); 48 | 49 | tabLayout.tabStubs.add(tabStub); 50 | 51 | // set accessibilityLabel on parent TabView, which is now available after addTab call 52 | if (tabStub.getContentDescription() != null) { 53 | tabStub.accessibilityLabelChanged(); 54 | } 55 | 56 | // when initial position was stored, update tab selection 57 | if (tabLayout.initialState == InitialState.TAB_POSITION_SET && 58 | tabLayout.initialTabPosition == index) { 59 | tabLayout.initialState = InitialState.TAB_SELECTED; 60 | tab.select(); 61 | } 62 | } 63 | 64 | @ReactProp(name = "selectedTab", defaultInt = 0) 65 | public void setSelectedTab(ReactTabLayout view, int selectedTab) { 66 | Log.d(REACT_CLASS, "selectedTab " + selectedTab); 67 | selectTab(view, selectedTab); 68 | } 69 | 70 | @ReactProp(name = "selectedTabIndicatorColor") 71 | public void setSelectedTabIndicatorColor(ReactTabLayout view, int indicatorColor) { 72 | Log.d(REACT_CLASS, "selectedTabIndicatorColor " + indicatorColor); 73 | view.setSelectedTabIndicatorColor(indicatorColor); 74 | } 75 | 76 | @ReactProp(name = "tabMode") 77 | public void setTabMode(ReactTabLayout view, String mode) { 78 | Log.d(REACT_CLASS, "tabMode " + mode); 79 | try { 80 | TabMode tabMode = TabMode.fromString(mode); 81 | view.setTabMode(tabMode.mode); 82 | } catch (IllegalArgumentException e) { 83 | Log.w(REACT_CLASS, "No valid tabMode: " + mode); 84 | } 85 | } 86 | 87 | @ReactProp(name = "tabGravity") 88 | public void setTabGravity(ReactTabLayout view, String gravity) { 89 | Log.d(REACT_CLASS, "tabGravity " + gravity); 90 | try { 91 | TabGravity tabGravity = TabGravity.fromString(gravity); 92 | view.setTabGravity(tabGravity.gravity); 93 | } catch (IllegalArgumentException e) { 94 | Log.w(REACT_CLASS, "No valid tabGravity: " + gravity); 95 | } 96 | } 97 | 98 | @Override 99 | public boolean needsCustomLayoutForChildren() { 100 | // don't bother to layout the child tab stub views 101 | return true; 102 | } 103 | 104 | @Override 105 | protected void addEventEmitters(ThemedReactContext reactContext, ReactTabLayout view) { 106 | mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); 107 | } 108 | 109 | @Override 110 | public Map getExportedCustomDirectEventTypeConstants() { 111 | return MapBuilder.of( 112 | TabSelectedEvent.EVENT_NAME, (Object) MapBuilder.of("registrationName", "onTabSelected") 113 | ); 114 | } 115 | 116 | private void selectTab(ReactTabLayout tabLayout, int position) { 117 | if (position < 0 || position > tabLayout.getTabCount() - 1) { 118 | if (tabLayout.initialState == InitialState.TAB_POSITION_UNSET) { 119 | // store initial position until tab is added 120 | tabLayout.initialTabPosition = position; 121 | tabLayout.initialState = InitialState.TAB_POSITION_SET; 122 | } else { 123 | Log.w(REACT_CLASS, "Tried to select out of bounds tab"); 124 | } 125 | return; 126 | } 127 | Tab tab = tabLayout.getTabAt(position); 128 | if (tab != null) { 129 | tab.select(); 130 | } 131 | } 132 | 133 | class TabLayoutOnTabSelectedListener implements OnTabSelectedListener { 134 | private final ReactTabLayout mTabLayout; 135 | 136 | TabLayoutOnTabSelectedListener(ReactTabLayout tabLayout) { 137 | this.mTabLayout = tabLayout; 138 | } 139 | 140 | @Override 141 | public void onTabSelected(Tab tab) { 142 | if (mTabLayout.initialState == InitialState.TAB_POSITION_SET) { 143 | // don't send tabSelected events when initial tab is set but not selected yet 144 | return; 145 | } 146 | ReactTabStub tabStub = findTabStubFor(tab); 147 | if (tabStub == null) { 148 | return; 149 | } 150 | Log.d(REACT_CLASS, "dispatchEvent"); 151 | int position = mTabLayout.tabStubs.indexOf(tabStub); 152 | mEventDispatcher.dispatchEvent(new TabSelectedEvent(tabStub.getId(), position)); 153 | mEventDispatcher.dispatchEvent(new TabSelectedEvent(mTabLayout.getId(), position)); 154 | } 155 | 156 | @Override 157 | public void onTabUnselected(Tab tab) { 158 | } 159 | 160 | @Override 161 | public void onTabReselected(Tab tab) { 162 | } 163 | 164 | private ReactTabStub findTabStubFor(Tab tab) { 165 | for (ReactTabStub tabStub : mTabLayout.tabStubs) { 166 | if (tabStub.tab.equals(tab)) { 167 | return tabStub; 168 | } 169 | } 170 | return null; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /android/src/main/java/com/xebia/reactnative/TabLayoutPackage.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 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 | public class TabLayoutPackage implements ReactPackage { 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactApplicationContext) { 16 | return Collections.emptyList(); 17 | } 18 | 19 | @Override 20 | public List> createJSModules() { 21 | return Collections.emptyList(); 22 | } 23 | 24 | @Override 25 | public List createViewManagers(ReactApplicationContext reactApplicationContext) { 26 | return Arrays.asList( 27 | new TabLayoutManager(), 28 | new TabManager() 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /android/src/main/java/com/xebia/reactnative/TabManager.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 2 | 3 | import android.util.Log; 4 | import android.view.View; 5 | import com.facebook.react.bridge.JSApplicationIllegalArgumentException; 6 | import com.facebook.react.common.MapBuilder; 7 | import com.facebook.react.uimanager.LayoutShadowNode; 8 | import com.facebook.react.uimanager.ViewGroupManager; 9 | import com.facebook.react.uimanager.annotations.ReactProp; 10 | import com.facebook.react.uimanager.SimpleViewManager; 11 | import com.facebook.react.uimanager.ThemedReactContext; 12 | 13 | import java.util.Map; 14 | 15 | public class TabManager extends ViewGroupManager { 16 | public static final String REACT_CLASS = "Tab"; 17 | 18 | @Override 19 | public String getName() { 20 | return REACT_CLASS; 21 | } 22 | 23 | @Override 24 | protected ReactTabStub createViewInstance(ThemedReactContext themedReactContext) { 25 | return new ReactTabStub(themedReactContext); 26 | } 27 | 28 | @Override 29 | public void addView(ReactTabStub view, View child, int index) { 30 | Log.d(REACT_CLASS, "addView"); 31 | if (index != 0) { 32 | throw new JSApplicationIllegalArgumentException("The Tab can only have a single child view"); 33 | } 34 | view.setCustomView(child); 35 | } 36 | 37 | @ReactProp(name = "name") 38 | public void setName(ReactTabStub view, String name) { 39 | view.setName(name); 40 | } 41 | 42 | @ReactProp(name = "iconResId") 43 | public void setIconResId(ReactTabStub view, String iconResId) { 44 | view.setIconResId(iconResId); 45 | } 46 | 47 | @ReactProp(name = "iconPackage") 48 | public void setIconPackage(ReactTabStub view, String iconPackage) { 49 | view.setIconPackage(iconPackage); 50 | } 51 | 52 | @ReactProp(name = "iconUri") 53 | public void setIconUri(ReactTabStub view, String iconUri) { 54 | view.setIconUri(iconUri); 55 | } 56 | 57 | @ReactProp(name = "iconSize") 58 | public void setIconSize(ReactTabStub view, int iconSize) { 59 | view.setIconSize(iconSize); 60 | } 61 | 62 | @ReactProp(name = "textColor") 63 | public void setTextColor(ReactTabStub view, int textColor) { 64 | view.setTextColor(textColor); 65 | } 66 | 67 | @Override 68 | public void setAccessibilityLabel(ReactTabStub view, String accessibilityLabel) { 69 | view.setAccessibilityLabel(accessibilityLabel); 70 | } 71 | 72 | @Override 73 | public Map getExportedCustomDirectEventTypeConstants() { 74 | return MapBuilder.of( 75 | TabSelectedEvent.EVENT_NAME, (Object) MapBuilder.of("registrationName", "onTabSelected") 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /android/src/main/java/com/xebia/reactnative/TabMode.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 2 | 3 | import android.support.design.widget.TabLayout; 4 | 5 | enum TabMode { 6 | SCROLLABLE(TabLayout.MODE_SCROLLABLE), 7 | FIXED(TabLayout.MODE_FIXED); 8 | 9 | public int mode; 10 | 11 | TabMode(int mode) { 12 | this.mode = mode; 13 | } 14 | 15 | public static TabMode fromString(String text) { 16 | return text != null ? Enum.valueOf(TabMode.class, text.trim().toUpperCase()) : null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /android/src/main/java/com/xebia/reactnative/TabSelectedEvent.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 2 | 3 | import android.os.SystemClock; 4 | import com.facebook.react.bridge.Arguments; 5 | import com.facebook.react.bridge.WritableMap; 6 | import com.facebook.react.uimanager.events.Event; 7 | import com.facebook.react.uimanager.events.RCTEventEmitter; 8 | 9 | public class TabSelectedEvent extends Event { 10 | public static final String EVENT_NAME = "tabSelected"; 11 | private final int position; 12 | 13 | public TabSelectedEvent(int viewTag, int position) { 14 | super(viewTag); 15 | this.position = position; 16 | } 17 | 18 | @Override 19 | public String getEventName() { 20 | return EVENT_NAME; 21 | } 22 | 23 | @Override 24 | public void dispatch(RCTEventEmitter rctEventEmitter) { 25 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); 26 | } 27 | 28 | private WritableMap serializeEventData() { 29 | WritableMap eventData = Arguments.createMap(); 30 | eventData.putInt("position", position); 31 | return eventData; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /android/src/main/res/layout/custom_tab_view.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | compile 'com.github.javaparser:javaparser-core:2.3.0' 9 | } 10 | 11 | task install(type: JavaExec) { 12 | main 'com.xebia.reactnative.Installer' 13 | classpath sourceSets.main.runtimeClasspath 14 | workingDir '../..' 15 | } 16 | -------------------------------------------------------------------------------- /docs/manual_install.md: -------------------------------------------------------------------------------- 1 | # Manual install guide 2 | 3 | Edit `android/settings.gradle`: 4 | 5 | ```gradle 6 | // ... 7 | 8 | include ':react-native-android-tablayout' 9 | project(':react-native-android-tablayout').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-android-tablayout/android') 10 | ``` 11 | 12 | Edit `android/app/build.gradle`: 13 | 14 | ```gradle 15 | // ... 16 | 17 | dependencies { 18 | // ... 19 | compile project(':react-native-android-tablayout') 20 | } 21 | ``` 22 | 23 | Register module in `MainActivity.java` for React-Native 0.19+: 24 | 25 | ```java 26 | // ... 27 | 28 | import com.xebia.reactnative.TabLayoutPackage; // <--- import 29 | 30 | public class MainActivity extends ReactActivity { 31 | // ... 32 | 33 | @Override 34 | protected List getPackages() { 35 | return Arrays.asList( 36 | new MainReactPackage(), 37 | new TabLayoutPackage() // <--- add package 38 | ); 39 | } 40 | 41 | // ... 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /index.android.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import Tab from './Tab'; 3 | import TabLayout from './TabLayout'; 4 | 5 | export { Tab, TabLayout }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-android-tablayout", 3 | "version": "0.3.0", 4 | "description": "React Native Android native TabLayout component", 5 | "main": "index.android.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "flow": "node_modules/.bin/flow check", 9 | "lint": "node_modules/.bin/eslint .", 10 | "prepush": "npm run lint" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/AlbertBrand/react-native-android-tablayout.git" 15 | }, 16 | "keywords": [ 17 | "react-component", 18 | "react-native", 19 | "android", 20 | "tablayout", 21 | "design", 22 | "support" 23 | ], 24 | "author": "Albert Brand ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/AlbertBrand/react-native-android-tablayout/issues" 28 | }, 29 | "homepage": "https://github.com/AlbertBrand/react-native-android-tablayout#readme", 30 | "devDependencies": { 31 | "babel-eslint": "6.1.0", 32 | "eslint": "2.13.1", 33 | "eslint-plugin-react": "5.2.2", 34 | "flow-bin": "0.26.0", 35 | "husky": "0.11.4" 36 | }, 37 | "peerDependencies": { 38 | "react-native": "0.33" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/xebia/reactnative/GradleModifier.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 2 | 3 | import java.nio.file.Files; 4 | import java.nio.file.Paths; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | public class GradleModifier { 9 | public static final String SETTINGS_GRADLE_PATH = "android/settings.gradle"; 10 | public static final String APP_BUILD_GRADLE_PATH = "android/app/build.gradle"; 11 | 12 | public static final String TABLAYOUT_INCLUDE_EXPR = "include ':react-native-android-tablayout'"; 13 | public static final String TABLAYOUT_PROJECTDIR_EXPR = "project(':react-native-android-tablayout').projectDir = new File('../node_modules/react-native-android-tablayout/android')"; 14 | 15 | public static final String TABLAYOUT_DEPENDENCY_EXPR = "compile project(':react-native-android-tablayout')"; 16 | public static final Pattern DEPENDENCIES_PATTERN = Pattern.compile( 17 | "(dependencies\\s*\\{)" + // $1 18 | "([^}]*)" + // $2 19 | "}" 20 | ); 21 | 22 | public void updateSettings() throws Exception { 23 | String content = new String(Files.readAllBytes(Paths.get(SETTINGS_GRADLE_PATH))); 24 | String newContent = null; 25 | 26 | if (!content.contains(TABLAYOUT_INCLUDE_EXPR)) { 27 | newContent = content.concat("\n\n" + TABLAYOUT_INCLUDE_EXPR + "\n" + TABLAYOUT_PROJECTDIR_EXPR + "\n"); 28 | } 29 | 30 | if (newContent != null) { 31 | InstallerUtil.writeToDisk(SETTINGS_GRADLE_PATH, newContent); 32 | System.out.println("Updated includes in " + SETTINGS_GRADLE_PATH); 33 | } else { 34 | System.out.println("Includes were not updated, is the file " + SETTINGS_GRADLE_PATH + " updated already?"); 35 | } 36 | } 37 | 38 | public void updateAppBuild() throws Exception { 39 | String content = new String(Files.readAllBytes(Paths.get(APP_BUILD_GRADLE_PATH))); 40 | String newContent = null; 41 | 42 | Matcher m = DEPENDENCIES_PATTERN.matcher(content); 43 | if (m.find() && !m.group(2).contains(TABLAYOUT_DEPENDENCY_EXPR)) { 44 | newContent = m.replaceFirst("$1$2 " + TABLAYOUT_DEPENDENCY_EXPR + "\n}"); 45 | } 46 | 47 | if (newContent != null) { 48 | InstallerUtil.writeToDisk(APP_BUILD_GRADLE_PATH, newContent); 49 | System.out.println("Updated dependencies in " + APP_BUILD_GRADLE_PATH); 50 | } else { 51 | System.out.println("Dependencies were not updated, is the file " + APP_BUILD_GRADLE_PATH + " updated already?"); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/xebia/reactnative/Installer.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 2 | 3 | class Installer { 4 | private GradleModifier groovyModifier = new GradleModifier(); 5 | private JavaModifier javaModifier = new JavaModifier(); 6 | 7 | public static void main(String[] args) throws Exception { 8 | new Installer().run(); 9 | } 10 | 11 | private void run() throws Exception { 12 | groovyModifier.updateSettings(); 13 | groovyModifier.updateAppBuild(); 14 | javaModifier.updateMainActivity(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xebia/reactnative/InstallerUtil.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Paths; 6 | import java.nio.file.StandardOpenOption; 7 | 8 | public class InstallerUtil { 9 | static void writeToDisk(String path, String content) throws IOException { 10 | Files.write(Paths.get(path), content.getBytes(), StandardOpenOption.CREATE); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/xebia/reactnative/JavaModifier.java: -------------------------------------------------------------------------------- 1 | package com.xebia.reactnative; 2 | 3 | import com.github.javaparser.JavaParser; 4 | import com.github.javaparser.ast.CompilationUnit; 5 | import com.github.javaparser.ast.ImportDeclaration; 6 | import com.github.javaparser.ast.body.BodyDeclaration; 7 | import com.github.javaparser.ast.body.MethodDeclaration; 8 | import com.github.javaparser.ast.body.TypeDeclaration; 9 | import com.github.javaparser.ast.expr.Expression; 10 | import com.github.javaparser.ast.expr.MethodCallExpr; 11 | import com.github.javaparser.ast.expr.NameExpr; 12 | import com.github.javaparser.ast.expr.ObjectCreationExpr; 13 | import com.github.javaparser.ast.stmt.ReturnStmt; 14 | import com.github.javaparser.ast.stmt.Statement; 15 | import com.github.javaparser.ast.type.ClassOrInterfaceType; 16 | 17 | import java.io.FileInputStream; 18 | import java.io.FileNotFoundException; 19 | import java.io.IOException; 20 | import java.nio.file.*; 21 | import java.nio.file.attribute.BasicFileAttributes; 22 | import java.util.List; 23 | 24 | public class JavaModifier { 25 | public static final String TAB_LAYOUT_PACKAGE_CLASS = "com.xebia.reactnative.TabLayoutPackage"; 26 | public static final String TAB_LAYOUT_PACKAGE_NAME = "TabLayoutPackage"; 27 | public static final String GET_PACKAGES_METHOD = "getPackages"; 28 | 29 | private String mainActivityPath; 30 | private CompilationUnit compilationUnit; 31 | 32 | public void updateMainActivity() throws Exception { 33 | findMainActivityPath(); 34 | if (mainActivityPath == null) { 35 | throw new FileNotFoundException("MainActivity.java not found"); 36 | } 37 | 38 | FileInputStream in = new FileInputStream(mainActivityPath); 39 | try { 40 | compilationUnit = JavaParser.parse(in); 41 | } finally { 42 | in.close(); 43 | } 44 | 45 | boolean importsChanged = updateImports(); 46 | boolean modulesChanged = updateModules(); 47 | 48 | if (importsChanged && modulesChanged) { 49 | InstallerUtil.writeToDisk(mainActivityPath, compilationUnit.toString()); 50 | System.out.println("Updated import and module statements in " + mainActivityPath); 51 | } else if (!importsChanged && !modulesChanged) { 52 | System.out.println("Import and module statements were not updated, is the class " + mainActivityPath + " updated already?"); 53 | } else { 54 | throw new Exception("Could not update import or module, update your code manually"); 55 | } 56 | } 57 | 58 | private boolean updateImports() { 59 | List imports = compilationUnit.getImports(); 60 | if (!hasImport(imports, TAB_LAYOUT_PACKAGE_CLASS)) { 61 | imports.add(new ImportDeclaration(new NameExpr(TAB_LAYOUT_PACKAGE_CLASS), false, false)); 62 | return true; 63 | } 64 | return false; 65 | } 66 | 67 | private boolean updateModules() { 68 | List types = compilationUnit.getTypes(); 69 | for (TypeDeclaration type : types) { 70 | List members = type.getMembers(); 71 | for (BodyDeclaration member : members) { 72 | if (!(member instanceof MethodDeclaration)) { 73 | continue; 74 | } 75 | MethodDeclaration method = (MethodDeclaration) member; 76 | if (!GET_PACKAGES_METHOD.equals(method.getName())) { 77 | continue; 78 | } 79 | for (Statement stmt : method.getBody().getStmts()) { 80 | if (!(stmt instanceof ReturnStmt)) { 81 | continue; 82 | } 83 | Expression returnStmtExpr = ((ReturnStmt) stmt).getExpr(); 84 | if (!(returnStmtExpr instanceof MethodCallExpr)) { 85 | continue; 86 | } 87 | MethodCallExpr callExpr = (MethodCallExpr) returnStmtExpr; 88 | if (callExpr.getArgs() == null) { 89 | continue; 90 | } 91 | List moduleList = callExpr.getArgs(); 92 | if (hasModule(moduleList, TAB_LAYOUT_PACKAGE_NAME)) { 93 | continue; 94 | } 95 | moduleList.add(new ObjectCreationExpr(null, new ClassOrInterfaceType(TAB_LAYOUT_PACKAGE_NAME), null)); 96 | return true; 97 | } 98 | } 99 | } 100 | return false; 101 | } 102 | 103 | private void findMainActivityPath() throws IOException { 104 | Files.walkFileTree(Paths.get("android/app/src/main/java"), new SimpleFileVisitor() { 105 | @Override 106 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 107 | Path name = file.getFileName(); 108 | if (name != null && name.toString().equals("MainActivity.java")) { 109 | mainActivityPath = file.toAbsolutePath().toString(); 110 | return FileVisitResult.TERMINATE; 111 | } 112 | return FileVisitResult.CONTINUE; 113 | } 114 | }); 115 | } 116 | 117 | private boolean hasModule(List moduleList, String packageName) { 118 | for (Expression moduleExpr : moduleList) { 119 | if (moduleExpr instanceof ObjectCreationExpr) { 120 | ObjectCreationExpr objectCreationExpr = (ObjectCreationExpr) moduleExpr; 121 | if (packageName.equals(objectCreationExpr.getType().toString())) { 122 | return true; 123 | } 124 | } 125 | } 126 | return false; 127 | } 128 | 129 | private boolean hasImport(List imports, String importName) { 130 | for (ImportDeclaration i : imports) { 131 | if (importName.equals(i.getName().toString())) { 132 | return true; 133 | } 134 | } 135 | return false; 136 | } 137 | } 138 | --------------------------------------------------------------------------------