├── .gitignore
├── .DS_Store
├── src
├── mask_left_dark@2x.png
├── mask_left_dark@3x.png
├── mask_left_light@2x.png
├── mask_left_light@3x.png
├── mask_left_xlight@2x.png
├── mask_left_xlight@3x.png
├── mask_right_dark@2x.png
├── mask_right_dark@3x.png
├── mask_right_light@2x.png
├── mask_right_light@3x.png
├── mask_right_xlight@2x.png
└── mask_right_xlight@3x.png
├── demo_images
└── scrollable_example.gif
├── index.js
├── Button.ios.js
├── Button.android.js
├── package.json
├── README.md
└── ScrollableTabBar.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .project
2 | npm-debug.log
3 | node_modules/
4 | .idea/
5 | .reploy
6 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/.DS_Store
--------------------------------------------------------------------------------
/src/mask_left_dark@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_left_dark@2x.png
--------------------------------------------------------------------------------
/src/mask_left_dark@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_left_dark@3x.png
--------------------------------------------------------------------------------
/src/mask_left_light@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_left_light@2x.png
--------------------------------------------------------------------------------
/src/mask_left_light@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_left_light@3x.png
--------------------------------------------------------------------------------
/src/mask_left_xlight@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_left_xlight@2x.png
--------------------------------------------------------------------------------
/src/mask_left_xlight@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_left_xlight@3x.png
--------------------------------------------------------------------------------
/src/mask_right_dark@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_right_dark@2x.png
--------------------------------------------------------------------------------
/src/mask_right_dark@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_right_dark@3x.png
--------------------------------------------------------------------------------
/src/mask_right_light@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_right_light@2x.png
--------------------------------------------------------------------------------
/src/mask_right_light@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_right_light@3x.png
--------------------------------------------------------------------------------
/src/mask_right_xlight@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_right_xlight@2x.png
--------------------------------------------------------------------------------
/src/mask_right_xlight@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/src/mask_right_xlight@3x.png
--------------------------------------------------------------------------------
/demo_images/scrollable_example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/HEAD/demo_images/scrollable_example.gif
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by whb on 2017/1/5.
3 | * https://github.com/WaterEye0o
4 | */
5 |
6 | const ScrollableTabView = require('./ScrollableTabBar')
7 |
8 | module.exports = ScrollableTabView
--------------------------------------------------------------------------------
/Button.ios.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const ReactNative = require('react-native');
3 | const {
4 | TouchableOpacity,
5 | View,
6 | } = ReactNative;
7 |
8 | const Button = (props) => {
9 | return
10 | {props.children}
11 | ;
12 | };
13 |
14 | module.exports = Button;
15 |
--------------------------------------------------------------------------------
/Button.android.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const ReactNative = require('react-native');
3 | const {
4 | TouchableNativeFeedback,
5 | View,
6 | } = ReactNative;
7 |
8 | const Button = (props) => {
9 | return
14 | {props.children}
15 | ;
16 | };
17 |
18 | module.exports = Button;
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-scrollable-tab-view-mask-bar",
3 | "version": "1.0.8",
4 | "description": "this is a custom tab bar for react-native-scrollable-tab-view",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar.git"
12 | },
13 | "keywords": [
14 | "react",
15 | "react-native",
16 | "scroll",
17 | "react-native-scrollable-tab-view"
18 | ],
19 | "author": "WateryEye",
20 | "license": "ISC",
21 | "bugs": {
22 | "url": "https://github.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar/issues"
23 | },
24 | "dependencies": {
25 | "react-timer-mixin": "^0.13.3",
26 | "prop-types": "^15.6.0",
27 | "create-react-class": "^15.6.2"
28 | },
29 | "homepage": "https://github.com/WaterEye0o/react-native-scrollable-tab-view-mask-bar#readme"
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-scrollable-tab-view-mask-bar [](https://badge.fury.io/js/react-native-scrollable-tab-view-mask-bar)
2 |
3 | this component is a custom component of the react-native-scrollable-tab-view repository ,so I suggest you use this component and the combination of react-native-scrollable-tab-view.
4 |
5 | # Install
6 |
7 | 1. Run `npm install react-native-scrollable-tab-view-mask-bar --save`
8 | 2. Run `npm install react-native-scrollable-tab-view --save`
9 |
10 | # Usage
11 |
12 | ```
13 | var ScrollableTabView = require('react-native-scrollable-tab-view');
14 | var MaskTabBar = require('react-native-scrollable-tab-view-mask-bar');
15 |
16 | var App = React.createClass({
17 | render() {
18 | return (
19 | }>
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | });
27 | ```
28 |
29 | # Demo
30 |
31 |
32 |
--------------------------------------------------------------------------------
/ScrollableTabBar.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const ReactNative = require('react-native');
3 | const createReactClass = require('create-react-class');
4 | const {
5 | View,
6 | Animated,
7 | StyleSheet,
8 | ScrollView,
9 | Text,
10 | Platform,
11 | Dimensions,
12 | Image,
13 | ViewPropTypes
14 | } = ReactNative;
15 | const Button = require('./Button');
16 | const PropTypes = require('prop-types');
17 |
18 | const WINDOW_WIDTH = Dimensions.get('window').width;
19 | const MASK_WIDTH = 60;
20 |
21 | const MASK_IMG = {
22 | LEFT: {
23 | LIGHT: require('./src/mask_left_light.png'),
24 | DARK: require('./src/mask_left_dark.png'),
25 | X_LIGHT: require('./src/mask_left_xlight.png')
26 | },
27 | RIGHT: {
28 | LIGHT: require('./src/mask_right_light.png'),
29 | DARK: require('./src/mask_right_dark.png'),
30 | X_LIGHT: require('./src/mask_right_xlight.png')
31 | }
32 | }
33 |
34 | const ScrollableTabBar = createReactClass({
35 | propTypes: {
36 | goToPage: PropTypes.func,
37 | activeTab: PropTypes.number,
38 | tabs: PropTypes.array,
39 | backgroundColor: PropTypes.string,
40 | activeTextColor: PropTypes.string,
41 | inactiveTextColor: PropTypes.string,
42 | scrollOffset: PropTypes.number,
43 | style: ViewPropTypes.style,
44 | tabStyle: ViewPropTypes.style,
45 | tabsContainerStyle: ViewPropTypes.style,
46 | textStyle: Text.propTypes.style,
47 | renderTab: PropTypes.func,
48 | underlineStyle: ViewPropTypes.style,
49 | underlineContainerStyle: ViewPropTypes.style,
50 | onScroll: PropTypes.func,
51 | showMask: PropTypes.bool,
52 | maskMode: PropTypes.oneOf(['light', 'dark','x-light'])
53 | },
54 |
55 | getDefaultProps() {
56 | return {
57 | scrollOffset: 52,
58 | activeTextColor: 'navy',
59 | inactiveTextColor: 'black',
60 | backgroundColor: null,
61 | style: {},
62 | tabStyle: {},
63 | tabsContainerStyle: {},
64 | underlineStyle: {},
65 | underlineContainerStyle: {},
66 | showMask: false,
67 | maskMode: 'x-light'
68 | };
69 | },
70 |
71 | getInitialState() {
72 | this._tabsMeasurements = [];
73 | switch (this.props.maskMode) {
74 | case 'light':
75 | this.maskImageSrc = {left: MASK_IMG.LEFT.LIGHT, right: MASK_IMG.RIGHT.LIGHT};
76 | break;
77 | case 'dark':
78 | this.maskImageSrc = {left: MASK_IMG.LEFT.DARK, right: MASK_IMG.RIGHT.DARK};
79 | break;
80 | case 'x-light':
81 | this.maskImageSrc = {left: MASK_IMG.LEFT.X_LIGHT, right: MASK_IMG.RIGHT.X_LIGHT};
82 | break;
83 | default:
84 | this.maskImageSrc = {left: MASK_IMG.LEFT.X_LIGHT, right: MASK_IMG.RIGHT.X_LIGHT};
85 | }
86 | return {
87 | _leftTabUnderline: new Animated.Value(0),
88 | _widthTabUnderline: new Animated.Value(0),
89 | _containerWidth: null,
90 | _showLeftMask: false,
91 | _showRightMask: false
92 | };
93 | },
94 |
95 | componentDidMount() {
96 | this.props.scrollValue.addListener(this.updateView);
97 | },
98 |
99 | updateView(offset) {
100 | const position = Math.floor(offset.value);
101 | const pageOffset = offset.value % 1;
102 | const tabCount = this.props.tabs.length;
103 | const lastTabPosition = tabCount - 1;
104 |
105 | if (tabCount === 0 || offset.value < 0 || offset.value > lastTabPosition) {
106 | return;
107 | }
108 |
109 | if (this.necessarilyMeasurementsCompleted(position, position === lastTabPosition)) {
110 | this.updateTabPanel(position, pageOffset);
111 | this.updateTabUnderline(position, pageOffset, tabCount);
112 | }
113 | },
114 |
115 | necessarilyMeasurementsCompleted(position, isLastTab) {
116 | return this._tabsMeasurements[position] &&
117 | (isLastTab || this._tabsMeasurements[position + 1]) &&
118 | this._tabContainerMeasurements &&
119 | this._containerMeasurements;
120 | },
121 |
122 | updateTabPanel(position, pageOffset) {
123 | const containerWidth = this._containerMeasurements.width;
124 | const tabWidth = this._tabsMeasurements[position].width;
125 | const nextTabMeasurements = this._tabsMeasurements[position + 1];
126 | const nextTabWidth = nextTabMeasurements && nextTabMeasurements.width || 0;
127 | const tabOffset = this._tabsMeasurements[position].left;
128 | const absolutePageOffset = pageOffset * tabWidth;
129 | let newScrollX = tabOffset + absolutePageOffset;
130 |
131 | // center tab and smooth tab change (for when tabWidth changes a lot between two tabs)
132 | newScrollX -= (containerWidth - (1 - pageOffset) * tabWidth - pageOffset * nextTabWidth) / 2;
133 | newScrollX = newScrollX >= 0 ? newScrollX : 0;
134 |
135 | if (Platform.OS === 'android') {
136 | this._scrollView.scrollTo({x: newScrollX, y: 0, animated: false,});
137 | } else {
138 | const rightBoundScroll = this._tabContainerMeasurements.width - (this._containerMeasurements.width);
139 | newScrollX = newScrollX > rightBoundScroll ? rightBoundScroll : newScrollX;
140 | this._scrollView.scrollTo({x: newScrollX, y: 0, animated: false,});
141 | }
142 |
143 | },
144 |
145 | updateTabUnderline(position, pageOffset, tabCount) {
146 | const lineLeft = this._tabsMeasurements[position].left;
147 | const lineRight = this._tabsMeasurements[position].right;
148 |
149 | if (position < tabCount - 1) {
150 | const nextTabLeft = this._tabsMeasurements[position + 1].left;
151 | const nextTabRight = this._tabsMeasurements[position + 1].right;
152 |
153 | const newLineLeft = (pageOffset * nextTabLeft + (1 - pageOffset) * lineLeft);
154 | const newLineRight = (pageOffset * nextTabRight + (1 - pageOffset) * lineRight);
155 |
156 | this.state._leftTabUnderline.setValue(newLineLeft);
157 | this.state._widthTabUnderline.setValue(newLineRight - newLineLeft);
158 | } else {
159 | this.state._leftTabUnderline.setValue(lineLeft);
160 | this.state._widthTabUnderline.setValue(lineRight - lineLeft);
161 | }
162 | },
163 |
164 | renderTab(name, page, isTabActive, onPressHandler, onLayoutHandler) {
165 | const {activeTextColor, inactiveTextColor, textStyle,} = this.props;
166 | const textColor = isTabActive ? activeTextColor : inactiveTextColor;
167 | const fontWeight = isTabActive ? 'bold' : 'normal';
168 |
169 | return ;
183 | },
184 |
185 | measureTab(page, event) {
186 | const {x, width, height,} = event.nativeEvent.layout;
187 | this._tabsMeasurements[page] = {left: x, right: x + width, width, height,};
188 | this.updateView({value: this.props.scrollValue._a._value,});
189 | },
190 |
191 | renderLeftMask(){
192 | return (
193 |
197 |
198 |
199 | )
200 | },
201 |
202 | renderRightMask(){
203 |
204 | return (
205 |
209 |
210 |
211 | )
212 | },
213 |
214 | showLeftMask(disable){
215 | if (disable !== this.state._showLeftMask) this.setState({_showLeftMask: disable});
216 | },
217 |
218 | showRightMask(disable){
219 | if (disable !== this.state._showRightMask) this.setState({_showRightMask: disable});
220 | },
221 |
222 | onScroll({nativeEvent:{contentOffset:{x}}}){
223 | this.props.onScroll && this.props.onScroll(...arguments)
224 |
225 | if (x >= MASK_WIDTH && !this.state._showLeftMask) {
226 | this.showLeftMask(true)
227 | } else if (x <= MASK_WIDTH && this.state._showLeftMask) {
228 | this.showLeftMask(false)
229 | }
230 |
231 | if (x >= this._tabContainerMeasurements.width - MASK_WIDTH - WINDOW_WIDTH && this.state._showRightMask) {
232 | this.showRightMask(false)
233 | } else if (x <= this._tabContainerMeasurements.width - MASK_WIDTH - WINDOW_WIDTH && !this.state._showRightMask) {
234 | this.showRightMask(true)
235 | }
236 | },
237 |
238 | render() {
239 | const tabUnderlineStyle = {
240 | position: 'absolute',
241 | height: 4,
242 | backgroundColor: 'navy',
243 | bottom: 0,
244 | };
245 |
246 | const dynamicTabUnderline = {
247 | left: this.state._leftTabUnderline,
248 | width: this.state._widthTabUnderline,
249 | };
250 |
251 | return (
252 |
253 |
257 |
258 | { this._scrollView = scrollView; }}
261 | horizontal={true}
262 | showsHorizontalScrollIndicator={false}
263 | showsVerticalScrollIndicator={false}
264 | directionalLockEnabled={true}
265 | onScroll={this.onScroll}
266 | bounces={false}
267 | scrollsToTop={false}
268 | >
269 |
274 | {this.props.tabs.map((name, page) => {
275 | const isTabActive = this.props.activeTab === page;
276 | const renderTab = this.props.renderTab || this.renderTab;
277 | return renderTab(name, page, isTabActive, this.props.goToPage, this.measureTab.bind(this, page));
278 | })}
279 |
280 |
281 |
282 |
283 |
284 | {this.props.showMask && this.renderLeftMask()}
285 | {this.props.showMask && this.renderRightMask()}
286 |
287 | );
288 | },
289 |
290 | componentWillReceiveProps(nextProps) {
291 | // If the tabs change, force the width of the tabs container to be recalculated
292 | if (JSON.stringify(this.props.tabs) !== JSON.stringify(nextProps.tabs) && this.state._containerWidth) {
293 | this.setState({_containerWidth: null,});
294 | }
295 | },
296 |
297 | onTabContainerLayout(e) {
298 | this._tabContainerMeasurements = e.nativeEvent.layout;
299 | let width = this._tabContainerMeasurements.width;
300 | if (width < WINDOW_WIDTH) {
301 | width = WINDOW_WIDTH;
302 | } else {
303 | this.setState({_showRightMask: true});
304 | }
305 | this.setState({_containerWidth: width,});
306 | this.updateView({value: this.props.scrollValue._a._value,});
307 | },
308 |
309 | onContainerLayout(e) {
310 | this._containerMeasurements = e.nativeEvent.layout;
311 | this.updateView({value: this.props.scrollValue._a._value,});
312 | },
313 | });
314 |
315 | module.exports = ScrollableTabBar;
316 | const styles = StyleSheet.create({
317 | tab: {
318 | height: 49,
319 | alignItems: 'center',
320 | justifyContent: 'center',
321 | paddingLeft: 20,
322 | paddingRight: 20,
323 | },
324 | container: {
325 | height: 50,
326 | borderWidth: 1,
327 | borderTopWidth: 0,
328 | borderLeftWidth: 0,
329 | borderRightWidth: 0,
330 | borderColor: '#ccc',
331 | },
332 | tabs: {
333 | flexDirection: 'row',
334 | justifyContent: 'space-around',
335 | },
336 | maskImg: {
337 | position: 'absolute',
338 | top: 0,
339 | bottom: 0,
340 | }
341 | });
342 |
343 |
--------------------------------------------------------------------------------