├── .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 [![npm version](https://badge.fury.io/js/react-native-scrollable-tab-view-mask-bar.svg)](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 | --------------------------------------------------------------------------------