【React Native】基于RecyclerListView的分组列表组件

概述

GroupedRecyclerList 是一个基于 recyclerlistview 的分组列表组件

安装依赖

npm install recyclerlistview

基本用法

import GroupedRecyclerList from './GroupedRecyclerList';

function App() {
  const data = [
    { 
      title: 'A', 
      data: [{id: 1, name: 'Apple'}, {id: 2, name: 'Apricot'}] 
    },
    { 
      title: 'B', 
      data: [{id: 3, name: 'Banana'}] 
    }
  ];

  return (
    <GroupedRecyclerList
      data={data}
      headHeight={40}
      itemHeight={60}
      renderHeader={(title) => (
        <View style={{height: 40, backgroundColor: '#ccc'}}>
          <Text>{title}</Text>
        </View>
      )}
      renderItem={(item) => (
        <View style={{height: 60, borderBottomWidth: 1}}>
          <Text>{item.name}</Text>
        </View>
      )}
      onSectionChanged={(index, title) => {
        console.log(`当前展示第${index}${title}`);
      }}
    />
  );
}

Props 说明

属性名类型必填说明
dataGroup<T>[]分组数据源,每个分组包含标题和子数据数组
renderHeader(title: string) => ReactNode标题行渲染函数
renderItem(item: T, index: number) => ReactNode条目渲染函数
headHeightnumber标题行高度(像素)
itemHeightnumber条目高度(像素)
onSectionChanged(index: number, title: string) => void当前可见分组变化时的回调

公共方法

scrollToSessionIndex(index: number)

滚动到指定分组的起始位置

源码

import React, {Component} from 'react';
import {DataProvider, LayoutProvider, RecyclerListView} from 'recyclerlistview';
import {View} from 'react-native';

const ViewTypes = {
  TITLE: 0,
  ITEM: 1,
  EMPTY: 2,
};

export class Group<T> {
  title?: string;
  data?: T[];
}

export interface FlatData<T> {
  title?: string;
  data?: T;
  type?: string | number;
  index: number;
}

// 组件 props 类型
export interface GroupedRecyclerListProps<T> {
  data: Group<T>[];
  renderHeader: (title: string) => React.ReactNode;
  renderItem: (item: T, index: number) => React.ReactNode;
  headHeight: number;
  itemHeight: number;
  onSectionChanged?: (index: number, title: string) => void;
}

interface GroupedRecyclerListState<T> {
  width: number;
  topTitle: string;
  topIndex: number;
  showList: FlatData<T>[];
  dataProvider: DataProvider;
}

// 主组件
export default class GroupedRecyclerList<T> extends Component<
  GroupedRecyclerListProps<T>,
  GroupedRecyclerListState<T>
> {
  _layoutProvider: LayoutProvider;
  private _recyclerListView: RecyclerListView<any, any> | null = null;

  constructor(props: GroupedRecyclerListProps<T>) {
    super(props);

    this._layoutProvider = new LayoutProvider(
      index => {
        return this.getData(index)?.type ?? 0;
      },
      (type, dim, index) => {
        const columnWidth = this.state.width;
        if (type === ViewTypes.TITLE) {
          dim.width = columnWidth;
          dim.height = this.props.headHeight;
        } else if (type === ViewTypes.ITEM) {
          dim.width = columnWidth;
          dim.height = this.props.itemHeight;
        } else {
          dim.width = 0;
          dim.height = 0;
        }
      },
    );

    this._renderRow = this._renderRow.bind(this);

    const result: FlatData<T>[] = [];
    result.push({
      type: ViewTypes.EMPTY,
      title: '',
      index: 0,
    });
    this.state = {
      width: 0,
      topTitle: '',
      topIndex: 0,
      showList: result,
      dataProvider: new DataProvider((r1, r2) => {
        return r1 !== r2;
      }).cloneWithRows(result),
    };
  }

  componentDidUpdate(
    prevProps: Readonly<GroupedRecyclerListProps<T>>,
    prevState: Readonly<GroupedRecyclerListState<T>>,
    snapshot?: any,
  ) {
    if (prevProps.data !== this.props.data) {
      const result: FlatData<T>[] = [];
      this.props.data.forEach((group, index) => {
        result.push({
          type: ViewTypes.TITLE,
          title: group.title,
          index: index,
        });

        group.data?.forEach(item => {
          result.push({
            type: ViewTypes.ITEM,
            title: group.title,
            data: item,
            index: index,
          });
        });
      });

      if (result.length === 0) {
        result.push({
          type: ViewTypes.EMPTY,
          title: '',
          index: 0,
        });
      }
      this.setState({
        showList: result,
        dataProvider: new DataProvider((r1, r2) => {
          return r1 !== r2;
        }).cloneWithRows(result),
      });
    }
  }

  getData = (index: number) => {
    return this.state.showList[index];
  };

  _renderRow(type: any, d: FlatData<T>, index: number) {
    switch (type) {
      case ViewTypes.TITLE:
        return (
          <View style={{opacity: index > 0 ? 1 : 0}}>
            {this.props.renderHeader(d.title ?? '')}
          </View>
        );
      case ViewTypes.ITEM:
        return <>{this.props.renderItem(d.data as T, index)}</>;
      case ViewTypes.EMPTY:
        return <></>;
      default:
        return null;
    }
  }

  scrollToSessionIndex(index: number) {
    console.log('scrollToSessionIndex', index);
    const idx = this.state.showList.findIndex(item => item.index === index);
    if (idx >= 0) {
      this._recyclerListView?.scrollToIndex(idx);
    }
  }

  render() {
    return (
      <View
        style={{
          width: '100%',
          flex: 1,
        }}
        onLayout={event => {
          if (this.state.width === 0 && event.nativeEvent.layout.width > 0) {
            this.setState({
              width: event.nativeEvent.layout.width,
            });
          }
        }}>
        <RecyclerListView
          ref={ref => {
            this._recyclerListView = ref;
          }}
          layoutProvider={this._layoutProvider}
          dataProvider={this.state.dataProvider}
          rowRenderer={this._renderRow}
          showsVerticalScrollIndicator={false}
          onVisibleIndicesChanged={(
            all: number[],
            now: number[],
            notNow: number[],
          ) => {
            if (all.length === 0) {
              return;
            }
            const d = this.getData(all[0]);
            if (d.title !== this.state.topTitle) {
              this.setState({
                topTitle: d.title ?? '',
                topIndex: d.index,
              });
              this.props.onSectionChanged?.(d.index, d.title ?? '');
            }
          }}
        />
        <View
          style={{
            position: 'absolute',
            width: this.state.width,
            height: this.props.headHeight,
          }}>
          {this.props.renderHeader(this.state.topTitle)}
        </View>
      </View>
    );
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值