tuya-panel-kit
Version:
a functional component library for developing tuya device panels!
357 lines (336 loc) • 9.95 kB
JavaScript
/* eslint-disable prettier/prettier */
/* eslint-disable react/no-unused-state */
import React from 'react';
import PropTypes from 'prop-types';
import {
View,
ScrollView,
TouchableOpacity,
Text,
StyleSheet,
Dimensions,
Animated,
Platform,
ViewPropTypes,
} from 'react-native';
import Utils from './utils';
import { RatioUtils } from '../../utils';
const { convert } = RatioUtils;
class TabBar extends React.Component {
static defaultProps = {
tabBarBackgroundColor: '#fff',
page: 5,
tabDefaultColor: '#333',
tabTextStyle: {},
tabStyle: {},
tabActiveTextStyle: {},
tabBarUnderlineStyle: {},
tabBarStyle: {},
tabsContainerStyle: {},
tabBarPosition: 'top',
};
static propTypes = {
/**
* 测试标志
*/
tabNavAccessibilityLabel: PropTypes.string.isRequired,
/**
* tab点击回调
*/
onTabClick: PropTypes.func.isRequired,
/**
* tab滚动数据源
*/
scrollValue: PropTypes.object.isRequired,
/**
* 设置tabBar的背景颜色
*/
tabBarBackgroundColor: PropTypes.string,
/**
* tab页
*/
page: PropTypes.number,
/**
* tab内容源
*/
panels: PropTypes.oneOfType([PropTypes.element, PropTypes.array]).isRequired,
/**
* 所激活视图的key
*/
activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
/**
* tab默认颜色
*/
tabDefaultColor: PropTypes.string,
/**
* 设置tab内文字样式
*/
tabTextStyle: Text.propTypes.style,
/**
* 设置激活的tab内文字样式
*/
tabActiveTextStyle: Text.propTypes.style,
/**
* 设置每个tab的样式
*/
tabStyle: ViewPropTypes.style,
/**
* 设置tabBar的下划线样式
*/
tabBarUnderlineStyle: ViewPropTypes.style,
/**
* 设置tabBar的样式
*/
tabBarStyle: ViewPropTypes.style,
/**
* 设置包裹tabBar的容器样式
*/
tabsContainerStyle: ViewPropTypes.style,
/**
* tabBar的位置
*/
tabBarPosition: PropTypes.oneOf(['top', 'bottom']),
};
constructor(props) {
super(props);
this.state = {
containerWidth: Dimensions.get('window').width,
tabContainerWidth: Dimensions.get('window').width,
underlineLeft: new Animated.Value(0),
underlineWidth: new Animated.Value(0),
};
this._tabsMeasurements = [];
}
componentDidMount() {
this.props.scrollValue.addListener(this.updateView);
}
onPress = index => {
const { onTabClick } = this.props;
onTabClick && onTabClick(index);
};
onTabLayout = (page, event) => {
const { x, width, height } = event.nativeEvent.layout;
this._tabsMeasurements[page] = { left: x, right: x + width, width, height };
this.updateView({ value: this.props.scrollValue._value });
};
onContainerLayout = e => {
this._containerLayout = e.nativeEvent.layout;
this.setState({ containerWidth: this._containerLayout.width });
this.updateView({ value: this.props.scrollValue._value });
};
onTabContainerLayout = e => {
this._tabContainerLayout = e.nativeEvent.layout;
let { width } = this._tabContainerLayout;
if (width < Dimensions.get('window').width) {
// eslint-disable-next-line prefer-destructuring
width = Dimensions.get('window').width;
}
this.setState({ tabContainerWidth: width });
this.updateView({ value: this.props.scrollValue._value });
};
getTabs = () => {
const {
panels,
activeKey,
tabDefaultColor,
tabTextStyle,
tabStyle,
tabActiveTextStyle,
page,
tabNavAccessibilityLabel,
} = this.props;
return React.Children.map(panels, (child, index) => {
if (!child) return;
const isActive = activeKey === child.key;
// eslint-disable-next-line max-len
const tabWidth = child.props.tabWidth
? child.props.tabWidth
: this.state.containerWidth / Math.min(page, panels.length);
const realTabStyle = [styles.tab, tabStyle, { width: tabWidth }];
const finalTabTextStyle = [
styles.tabText,
tabDefaultColor && { color: tabDefaultColor },
tabTextStyle,
isActive && styles.activeText,
isActive && tabActiveTextStyle,
];
return (
<TouchableOpacity
activeOpacity={1}
onPress={() => this.onPress(index)}
key={index}
accessibilityLabel={`${tabNavAccessibilityLabel}_${index}`}
style={tabStyle}
onLayout={e => this.onTabLayout(index, e)}
>
<View style={realTabStyle}>
{typeof child.props.tab === 'string' ? (
<Text numberOfLines={1} style={finalTabTextStyle}>
{child.props.tab}
</Text>
) : (
child.props.tab
)}
</View>
</TouchableOpacity>
);
});
};
getUnderLine = () => {
const { tabBarUnderlineStyle } = this.props;
const tabUnderlineStyle = [
{ position: 'absolute', bottom: 0 },
styles.underline,
{ width: this.state.underlineWidth },
tabBarUnderlineStyle,
{ left: this.state.underlineLeft },
];
return <Animated.View style={tabUnderlineStyle} />;
};
updateTabUnderline = (page, offset, count) => {
const { left, right } = this._tabsMeasurements[page];
if (page === count - 1) {
this.state.underlineLeft.setValue(left);
this.state.underlineWidth.setValue(right - left);
return;
}
if (page >= 0 && page <= count - 1) {
const nowLeft = left;
const nextTabLeft = this._tabsMeasurements[page + 1].left;
const newLineLeft = offset * nextTabLeft + (1 - offset) * nowLeft;
const nowRight = right;
const nextTabRight = this._tabsMeasurements[page + 1].right;
const newLineRight = offset * nextTabRight + (1 - offset) * nowRight;
this.state.underlineWidth.setValue(newLineRight - newLineLeft);
this.state.underlineLeft.setValue(newLineLeft);
}
};
updateTabPanel = (page, offset) => {
const containerWidth = this._containerLayout.width;
const tabWidth = this._tabsMeasurements[page].width;
const nextTabMeasurements = this._tabsMeasurements[page + 1];
const nextTabWidth = (nextTabMeasurements && nextTabMeasurements.width) || 0;
const tabOffset = this._tabsMeasurements[page].left;
const absolutePageOffset = offset * tabWidth;
let newScrollX = tabOffset + absolutePageOffset;
newScrollX -= (containerWidth - (1 - offset) * tabWidth - offset * nextTabWidth) / 2;
newScrollX = newScrollX >= 0 ? newScrollX : 0;
if (Platform.OS === 'android') {
this.scrollView && this.scrollView.scrollTo({ x: newScrollX, y: 0, animated: true });
} else {
// eslint-disable-next-line max-len
const rightBoundScroll = Math.max(
this._tabContainerLayout.width - this._containerLayout.width,
0
);
newScrollX = newScrollX > rightBoundScroll ? rightBoundScroll : newScrollX;
this.scrollView && this.scrollView.scrollTo({ x: newScrollX, y: 0, animated: true });
}
};
measureIsReady = (page, isLastTab) => {
return (
this._tabsMeasurements[page] &&
(isLastTab || this._tabsMeasurements[page + 1]) &&
this._tabContainerLayout &&
this._containerLayout
);
};
updateView = offset => {
const position = Math.floor(offset.value);
const pageOffset = offset.value % 1;
const tabCount = Utils.toArray(this.props.panels).length;
const lastTabPosition = tabCount - 1;
if (tabCount === 0 || offset.value < 0 || offset.value > lastTabPosition) {
return;
}
if (this.measureIsReady(position, position === lastTabPosition)) {
this.updateTabPanel(position, pageOffset);
this.updateTabUnderline(position, pageOffset, tabCount);
}
};
render() {
const {
panels,
activeKey,
tabBarStyle,
tabsContainerStyle,
tabBarBackgroundColor,
tabBarPosition,
} = this.props;
const page = Utils.getActiveIndex(panels, activeKey);
const borderPosition =
tabBarPosition === 'top'
? {
borderBottomWidth: 1,
}
: {
borderTopWidth: 1,
};
return (
<View
onLayout={this.onContainerLayout}
style={[styles.container, borderPosition, tabBarStyle]}
>
<ScrollView
ref={scrollView => {
this.scrollView = scrollView;
}}
horizontal={true}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
directionalLockEnabled={true}
bounces={false}
scrollsToTop={false}
scrollEnabled={panels.length > page}
>
<View
onLayout={this.onTabContainerLayout}
// eslint-disable-next-line max-len
style={[
styles.tabContainer,
tabsContainerStyle,
{ backgroundColor: tabBarBackgroundColor },
]}
>
{this.getTabs()}
{this.getUnderLine()}
</View>
</ScrollView>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
minHeight: convert(43.5),
borderColor: '#eee',
},
tabContainer: {
flex: 1,
flexDirection: 'row',
backgroundColor: '#fff',
justifyContent: 'space-around',
},
tab: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: 0,
paddingBottom: 0,
paddingRight: convert(2),
paddingLeft: convert(2),
flexDirection: 'row',
},
tabText: {
fontSize: convert(15),
color: '#000',
},
activeText: {
color: '#108ee9',
},
underline: {
height: convert(2),
backgroundColor: '#108ee9',
},
});
export default TabBar;