UNPKG

react-native-underline-tabbar

Version:

Custom Tabbar for https://github.com/skv-headless/react-native-scrollable-tab-view

405 lines (375 loc) 12 kB
/** * Created by Konstantin Yakushin. * react-native-underline-tabbar */ // @flow import React, { Component } from 'react'; import { Text, TouchableOpacity, View, Animated, ScrollView } from 'react-native'; import MatrixMath from 'react-native/Libraries/Utilities/MatrixMath'; import styles from './Styles'; function transformOrigin(matrix, origin) { const { x, y, z } = origin; const translate = MatrixMath.createIdentityMatrix(); MatrixMath.reuseTranslate3dCommand(translate, x, y, z); MatrixMath.multiplyInto(matrix, translate, matrix); const untranslate = MatrixMath.createIdentityMatrix(); MatrixMath.reuseTranslate3dCommand(untranslate, -x, -y, -z); MatrixMath.multiplyInto(matrix, matrix, untranslate); } function createTranslateXScaleX(scaleXFactor, x) { // prettier-ignore return [ scaleXFactor, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, 0, 0, 1, ]; } export type TabType = { label: string, badge: string, badgeColor?: string, [string]: any, }; export type Props = { goToPage: Function, activeTab: number, tabs: TabType[], underlineColor: string, underlineHeight: number, underlineBottomPosition: number, backgroundColor: string, activeTextColor: string, inactiveTextColor: string, tabBadgeColor: string, scrollValue: Animated.Value, scrollContainerStyle: Object, tabStyles: { tab?: Object, badgeBubble?: Object, badgeText?: Object, }, tabMargin: number, style: Object, activeTabTextStyle: Object, tabBarTextStyle: Object, tabBarStyle: Object, renderTab: ( tab: TabType, page: number, isTabActive: boolean, onPressHandler: Function, onTabLayout: Function, ) => any, }; type State = { renderUnderline: boolean, tabScrollValue: number, }; type LayoutType = { x: number, y: number, width: number, height: number, }; type TabStateType = { [string | number]: LayoutType, }; class TabBar extends Component<Props, State> { static defaultProps = { tabMargin: 20, tabBarTextStyle: {}, tabStyles: { tab: {}, badgeBubble: {}, badgeText: {}, }, scrollContainerStyle: {}, style: {}, underlineColor: 'navy', underlineHeight: 2, underlineBottomPosition: 0, }; initialSetupWasDone: boolean = false; currentContentOffset: { y: number, x: number } = { x: 0, y: 0 }; tabState: TabStateType = {}; tabContainerLayout: LayoutType = { width: 0, height: 0, x: 0, y: 0, }; scrollContainerLayout: LayoutType = { width: 0, height: 0, x: 0, y: 0, }; underlineRef: ?any = null; scrollView: ?any = null; _animateListenerId: ?string = null; offsetCollection: ?Object = null; widthCollection: ?Object = null; scrollOffsetsCollection: ?Object = null; state = { renderUnderline: false, tabScrollValue: 0, }; componentDidMount() { this._animateListenerId = this.props.scrollValue.addListener(this.handleScrolling); } shouldComponentUpdate(nextProps: Props, nextState: State) { const serializedState = JSON.stringify(this.props) + JSON.stringify(this.state); const serializedNextState = JSON.stringify(nextProps) + JSON.stringify(nextState); return serializedState !== serializedNextState; } componentWillUnmount() { if (this._animateListenerId) { this.props.scrollValue.removeListener(this._animateListenerId); } } measureTabsContainer = (event: Object) => { this.tabContainerLayout = event.nativeEvent.layout; this.checkMeasures(); }; onScrollContentSizeChange = (width: number, height: number) => { this.scrollContainerLayout = { width, height, x: 0, y: 0 }; this.checkMeasures(); }; setUnderlineRef = (ref: any) => { this.underlineRef = ref; }; handleScrolling = (event: { value: number }) => { const { value } = event; const dx = (this.offsetCollection && this.offsetCollection._interpolation(value)) || 0; const scaleX = (this.widthCollection && this.widthCollection._interpolation(value)) || 0; this.applyTransformToUnderline(scaleX, dx); if (this.scrollOffsetsCollection) { const scrollOffset = this.scrollOffsetsCollection && this.scrollOffsetsCollection._interpolation(value); if (this.scrollView) { this.scrollView.scrollTo({ x: scrollOffset, animated: false }); } } }; applyTransformToUnderline(scaleXFactor: number, dx: number) { const { underlineRef } = this; if (!underlineRef) return; const matrix = createTranslateXScaleX(scaleXFactor, dx); transformOrigin(matrix, { x: -0.5, y: 0, z: 0 }); underlineRef.setNativeProps({ style: { transform: [ { matrix, }, ], }, }); } onTabLayout(event: Object, page: number) { const { x, y, width, height } = event.nativeEvent.layout; this.tabState[page] = { x, y, width, height }; if (this.state.renderUnderline) { this.calculateInterpolations(); // The next lines may look awful and it will be true // But they are needed to update underline without glitches // FIXME: Find another way to rerender update underline without glitches this.setState({ renderUnderline: false }, () => { this.setState({ renderUnderline: true }, () => { this.handleScrolling({ value: this.props.scrollValue.__getValue() }); }); }); return; } if (this.props.tabs.length === Object.keys(this.tabState).length) { this.setState({ renderUnderline: true }, this.checkMeasures); } } checkMeasures = () => { if ( this.state.renderUnderline && this.tabContainerLayout.width !== 0 && this.scrollContainerLayout.width !== 0 ) { this.calculateInterpolations(); if (!this.initialSetupWasDone) { const { activeTab } = this.props; this.handleScrolling({ value: activeTab }); this.initialSetupWasDone = true; } } }; calculateInterpolations = () => { const inputRange = Object.keys(this.tabState).map(Number); const outputRangeLeft = []; const outputRangeWidth = []; const tabContainerWidth = this.tabContainerLayout.width; const scrollWidth = this.scrollContainerLayout.width; const marginValue = this.props.tabMargin; for (let i = 0, len = inputRange.length; i < len; i += 1) { const key = inputRange[i]; outputRangeLeft.push(this.tabState[key].x); outputRangeWidth.push(this.tabState[key].width); } // Serve the case when we have only one tab // Also it doesn't make sense to handle case when we don't have tabs at all // because ScrollableTabView wouldn't work if (inputRange.length < 2) { inputRange.push(1); outputRangeLeft.push(0); outputRangeWidth.push(0); } // $FlowFixMe this.offsetCollection = this.props.scrollValue.interpolate({ inputRange, // $FlowFixMe outputRange: outputRangeLeft, // $FlowFixMe }); // $FlowFixMe this.widthCollection = this.props.scrollValue.interpolate({ inputRange, // $FlowFixMe outputRange: outputRangeWidth, // $FlowFixMe }); const outputRangeScroll = [0]; for (let i = 1, len = inputRange.length; i < len; i += 1) { const isLast = i === len - 1; const offset = outputRangeLeft[i]; const tabWidth = outputRangeWidth[i]; const nextTabWidth = outputRangeWidth[i + 1] || 0; let scrollOffset = offset; if (offset + tabWidth + nextTabWidth + 2 * marginValue >= scrollWidth) { if (isLast) { scrollOffset = offset - (tabContainerWidth - (tabWidth + marginValue)); } else { // 1.3 - is a magical constant // actually it is just 1.3 of margins. If to place 2 // scrollOffset will match with last tab's offset // And it will prevent bounce effect while scrolling scrollOffset = offset - (tabContainerWidth - (tabWidth + nextTabWidth + 1.3 * marginValue)); } } else { scrollOffset = offset - (tabContainerWidth - (tabWidth + marginValue) + (nextTabWidth + 2 * marginValue)) / 2; scrollOffset = scrollOffset >= 0 ? scrollOffset : 0; } outputRangeScroll.push(scrollOffset); } if (scrollWidth <= tabContainerWidth) { this.scrollOffsetsCollection = this.props.scrollValue.interpolate({ inputRange: [-1, 0], // $FlowFixMe outputRange: [-40, 0], // $FlowFixMe extrapolate: 'clamp', // $FlowFixMe }); return; } // $FlowFixMe this.scrollOffsetsCollection = this.props.scrollValue.interpolate({ inputRange: [-1, ...inputRange], // $FlowFixMe outputRange: [-40, ...outputRangeScroll], // $FlowFixMe }); }; onTabBarScrolling = (e: Object) => { this.currentContentOffset = e.nativeEvent.contentOffset; }; renderUnderline() { const tabUnderlineStyle = { position: 'absolute', backgroundColor: this.props.underlineColor, height: this.props.underlineHeight, width: 1, bottom: this.props.underlineBottomPosition, padding: 0, }; return <Animated.View ref={this.setUnderlineRef} style={[tabUnderlineStyle]} />; } renderTab = ( tab: TabType, page: number, isTabActive: boolean, onPressHandler: Function, onTabLayout: Function, ) => { const { tabBadgeColor, activeTabTextStyle } = this.props; const { label, badge, badgeColor } = tab; const activeTextColor = this.props.activeTextColor || 'navy'; const inactiveTextColor = this.props.inactiveTextColor || 'black'; const textStyle = this.props.tabBarTextStyle; return ( <TouchableOpacity style={[ styles.tab, this.props.tabMargin && { marginLeft: this.props.tabMargin }, this.props.tabStyles.tab, ]} key={page} onPress={onPressHandler} onLayout={onTabLayout} > <Text style={[ { color: isTabActive ? activeTextColor : inactiveTextColor }, textStyle, isTabActive && activeTabTextStyle, ]} > {label} </Text> {badge != null && (parseInt(badge, 10) > 0) && ( <View style={[ styles.badgeBubble, this.props.tabStyles.badgeBubble, { backgroundColor: badgeColor || tabBadgeColor || activeTextColor }, ]} > <Text style={[styles.badgeText, this.props.tabStyles.badgeText]}>{badge}</Text> </View> )} </TouchableOpacity> ); }; render() { const { style, backgroundColor, tabBarStyle, tabMargin, scrollContainerStyle, tabs, } = this.props; return ( <View style={[styles.tabs, { backgroundColor }, style, tabBarStyle]} onLayout={this.measureTabsContainer} > <ScrollView horizontal contentContainerStyle={[ styles.scrollContainer, tabMargin && { paddingRight: tabMargin }, scrollContainerStyle, ]} showsHorizontalScrollIndicator={false} onContentSizeChange={this.onScrollContentSizeChange} ref={node => (this.scrollView = node)} bounces={false} scrollEventThrottle={1} onScroll={this.onTabBarScrolling} > {tabs.map((tab: TabType, page: number) => { const renderTab = this.props.renderTab || this.renderTab; const isTabActive = this.props.activeTab === page; const onPressHandler = () => this.props.goToPage(page); const onTabLayout = (event: Object) => this.onTabLayout(event, page); return renderTab(tab, page, isTabActive, onPressHandler, onTabLayout); })} {this.state.renderUnderline && this.renderUnderline()} </ScrollView> </View> ); } } export default TabBar;