学习封装,减少没有必要的重复工作。
在封装前,先设计好这个组件应该具有的功能和api,然后在此基础上进行。
一、List组件的设计
应当尽可能的保持跟官网相似的api接口,减少学习成本。提供的api功能单一,不能一个api多种用途,减少入坑几率。应该有统一的规范,对组件的说明要明确,相同的行为只能有一种输出。
1.1 List规范与说明
此List既包含了FlatList也包含了SectionList。
- 当传入的是data时是普通列表,传入的是sections时是分组列表
- 上拉加载和下拉刷新使用的是react-native-spinkit,不可自定义
- 必须指定每一项的高度,item的高度,section的高度可以不指定,不指定代表高度为0
- 不能手动开启刷新和开始加载
- 可以通过onRefresh中的回调结束刷新,也可以通过ref的方式结束刷新,加载相同
1.2 api定义
| api | 类型说明 | api详情 |
|---|---|---|
| data | array | 数据源,一维数据 |
| sections | array | 当是二维数组的数据 |
| renderItem | ()=> React.ReactElement | 渲染的每一项 |
| ListEmptyComponent | ()=> React.ReactElement | 当数据为空的时候渲染的组件 |
| ListFooterComponent | ()=> React.ReactElement | 尾部组件 |
| ListHeaderComponent | ()=> React.ReactElement | 头部组件 |
| onRefresh | function | 当刷新时调用的函数,提供一个回调,执行则代表刷新结束 |
| onEndReached | function | 当到达底部调用的函数,提供一个回调,执行则代表上拉加载结束 |
| itemHeight | (index)=>number | 每一项的高度 |
| sectionHeight | (index)=>number | 每一组头部的高度 |
| renderSection | ()=> React.ReactElement | 渲染的每一组的头部 |
| loadingCompleted | boolean | 当上拉加载的时候数据是否已经全部完成 |
| style | ViewStyle | 整体的样式 |
| itemStyle | ViewStyle | 每一项的样式 |
| sectionStyle | ViewStyle | 每一组头部的样式 |
提供的方法
| api | 函数定义 | 使用详情 |
|---|---|---|
| scrollToEnd | list.scrollToEnd(); | 跳转到底部 |
| scrollToStart | list.scrollToStart(); | 跳转到顶部 |
| scrollToIndex | list.scrollToIndex(index) | 跳转到指定item |
| scrollToSectionIndex | list.scrollToSectionIndex(index) | 跳转到指定section头部 |
| endRefresh | endRefresh() | 结束刷新 |
| endEndReached | endEndReached() | 结束上拉加载 |
二、已经实现的api和未实现的api
- data
- sections
- renderItem
- ListEmptyComponent
- ListFooterComponent
- ListHeaderComponent
- onRefresh
- onEndReached
- itemHeight
- sectionHeight
- renderSection
- loadingCompleted
- style
- itemStyle
- sectionStyle
- scrollToEnd
- scrollToStart
- scrollToIndex
- scrollToSectionIndex
- endRefresh
- endEndReached
三、具体的代码
import React, { forwardRef, useRef, useImperativeHandle } from 'react'
import { LargeList, RefreshHeader, SpinKit } from '../../../../modules'
import { View, StyleSheet } from 'react-native'
import { LoadingFooter } from 'react-native-spring-scrollview'
const __Refresh = ((refreshStyle) => {
let refreshInstance = null
return () => {
if (refreshInstance === null) {
refreshInstance = class extends RefreshHeader {
render() {
const { size = 20, type = "Circle", color = "red" } = refreshStyle || {}
return (
<View style={{ justifyContent: 'center', alignItems: 'center' }}>
<SpinKit isVisible={true} size={size} type={type} color={color} />
</View>
)
}
}
}
return refreshInstance
}
})()
const __EndReached = ((endReachedStyle) => {
let endReachedInstance = null
return () => {
if (endReachedInstance === null) {
endReachedInstance = class extends LoadingFooter {
render() {
const { size = 20, type = "Circle", color = "red" } = endReachedStyle || {}
return (
<View style={{ justifyContent: 'center', alignItems: 'center' }}>
<SpinKit isVisible={true} size={size} type={type} color={color} />
</View>
)
}
}
}
return endReachedInstance
}
})()
const getData = (data, sections) => {
if (!data) data = []
else if (!sections) sections = []
if (!Array.isArray(data)) throw new Error("你传入的并不是数组,而是:", typeof data)
if (data) {
return [{ items: data }]
} else {
return sections || []
}
}
const RenderItem = (section, index, data, renderItem, itemStyle, itemHeight) => {
return (<View style={StyleSheet.flatten([itemStyle, { height: itemHeight({ section, index }), overflow: 'hidden' }])}>
{renderItem({ item: data[section].items[index], index })}
</View>)
}
const RenderSection = (section, data, renderSection, sectionStyle, sectionHeight) => {
return (<View style={StyleSheet.flatten([sectionStyle, { height: sectionHeight(section), overflow: 'hidden' }])}>
{renderSection({ section: data[section], index: section })}
</View>)
}
const List = forwardRef(({
data,
renderItem,
ListFooterComponent,
ListHeaderComponent,
onRefresh,
onEndReached,
itemHeight,
sectionHeight,
renderSection,
loadingCompleted,
style = {},
refreshStyle = {},
endReachedStyle = {}
}, ref) => {
const listRef = useRef(null)
useImperativeHandle(ref, () => {
return {
...listRef.current || {},
scrollToStart: () => {
console.log("靠转到scrollToStart")
listRef.current.scrollToIndexPath({ section: 0, row: 0 })
},
scrollToEnd: () => {
let row = data[data.length - 1].items.length - 1
listRef.current.scrollToIndexPath({ section: data.length - 1, row })
},
scrollToIndex: (index) => {
if (data.length > 1) {
console.warn("此api适用于普通列表,分组列表请使用scrollToSectionIndex")
}
if (index >= data[0].length) {
index = data[0].length - 1
}
listRef.current.scrollToIndexPath({ section: 0, row: index })
},
scrollToSectionIndex: (section) => {
if (section >= data.length) {
section = data.length - 1
}
listRef.current.scrollToIndexPath({ section: section, row: 0 })
}
}
})
const endRefresh = () => {
if (listRef && listRef.current) {
listRef.current.endRefresh()
}
}
const _onRefresh = () => {
onRefresh && onRefresh(endRefresh)
}
const endLoading = () => {
if (listRef && listRef.current) {
listRef.current.endLoading
}
}
const _onEndReached = () => {
onEndReached && onEndReached(endLoading)
}
if (!itemHeight) {
throw new Error("必须指定每一项的高度")
} else if (typeof itemHeight !== 'function') {
throw new Error("itemHeight需要的类型是函数,并不是:", typeof (itemHeight))
}
return (
<LargeList
data={data}
ref={listRef}
style={style}
{...onRefresh && { refreshHeader: __Refresh(refreshStyle), onRefresh: _onRefresh }}
{...onEndReached && { loadingFooter: __EndReached(endReachedStyle), onEndReached: _onEndReached }}
heightForIndexPath={itemHeight}
renderHeader={ListHeaderComponent}
renderFooter={ListFooterComponent}
renderSection={renderSection}
allLoaded={loadingCompleted}
heightForSection={sectionHeight}
renderIndexPath={renderItem} />
)
})
const SwitchEmptyOrList = forwardRef(({
data, // 数据,一维数组
sections,
renderItem,
itemStyle,
renderSection,
sectionStyle,
sectionHeight = () => 0,
itemHeight,
ListEmptyComponent = () => null,
...props
}, ref) => {
const _data = getData(data, sections)
if (_data.length === 0) {
return ListEmptyComponent()
} else {
return (<List
data={_data}
ref={ref}
renderItem={({ section, row }) => RenderItem(section, row, _data, renderItem, itemStyle, itemHeight)}
{..._data.length > 1 && {
renderSection,
sectionHeight: (section) => RenderSection(section, _data, renderSection, sectionStyle, sectionHeight)
}}
itemHeight={itemHeight}
{...props} />)
}
})
export default SwitchEmptyOrList
四、简单使用
function TestList() {
const _renderItem = ({item, index}) => {
return (
<Text key={index}>{item}</Text>
)
}
return (
<List
data={[1, 2, 3, 4, 5, 6]}
itemHeight={() => 50}
renderItem={_renderItem}
/>
)
}