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