【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-自定义容器样式

方法说明

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}
      />
    );
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值