UNPKG

react-native-scrollable-tab-view-forked

Version:

forked react-native-scrollable-tab-view ,you can set line width

435 lines (389 loc) 12.7 kB
const React = require('react') const { Component } = React const { ViewPropTypes } = (ReactNative = require('react-native')) const createReactClass = require('create-react-class') const PropTypes = require('prop-types') const { Dimensions, View, Animated, ScrollView, Platform, StyleSheet, InteractionManager, } = ReactNative const ViewPagerAndroid = require('react-native-pager-view').default const TimerMixin = require('react-timer-mixin') const SceneComponent = require('./SceneComponent') const DefaultTabBar = require('./DefaultTabBar') const ScrollableTabBar = require('./ScrollableTabBar') const screenWidth = Dimensions.get('window').width const AnimatedViewPagerAndroid = Platform.OS === 'android' ? Animated.createAnimatedComponent(ViewPagerAndroid) : undefined const ScrollableTabView = createReactClass({ mixins: [TimerMixin], statics: { DefaultTabBar, ScrollableTabBar, }, scrollOnMountCalled: false, propTypes: { tabBarPosition: PropTypes.oneOf(['top', 'bottom', 'overlayTop', 'overlayBottom']), initialPage: PropTypes.number, page: PropTypes.number, onChangeTab: PropTypes.func, onScroll: PropTypes.func, renderTabBar: PropTypes.any, tabBarUnderlineStyle: ViewPropTypes.style, tabBarBackgroundColor: PropTypes.string, tabBarActiveTextColor: PropTypes.string, tabBarInactiveTextColor: PropTypes.string, tabBarTextStyle: PropTypes.object, style: ViewPropTypes.style, contentProps: PropTypes.object, scrollWithoutAnimation: PropTypes.bool, locked: PropTypes.bool, prerenderingSiblingsNumber: PropTypes.number, containerWidth: PropTypes.number, tabsWidth: PropTypes.number, // tabs的宽度,默认是ContainerWidth,可以自定义tabsWidth,不能修改containerWidth,修改了,ios的内容宽度就变了,android的宽度是整个屏幕宽度 }, getDefaultProps() { return { tabBarPosition: 'top', initialPage: 0, page: -1, onChangeTab: () => {}, onScroll: () => {}, contentProps: {}, scrollWithoutAnimation: false, locked: false, prerenderingSiblingsNumber: 0, containerWidth: screenWidth, } }, getInitialState() { let scrollValue let scrollXIOS let positionAndroid let offsetAndroid if (Platform.OS === 'ios') { scrollXIOS = new Animated.Value(this.props.initialPage * this.props.containerWidth) const containerWidthAnimatedValue = new Animated.Value(this.props.containerWidth) // Need to call __makeNative manually to avoid a native animated bug. See // https://github.com/facebook/react-native/pull/14435 containerWidthAnimatedValue.__makeNative() scrollValue = Animated.divide(scrollXIOS, containerWidthAnimatedValue) const callListeners = this._polyfillAnimatedValue(scrollValue) scrollXIOS.addListener(({ value }) => callListeners(value / this.props.containerWidth)) } else { positionAndroid = new Animated.Value(this.props.initialPage) offsetAndroid = new Animated.Value(0) scrollValue = Animated.add(positionAndroid, offsetAndroid) const callListeners = this._polyfillAnimatedValue(scrollValue) let positionAndroidValue = this.props.initialPage let offsetAndroidValue = 0 positionAndroid.addListener(({ value }) => { positionAndroidValue = value callListeners(positionAndroidValue + offsetAndroidValue) }) offsetAndroid.addListener(({ value }) => { offsetAndroidValue = value callListeners(positionAndroidValue + offsetAndroidValue) }) } return { currentPage: this.props.initialPage, scrollValue, scrollXIOS, positionAndroid, offsetAndroid, containerWidth: screenWidth, sceneKeys: this.newSceneKeys({ currentPage: this.props.initialPage }), } }, componentDidUpdate(prevProps) { if (this.props.children !== prevProps.children) { this.updateSceneKeys({ page: this.state.currentPage, children: this.props.children }) } if (this.props.page >= 0 && this.props.page !== this.state.currentPage) { this.goToPage(this.props.page) } }, componentWillUnmount() { if (Platform.OS === 'ios') { this.state.scrollXIOS.removeAllListeners() } else { this.state.positionAndroid.removeAllListeners() this.state.offsetAndroid.removeAllListeners() } }, goToPage(pageNumber) { if (Platform.OS === 'ios') { const offset = pageNumber * this.props.containerWidth if (this.scrollView) { this.scrollView .getNode() .scrollTo({ x: offset, y: 0, animated: !this.props.scrollWithoutAnimation }) } } else if (this.scrollView) { if (this.props.scrollWithoutAnimation) { this.scrollView.getNode().setPageWithoutAnimation(pageNumber) } else { this.scrollView.getNode().setPage(pageNumber) } } const currentPage = this.state.currentPage this.updateSceneKeys({ page: pageNumber, callback: this._onChangeTab.bind(this, currentPage, pageNumber), }) }, renderTabBar(props) { if (this.props.renderTabBar === false) { return null } else if (this.props.renderTabBar) { return React.cloneElement(this.props.renderTabBar(props), props) } return <DefaultTabBar {...props} /> }, updateSceneKeys({ page, children = this.props.children, callback = () => {} }) { const newKeys = this.newSceneKeys({ previousKeys: this.state.sceneKeys, currentPage: page, children, }) this.setState({ currentPage: page, sceneKeys: newKeys }, callback) }, newSceneKeys({ previousKeys = [], currentPage = 0, children = this.props.children }) { const newKeys = [] this._children(children).forEach((child, idx) => { const key = this._makeSceneKey(child, idx) if (this._keyExists(previousKeys, key) || this._shouldRenderSceneKey(idx, currentPage)) { newKeys.push(key) } }) return newKeys }, // Animated.add and Animated.divide do not currently support listeners so // we have to polyfill it here since a lot of code depends on being able // to add a listener to `scrollValue`. See https://github.com/facebook/react-native/pull/12620. _polyfillAnimatedValue(animatedValue) { const listeners = new Set() const addListener = (listener) => { listeners.add(listener) } const removeListener = (listener) => { listeners.delete(listener) } const removeAllListeners = () => { listeners.clear() } animatedValue.addListener = addListener animatedValue.removeListener = removeListener animatedValue.removeAllListeners = removeAllListeners return (value) => listeners.forEach((listener) => listener({ value })) }, _shouldRenderSceneKey(idx, currentPageKey) { const numOfSibling = this.props.prerenderingSiblingsNumber return idx < currentPageKey + numOfSibling + 1 && idx > currentPageKey - numOfSibling - 1 }, _keyExists(sceneKeys, key) { return sceneKeys.find((sceneKey) => key === sceneKey) }, _makeSceneKey(child, idx) { return `${child.props.tabLabel}_${idx}` }, renderScrollableContent() { if (Platform.OS === 'ios') { const scenes = this._composeScenes() return ( <Animated.ScrollView horizontal pagingEnabled automaticallyAdjustContentInsets={false} contentOffset={{ x: this.props.initialPage * this.props.containerWidth }} ref={(scrollView) => { this.scrollView = scrollView }} onScroll={Animated.event( [{ nativeEvent: { contentOffset: { x: this.state.scrollXIOS } } }], { useNativeDriver: true, listener: this._onScroll } )} onMomentumScrollBegin={this._onMomentumScrollBeginAndEnd} onMomentumScrollEnd={this._onMomentumScrollBeginAndEnd} scrollEventThrottle={16} scrollsToTop={false} showsHorizontalScrollIndicator={false} scrollEnabled={!this.props.locked} directionalLockEnabled alwaysBounceVertical={false} keyboardDismissMode='on-drag' {...this.props.contentProps} > {scenes} </Animated.ScrollView> ) } const scenes = this._composeScenes() return ( <AnimatedViewPagerAndroid key={this._children().length} style={styles.scrollableContentAndroid} initialPage={this.props.initialPage} onPageSelected={this._updateSelectedPage} keyboardDismissMode='on-drag' scrollEnabled={!this.props.locked} onPageScroll={Animated.event( [ { nativeEvent: { position: this.state.positionAndroid, offset: this.state.offsetAndroid, }, }, ], { useNativeDriver: true, listener: this._onScroll, } )} ref={(scrollView) => { this.scrollView = scrollView }} {...this.props.contentProps} > {scenes} </AnimatedViewPagerAndroid> ) }, _composeScenes() { return this._children().map((child, idx) => { const key = this._makeSceneKey(child, idx) return ( <SceneComponent key={child.key} shouldUpdated={this._shouldRenderSceneKey(idx, this.state.currentPage)} style={{ width: this.props.containerWidth }} > {this._keyExists(this.state.sceneKeys, key) ? ( child ) : ( <View tabLabel={child.props.tabLabel} /> )} </SceneComponent> ) }) }, _onMomentumScrollBeginAndEnd(e) { const offsetX = e.nativeEvent.contentOffset.x const page = Math.round(offsetX / this.props.containerWidth) if (this.state.currentPage !== page) { this._updateSelectedPage(page) } }, _updateSelectedPage(nextPage) { let localNextPage = nextPage if (typeof localNextPage === 'object') { localNextPage = nextPage.nativeEvent.position } const currentPage = this.state.currentPage this.updateSceneKeys({ page: localNextPage, callback: this._onChangeTab.bind(this, currentPage, localNextPage), }) }, _onChangeTab(prevPage, currentPage) { this.props.onChangeTab({ i: currentPage, ref: this._children()[currentPage], from: prevPage, }) }, _onScroll(e) { if (Platform.OS === 'ios') { const offsetX = e.nativeEvent.contentOffset.x if (offsetX === 0 && !this.scrollOnMountCalled) { this.scrollOnMountCalled = true } else { this.props.onScroll(offsetX / this.props.containerWidth) } } else { const { position, offset } = e.nativeEvent this.props.onScroll(position + offset) } }, _handleLayout(e) { const { width } = e.nativeEvent.layout if (!width || width <= 0 || Math.round(width) === Math.round(this.state.containerWidth)) { return } if (Platform.OS === 'ios') { const containerWidthAnimatedValue = new Animated.Value(width) // Need to call __makeNative manually to avoid a native animated bug. See // https://github.com/facebook/react-native/pull/14435 containerWidthAnimatedValue.__makeNative() scrollValue = Animated.divide(this.state.scrollXIOS, containerWidthAnimatedValue) this.setState({ containerWidth: width, scrollValue }) } else { this.setState({ containerWidth: width }) } this.requestAnimationFrame(() => { this.goToPage(this.state.currentPage) }) }, _children(children = this.props.children) { return React.Children.map(children, (child) => child) }, render() { const overlayTabs = this.props.tabBarPosition === 'overlayTop' || this.props.tabBarPosition === 'overlayBottom' const tabBarProps = { goToPage: this.goToPage, tabs: this._children().map((child) => child.props), activeTab: this.state.currentPage, scrollValue: this.state.scrollValue, // containerWidth: this.props.containerWidth, containerWidth: this.props.tabsWidth || this.props.containerWidth, initialPage: this.props.initialPage, } if (this.props.tabBarBackgroundColor) { tabBarProps.backgroundColor = this.props.tabBarBackgroundColor } if (this.props.tabBarActiveTextColor) { tabBarProps.activeTextColor = this.props.tabBarActiveTextColor } if (this.props.tabBarInactiveTextColor) { tabBarProps.inactiveTextColor = this.props.tabBarInactiveTextColor } if (this.props.tabBarTextStyle) { tabBarProps.textStyle = this.props.tabBarTextStyle } if (this.props.tabBarUnderlineStyle) { tabBarProps.underlineStyle = this.props.tabBarUnderlineStyle } if (overlayTabs) { tabBarProps.style = { position: 'absolute', left: 0, right: 0, [this.props.tabBarPosition === 'overlayTop' ? 'top' : 'bottom']: 0, } } return ( <View style={[styles.container, this.props.style]} onLayout={this._handleLayout}> {this.props.tabBarPosition === 'top' && this.renderTabBar(tabBarProps)} {this.renderScrollableContent()} {(this.props.tabBarPosition === 'bottom' || overlayTabs) && this.renderTabBar(tabBarProps)} </View> ) }, }) module.exports = ScrollableTabView const styles = StyleSheet.create({ container: { flex: 1, }, scrollableContentAndroid: { flex: 1, }, })