概述
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 说明
属性名 | 类型 | 必填 | 说明 |
---|---|---|---|
data | Group<T>[] | 分组数据源,每个分组包含标题和子数据数组 | |
renderHeader | (title: string) => ReactNode | 标题行渲染函数 | |
renderItem | (item: T, index: number) => ReactNode | 条目渲染函数 | |
headHeight | number | 标题行高度(像素) | |
itemHeight | number | 条目高度(像素) | |
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>
);
}
}