【React Native】自定义TabLayout

组件概述

TabLayout 是一个 React Native 标签页组件,支持水平和垂直布局、自定义标签样式、等宽/不等宽布局、平滑滚动和居中定位功能。

Props 参数说明

参数名类型默认值说明
dataT[][]数据源数组
selectIndexnumber0初始选中的标签索引
tabSpacenumber0标签之间的间距
isEquWidthboolean-是否启用等宽标签
isVerticalboolean-是否垂直布局
itemWidthnumber-单个标签宽度(当 isEquWidth=false 时生效)
builder(item: T, index: number, isSelected: boolean) => React.ReactNode-自定义标签渲染函数
onSelectChange(index: number) => void() => {}标签选中回调
keyExtractor(item: T, index: number) => string-提取唯一键的函数
styleStyleProp-自定义容器样式

使用示例

// 水平布局示例
<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)}
/>
  • : 如果不能确定父组件是否正常更新,可以给 <TabLayout> 添加 key 强制刷新,强制让 <TabLayout> 在数据变化时重建。

源码

import React, {Component, createRef} from 'react';
import {
  FlatList,
  LayoutChangeEvent,
  StyleProp,
  TouchableOpacity,
  View,
  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;
  /**
   * 是否弹性换行
   */
  isWrap?: boolean;
};

type TabLayoutState = {
  selectedIndex: number;
  containerWidth: number;
  containerHeight: number;
  itemWidthList: number[];
  itemHeightList: 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,
    itemWidthList: [],
    itemHeightList: [],
  };

  componentDidUpdate(
    prevProps: Readonly<TabLayoutProps<T>>,
    prevState: Readonly<TabLayoutState>,
    snapshot?: any,
  ) {
    if (prevProps.selectIndex !== this.props.selectIndex) {
      const index = this.props.selectIndex ?? 0;
      this.scrollToIndex(index);
    }
    if (prevProps.data !== this.props.data) {
      this.setState({
        containerWidth: 0, // 重置宽度,让 onLayout 重新计算
        containerHeight: 0, // 重置高度
        itemHeightList: new Array(this.props.data?.length ?? 0).fill(0),
        itemWidthList: new Array(this.props.data?.length ?? 0).fill(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 = (index: number) => {
    const {isVertical} = this.props;
    const {itemWidthList, itemHeightList} = this.state;

    if (isVertical) {
      return itemHeightList[index] ?? 0;
    } else {
      return itemWidthList[index] ?? 0;
    }
  };

  handlePress = (index: number) => {
    const {onSelectChange} = this.props;
    onSelectChange?.(index);
    this.scrollToIndex(index);
  };

  scrollToIndex = (index: number) => {
    if (this.state.selectedIndex !== index) {
      this.setState({selectedIndex: index});
    }
    if (!this.listRef.current) {
      return;
    }

    const len = this.props.data?.length ?? 0;
    if (index < (this.props.data?.length ?? 0) && index >= 0) {
      this.listRef.current.scrollToIndex({
        index,
        animated: true,
        viewPosition: index === len - 1 ? 0 : 0.5,
      });
    }
  };

  renderItem = ({item, index}: {item: T; index: number}) => {
    const isSelected = index === this.state.selectedIndex;
    const isVertical = this.props.isVertical;

    return (
      <TouchableOpacity
        activeOpacity={1}
        onPress={() => this.handlePress(index)}
        onLayout={e => {
          this.updateSize(e, index, isVertical);
        }}>
        {this.props.builder?.(item, index, isSelected)}
      </TouchableOpacity>
    );
  };

  updateSize(
    e: LayoutChangeEvent,
    index: number,
    isVertical: boolean | undefined,
  ) {
    if (isVertical) {
      const itemHeightList = this.state.itemHeightList;
      const h = itemHeightList[index];
      let newH = e.nativeEvent.layout.height;
      if (h > 0 && h >= newH) {
        return;
      }
      itemHeightList[index] = newH;
      this.setState({
        itemHeightList: itemHeightList,
      });
    } else {
      const itemWidthList = this.state.itemWidthList;
      const w = itemWidthList[index];
      let newW = e.nativeEvent.layout.width;
      if (w > 0 && w >= newW) {
        return;
      }
      itemWidthList[index] = newW;
      this.setState({
        itemWidthList: itemWidthList,
      });
    }
  }

  render() {
    const {
      data,
      tabSpace,
      isVertical,
      keyExtractor,
      style,
      isWrap,
      isEquWidth,
    } = this.props;
    const {selectedIndex} = this.state;

    if (isWrap) {
      return (
        <View
          style={[
            {
              flexDirection: 'row',
              flexWrap: 'wrap',
            },
            style,
          ]}>
          {data?.map((item, index) => {
            return (
              <View
                style={{
                  marginRight: tabSpace,
                  marginBottom: tabSpace,
                }}
                key={keyExtractor?.(item, index)}>
                {this.renderItem({item, index})}
              </View>
            );
          })}
        </View>
      );
    }

    if (isEquWidth) {
      return (
        <View
          style={[
            {
              flexDirection: 'row',
            },
            style,
          ]}>
          {data?.map((item, index) => {
            return (
              <View style={{flex: 1}} key={keyExtractor?.(item, index)}>
                {this.renderItem({item, index})}
              </View>
            );
          })}
        </View>
      );
    }

    return (
      <View style={style}>
        <FlatList
          ref={this.listRef}
          data={data}
          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(index),
            offset: this.getItemSize(index) * index,
            index,
          })}
          initialScrollIndex={selectedIndex}
        />
      </View>
    );
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值