import React from 'react'; import {Animated, View, Dimensions, LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, Platform, ScrollView, Text} from 'react-native'; import {WaitLoad} from '..'; import {WithTheme, WithThemeStyles, $Theme} from '../style'; import {DefaultTabBar} from './DefaultTabBar'; import {PropsType, TabData} from './PropsType'; import TabsStyles, {TabsStyle} from './style/tabs'; export interface StateType { currentTab: number; scrollX: Animated.Value; scrollValue: Animated.Value; containerWidth: number; } let instanceId: number = 0; export interface TabsProps extends PropsType, WithThemeStyles {} export class tabs extends React.PureComponent { static defaultProps: PropsType = { tabBarPosition: 'top', initialPage: 0, swipeable: true, animated: true, prerenderingSiblingsNumber: 1, tabs: [], destroyInactiveTab: false, usePaged: true, tabDirection: 'horizontal', distanceToChangeTab: 0.3, style: {}, shadowUnderLine: true, }; static DefaultTabBar = DefaultTabBar; scrollView: {_component: ScrollView} | undefined; //viewPager: any; protected instanceId: number; protected prevCurrentTab: number | undefined; protected tabCache: {[index: number]: React.ReactNode} = {}; /** compatible for different between react and preact in `setState`. */ private nextCurrentTab: number; constructor(props: PropsType) { super(props); const width = Dimensions.get('window').width; const pageIndex = this.getTabIndex(props); this.state = { currentTab: pageIndex, scrollX: new Animated.Value(pageIndex * width), scrollValue: new Animated.Value(pageIndex), containerWidth: width, }; this.nextCurrentTab = this.state.currentTab; this.instanceId = instanceId++; } componentDidMount() { this.prevCurrentTab = this.state.currentTab; this.state.scrollX.addListener(({value}) => { const scrollValue = value / this.state.containerWidth; this.state.scrollValue.setValue(scrollValue); }); } setScrollView = (sv: any) => { this.scrollView = sv; this.scrollTo(this.state.currentTab); }; renderContent = (getSubElements = this.getSubElements()) => { const {tabs, usePaged, destroyInactiveTab, keyboardShouldPersistTaps} = this.props; const {currentTab = 0, containerWidth = 0} = this.state; const content = tabs.map((tab, index) => { const key = tab.key || `tab_${index}`; // update tab cache if (this.shouldRenderTab(index)) { this.tabCache[index] = this.getSubElement(tab, index, getSubElements); } else if (destroyInactiveTab) { this.tabCache[index] = undefined; } return ( {this.tabCache[index]} ); }); if (Platform.OS === 'android') { //const _ViewPagerAndroid = React.lazy(() => import('@react-native-community/viewpager')); //const ViewPagerAndroid = WaitLoad(_ViewPagerAndroid); //return ( // { //this.state.scrollX.setValue(e.nativeEvent.position * this.state.containerWidth); //}} //style={{flex: 1}} //onPageSelected={(e: any) => { //const index = e.nativeEvent.position; //this.setState( //{ //currentTab: index, //}, //() => { //// tslint:disable-next-line:no-unused-expression //this.props.onChange && this.props.onChange(tabs[index], index); //}, //); //this.nextCurrentTab = index; //}} //ref={(ref: any) => (this.viewPager = ref)}> //{content} // //); } return ( {content} ); }; onMomentumScrollEnd = (e: NativeSyntheticEvent) => { const offsetX = e.nativeEvent.contentOffset.x; const page = this.getOffsetIndex(offsetX, this.state.containerWidth); if (this.state.currentTab !== page) { this.goToTab(page); } }; handleLayout = (e: LayoutChangeEvent) => { const {width} = e.nativeEvent.layout; requestAnimationFrame(() => { this.scrollTo(this.state.currentTab, false); }); if (Math.round(width) !== Math.round(this.state.containerWidth)) { this.setState({containerWidth: width}); } }; scrollTo = (index: number, animated = true) => { if (Platform.OS === 'android') { //if (this.viewPager) { //if (animated) { //this.viewPager.setPage(index); //} else { //this.viewPager.setPageWithoutAnimation(index); //} //return; //} } const {containerWidth} = this.state; if (containerWidth) { const offset = index * containerWidth; if (this.scrollView && this.scrollView._component) { const {scrollTo} = this.scrollView._component; // tslint:disable-next-line:no-unused-expression scrollTo && scrollTo.call(this.scrollView._component, {x: offset, animated}); } } }; render() { const {tabBarPosition, noRenderContent, keyboardShouldPersistTaps, shadowUnderLine} = this.props; const {scrollX, scrollValue, containerWidth} = this.state; // let overlayTabs = (this.props.tabBarPosition === 'overlayTop' || this.props.tabBarPosition === 'overlayBottom'); const overlayTabs = false; const tabBarProps = { ...this.getTabBarBaseProps(), keyboardShouldPersistTaps, scrollX: scrollX, scrollValue: scrollValue, containerWidth: containerWidth, }; if (overlayTabs) { // tabBarProps.style = { // position: 'absolute', // left: 0, // right: 0, // [this.props.tabBarPosition === 'overlayTop' ? 'top' : 'bottom']: 0, // }; } return ( {(styles) => { const content = [ {this.renderTabBar(tabBarProps, DefaultTabBar as any)} , !noRenderContent && this.renderContent(), ]; return ( {tabBarPosition === 'top' ? content : content.reverse()} ); }} ); } getTabIndex(props: PropsType) { const {page, initialPage, tabs} = props; const param = (page !== undefined ? page : initialPage) || 0; let index = 0; if (typeof (param as any) === 'string') { tabs.forEach((t, i) => { if (t.key === param) { index = i; } }); } else { index = (param as number) || 0; } return index < 0 ? 0 : index; } isTabVertical = (direction = this.props.tabDirection) => direction === 'vertical'; shouldRenderTab = (idx: number) => { const {prerenderingSiblingsNumber = 0} = this.props; const {currentTab = 0} = this.state; return currentTab - prerenderingSiblingsNumber <= idx && idx <= currentTab + prerenderingSiblingsNumber; }; UNSAFE_componentWillReceiveProps(nextProps: PropsType) { if (this.props.page !== nextProps.page && nextProps.page !== undefined) { this.goToTab(this.getTabIndex(nextProps), true); } } componentDidUpdate() { this.prevCurrentTab = this.state.currentTab; } getOffsetIndex = (current: number, width: number, threshold = this.props.distanceToChangeTab || 0) => { const ratio = Math.abs(current / width); const direction = ratio > this.state.currentTab ? '<' : '>'; const index = Math.floor(ratio); switch (direction) { case '<': return ratio - index > threshold ? index + 1 : index; case '>': return 1 - ratio + index > threshold ? index : index + 1; default: return Math.round(ratio); } }; goToTab(index: number, force = false, newState: any = {}) { if (!force && this.nextCurrentTab === index) { return false; } this.nextCurrentTab = index; const {tabs, onChange} = this.props as PropsType; if (index >= 0 && index < tabs.length) { if (!force) { // tslint:disable-next-line:no-unused-expression onChange && onChange(tabs[index], index); } this.setState( { currentTab: index, ...newState, }, () => { requestAnimationFrame(() => { this.scrollTo(this.state.currentTab, this.props.animated); }); }, ); } return true; } tabClickGoToTab(index: number) { this.goToTab(index); } getTabBarBaseProps() { const {currentTab} = this.state; const { animated, onTabClick, tabBarActiveTextColor, tabBarBackgroundColor, tabBarInactiveTextColor, tabBarPosition, tabBarTextStyle, tabBarUnderlineStyle, renderTab, renderUnderline, tabs, } = this.props; return { activeTab: currentTab, animated: !!animated, goToTab: this.tabClickGoToTab.bind(this), onTabClick, tabBarActiveTextColor, tabBarBackgroundColor, tabBarInactiveTextColor, tabBarPosition, tabBarTextStyle, tabBarUnderlineStyle, renderTab, renderUnderline, tabs, instanceId: this.instanceId, }; } // tslint:disable-next-line:no-shadowed-variable renderTabBar(tabBarProps: any, DefaultTabBar: React.ComponentClass) { const {renderTabBar} = this.props; if (renderTabBar === false) { return null; } else if (renderTabBar) { return renderTabBar(tabBarProps); } else { return ; } } getSubElements = () => { const {children} = this.props; const subElements: {[key: string]: React.ReactNode} = {}; return (defaultPrefix: string = '$i$-', allPrefix: string = '$ALL$') => { if (Array.isArray(children)) { children.forEach((child: any, index) => { if (child.key) { subElements[child.key] = child; } subElements[`${defaultPrefix}${index}`] = child; }); } else if (children) { subElements[allPrefix] = children; } return subElements; }; }; getSubElement( tab: TabData, index: number, subElements: (defaultPrefix: string, allPrefix: string) => {[key: string]: any}, defaultPrefix: string = '$i$-', allPrefix: string = '$ALL$', ) { const key = tab.key || `${defaultPrefix}${index}`; const elements = subElements(defaultPrefix, allPrefix); let component = elements[key] || elements[allPrefix]; if (component instanceof Function) { component = component(tab, index); } return component || null; } }