组件概述
TabLayout 是一个 React Native 标签页组件,支持水平和垂直布局、自定义标签样式、等宽/不等宽布局、平滑滚动和居中定位功能。
Props 参数说明
参数名 | 类型 | 默认值 | 说明 |
---|---|---|---|
data | T[] | [] | 数据源数组 |
selectIndex | number | 0 | 初始选中的标签索引 |
tabSpace | number | 0 | 标签之间的间距 |
isEquWidth | boolean | - | 是否启用等宽标签 |
isVertical | boolean | - | 是否垂直布局 |
itemWidth | number | - | 单个标签宽度(当 isEquWidth=false 时生效) |
builder | (item: T, index: number, isSelected: boolean) => React.ReactNode | - | 自定义标签渲染函数 |
onSelectChange | (index: number) => void | () => {} | 标签选中回调 |
keyExtractor | (item: T, index: number) => string | - | 提取唯一键的函数 |
style | StyleProp | - | 自定义容器样式 |
方法说明
scrollToIndex(index: number)
滚动到指定索引的标签并居中显示
参数:
- index: 要滚动到的标签索引
使用示例
// 水平布局示例
<TabLayout
data={['标签1', '标签2', '标签3']}
selectIndex={0}
tabSpace={10}
isEquWidth={true}
builder={(item, index, isSelected) => (
<Text style={{color: isSelected ? 'red' : 'black'}}>
{item}
</Text>
)}
onSelectChange={(index) => console.log('选中:', index)}
/>
// 垂直布局示例
<TabLayout
data={['标签1', '标签2', '标签3']}
selectIndex={0}
tabSpace={10}
isVertical={true}
builder={(item, index, isSelected) => (
<Text style={{color: isSelected ? 'red' : 'black'}}>
{item}
</Text>
)}
onSelectChange={(index) => console.log('选中:', index)}
/>
源码
import React, {Component, createRef} from 'react';
import {
FlatList,
LayoutChangeEvent,
StyleProp,
TouchableOpacity,
ViewStyle,
} from 'react-native';
type TabLayoutProps<T> = {
/**
* 数据源
*/
data?: T[];
/**
* 选中的索引
*/
selectIndex?: number;
/**
* 标签之间的间距
*/
tabSpace?: number;
/**
* 是否等宽
*/
isEquWidth?: boolean;
/**
* 是否垂直
*/
isVertical?: boolean;
/**
* 单个item宽度
*/
itemWidth?: number;
/**
* 单个item高度
*/
itemHeight?: number;
/**
* 自定义item渲染
* @param item 当前项
* @param isSelected 是否选中
* @param index 索引
*/
builder?: (item: T, index: number, isSelected: boolean) => React.ReactNode;
/**
* 选中回调
* @param index 选中的索引
*/
onSelectChange?: (index: number) => void;
/**
* 用于提取给定索引处的项的唯一键。键用于缓存和作为跟踪项重新排序的react键。默认提取器检查item.key,然后回退到使用索引,如React一样。
*/
keyExtractor?: ((item: T, index: number) => string) | undefined;
/**
* 自定义样式
*/
style?: StyleProp<ViewStyle> | undefined;
};
type TabLayoutState = {
selectedIndex: number;
containerWidth: number;
containerHeight: number;
itemWidth: number;
itemHeight: number;
};
export default class TabLayout<T> extends Component<
TabLayoutProps<T>,
TabLayoutState
> {
static defaultProps = {
selectIndex: 0,
tabSpace: 0,
onSelectChange: () => {},
builder: undefined,
data: [],
};
private listRef = createRef<FlatList<any>>();
state: TabLayoutState = {
selectedIndex: 0,
containerWidth: 0,
containerHeight: 0,
itemWidth: this.props.itemWidth ?? 0,
itemHeight: this.props.itemHeight ?? 0,
};
// 获取容器尺寸
onLayout = (event: LayoutChangeEvent) => {
const {width, height} = event.nativeEvent.layout;
if (
width !== this.state.containerWidth ||
height !== this.state.containerHeight
) {
this.setState({containerWidth: width, containerHeight: height});
}
this.setState({
selectedIndex: this.props.selectIndex ?? 0,
});
};
// 计算尺寸
getItemSize = () => {
const {data, tabSpace, isEquWidth, isVertical} = this.props;
const {containerWidth, containerHeight, itemWidth, itemHeight} = this.state;
if (isVertical) {
// 垂直方向处理
if (isEquWidth && data && data.length > 0) {
const spacing = tabSpace ?? 0;
const availableHeight = containerHeight - 2 * spacing;
return availableHeight / data.length;
}
return itemHeight ?? 0;
} else {
// 水平方向处理
if (isEquWidth && data && data.length > 0) {
const spacing = tabSpace ?? 0;
const availableWidth = containerWidth - 2 * spacing;
return availableWidth / data.length;
}
return itemWidth ?? 0;
}
};
handlePress = (index: number) => {
const {onSelectChange} = this.props;
this.setState({selectedIndex: index});
onSelectChange?.(index);
this.scrollToIndex(index);
};
scrollToIndex = (index: number) => {
if (!this.listRef.current) {
return;
}
this.listRef.current.scrollToIndex({
index,
animated: true,
viewPosition: 0.5,
});
};
renderItem = ({item, index}: {item: T; index: number}) => {
let size: number | undefined = this.getItemSize();
if (size === 0) {
size = undefined;
}
const isSelected = index === this.state.selectedIndex;
const isVertical = this.props.isVertical;
return (
<TouchableOpacity
activeOpacity={0.7}
onPress={() => this.handlePress(index)}
onLayout={e => {
if (isVertical) {
if (this.state.itemHeight > 0) {
return;
}
this.setState({
itemHeight: e.nativeEvent.layout.height,
});
} else {
if (this.state.itemWidth > 0) {
return;
}
this.setState({
itemWidth: e.nativeEvent.layout.width,
});
}
}}
style={{
width: isVertical ? undefined : size,
height: isVertical ? size : undefined,
}}>
{this.props.builder?.(item, index, isSelected)}
</TouchableOpacity>
);
};
render() {
const {data, tabSpace, isVertical, keyExtractor, style} = this.props;
const {selectedIndex} = this.state;
return (
<FlatList
ref={this.listRef}
data={data}
style={style}
horizontal={!isVertical}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
paddingHorizontal: isVertical ? undefined : tabSpace,
paddingVertical: isVertical ? tabSpace : undefined,
}}
keyExtractor={keyExtractor}
renderItem={this.renderItem}
bounces={false}
onLayout={this.onLayout}
getItemLayout={(d, index) => ({
length: this.getItemSize(),
offset: this.getItemSize() * index,
index,
})}
initialScrollIndex={selectedIndex}
/>
);
}
}