安装
npm install --save recyclerlistview或者:
npm install --save recyclerlistview@beta
概述和功能
RecyclerListView 是一个高性能的列表(listview)组件,同时支持 React Native 和 Web ,并且可用于复杂的列表。RecyclerListView 组件的实现灵感,来自于 Android RecyclerView
原生组件及iOS UICollectionView
原生组件。
RecyclerListView使用“cell recycling”来重用不再可见的视图来呈现项目,而不是创建新的视图对象。 对象的创建非常昂贵并且带有内存开销,这意味着当您滚动列表时内存占用量不断增加。 从内存中释放不可见的项目是另一种技术,但这会导致创建更多的对象和大量的垃圾收集。 回收是渲染无限列表的最佳方式,不会影响性能或内存效率。
为什么需要RecyclerListView
我们知道,React Native 的其他列表组件如ListView
,会一次性创建所有的列表单元格——cell
。如果列表数据比较多,则会创建很多的视图对象,而视图对象是非常消耗内存的。所以,ListView
组件,对于我们业务中的这种无限列表,基本上是不可以用的。
对于React Native 官方提供的高性能的列表组件FlatList
, 在Android设备上的表现,并不是十分友好。它的实现原理,是将列表中不在可视区域内的视图,进行回收,然后根据页面的滚动,不断的渲染出现在可视区域内的视图。这里需要注意的是,FlatList
是将不可见的视图回收,从内存中清除了,下次需要的时候,再重新创建。这就要求设备在滚动的时候,能快速的创建出需要的视图,才能让列表流畅的展现在用户面前。而问题也就出现在这里,Android设备因为老化等原因,计算力等跟不上,加之React Native 本身 JS 层与 Native 层之间交互的一些问题(这里不做深入追究),导致创建视图的速度达不到使列表流畅滚动的要求。
那怎样来解决这样的问题呢?
RecyclerListView 受到 Android RecyclerView
和 iOS UICollectionView
的启发,进行两方面的优化:
- 仅创建可见区域的视图,这步与
FlatList
是一致的。 -
cell recycling
,重用单元格,这个做法是FlatList
缺乏的。
对于程序来说,视图对象的创建是非常昂贵的,并且伴随着内存的消耗。意味着如果不断的创建视图,在列表滚动的过程中,内存占用量会不断增加。FlatList
中将不可见的视图从内存中移除,这是一个比较好的优化手段,但同时也会导致大量的视图重新创建以及垃圾回收。
RecyclerListView 通过对不可见视图对象进行缓存及重复利用,一方面不会创建大量的视图对象,另一方面也不需要频繁的创建视图对象和垃圾回收。
基于这样的理论,所以RecyclerListView的性能是会优于FlatList的
RecyclerListView的使用
属性:
1、dataProvider
首先需要定义一个数据驱动方法
let dataProvider = new DataProvider((r1, r2) => {
return r1 !== r2;
})
定义完成之后去初始化数据
this.state = {
dataProvider: dataProvider.cloneWithRows(this._generateArray(300))
};
_generateArray(n) {
let arr = new Array(n);
for (let i = 0; i < n; i++) {
arr[i] = i;
}
return arr;
}
cloneWithRows
想要更新列表的dataProvider数据也就是(DataSource)必须每次通过cloneWithRows这个来重新挂载datasource的值。clone方法会自动提取新数据并进行逐行对比(使用rowHasChanged方法中的策略),这样列表就知道哪些行需要重新渲染了
2、LayoutProvider
定义列表布局
在这之前我们可以根据我们的业务场景,规划处几类的布局,然后自定义每种布局的类型来区分
//表示列表中会出现三种ui类型的item
const ViewTypes = {
FULL: 0,
HALF_LEFT: 1,
HALF_RIGHT: 2
}
下面就可以来区分布局了
为了进行cell-recycling
,RecyclerListView要求对每个cell
(通常也叫Item)定义一个type
,根据type
设置cell
的dim.width
和dim.height
:
//第一个函数是定义item的ui类型,第二个是定义item的高宽
this._layoutProvider = new LayoutProvider(
index => {
if (index % 3 === 0) {
return ViewTypes.FULL;
}
...
},
(type, dim) => {
switch (type) {
case ViewTypes.HALF_LEFT:
dim.width = width / 2;
dim.height = 160;
break;
...
}
}
)
3、rowRenderer
rowRenderer
负责渲染一个cell
,同样是根据type
来进行渲染:
_rowRenderer(type, data) {
switch (type) {
case ViewTypes.HALF_LEFT:
return (
<CellContainer style={styles.containerGridLeft}>
<Text>Data: {data}</Text>
</CellContainer>
);
...
}
}
那么下面我们就来看看具体的一个简单的小栗子:
/***
* To test out just copy this component and render in you root component
*/
export default class RecycleTestComponent extends React.Component {
constructor(args) {
super(args);
let { width } = Dimensions.get("window");
//Create the data provider and provide method which takes in two rows of data and return if those two are different or not.
let dataProvider = new DataProvider((r1, r2) => {
return r1 !== r2;
});
//Create the layout provider
//First method: Given an index return the type of item e.g ListItemType1, ListItemType2 in case you have variety of items in your list/grid
//Second: Given a type and object set the height and width for that type on given object
//If you need data based check you can access your data provider here
//You'll need data in most cases, we don't provide it by default to enable things like data virtualization in the future
//NOTE: For complex lists LayoutProvider will also be complex it would then make sense to move it to a different file
this._layoutProvider = new LayoutProvider(
index => {
if (index % 3 === 0) {
return ViewTypes.FULL;
} else if (index % 3 === 1) {
return ViewTypes.HALF_LEFT;
} else {
return ViewTypes.HALF_RIGHT;
}
},
(type, dim) => {
switch (type) {
case ViewTypes.HALF_LEFT:
dim.width = width / 2 - 0.0001;
dim.height = 160;
break;
case ViewTypes.HALF_RIGHT:
dim.width = width / 2;
dim.height = 160;
break;
case ViewTypes.FULL:
dim.width = width;
dim.height = 140;
break;
default:
dim.width = 0;
dim.height = 0;
}
}
);
this._rowRenderer = this._rowRenderer.bind(this);
//Since component should always render once data has changed, make data provider part of the state
this.state = {
dataProvider: dataProvider.cloneWithRows(this._generateArray(300))
};
}
_generateArray(n) {
let arr = new Array(n);
for (let i = 0; i < n; i++) {
arr[i] = i;
}
return arr;
}
//Given type and data return the view component
_rowRenderer(type, data) {
//You can return any view here, CellContainer has no special significance
switch (type) {
case ViewTypes.HALF_LEFT:
return (
<CellContainer style={styles.containerGridLeft}>
<Text>Data: {data}</Text>
</CellContainer>
);
case ViewTypes.HALF_RIGHT:
return (
<CellContainer style={styles.containerGridRight}>
<Text>Data: {data}</Text>
</CellContainer>
);
case ViewTypes.FULL:
return (
<CellContainer style={styles.container}>
<Text>Data: {data}</Text>
</CellContainer>
);
default:
return null;
}
}
render() {
return <RecyclerListView layoutProvider={this._layoutProvider} dataProvider={this.state.dataProvider} rowRenderer={this._rowRenderer} />;
}
}
const styles = {
container: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#00a1f1"
},
containerGridLeft: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#ffbb00"
},
containerGridRight: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#7cbb00"
}
};
页面效果:
但是在实际的业务开发中肯定不会是这么简单的,一般都会用到分页,下拉刷新什么的;下面介绍几个比较常用的属性:
4、onEndReached
列表触底是触发,一般是用来做上拉加载更过数据的时候来使用的
<RecyclerListView
layoutProvider={this._layoutProvider}
dataProvider={this.dataProvider.cloneWithRows(this.state.infoList)}
rowRenderer={this._rowRenderer}
onEndReached={this._onLoadMore}
/>
5、onEndReachedThreshold
列表距离底部多大距离时触发onEndReached的回调,这个填写的是具体的像素值,与FlatList是有区别的,FlatList填写的是百分比
<RecyclerListView
layoutProvider={this._layoutProvider}
dataProvider={this.dataProvider.cloneWithRows(this.state.infoList)}
rowRenderer={this._rowRenderer}
onEndReached={this._onLoadMore}
onEndReachedThreshold={50}
/>
6、extendedState
在更新目前列表渲染以外的数据时,可以使用此属性更新状态,以便绘制出新的列表,并且不再重新渲染以前的列表数据
<RecyclerListView
layoutProvider={this._layoutProvider}
dataProvider={this.dataProvider.cloneWithRows(this.state.infoList)}
rowRenderer={this._rowRenderer}
onEndReached={this._onLoadMore}
onEndReachedThreshold={50}
extendedState={this.state}
/>
7、scrollViewProps
继承scrollView的属性,RecyclerListView本身是不具有刷新属性的,要想使用刷新功能,就可以继承scrollView的下拉刷新
<RecyclerListView
scrollViewProps={{
refreshControl: (
<RefreshControl
refreshing={this.state.loading}
onRefresh={async () => {
this.setState({ loading: true });
await this.getInfo();
this.setState({ loading: false });
}}
/>
)
}}
/>
下面看一下完整的demo
import React, { Component } from "react";
import { View, Text, Dimensions, StyleSheet, RefreshControl, Alert } from "react-native";
import { RecyclerListView, DataProvider, LayoutProvider } from "recyclerlistview";
import WBCST from "./../../rn-app";
const ViewTypes = {
FULL: 0
};
const { width } = Dimensions.get("window");
const styles = StyleSheet.create({
container: {
flexDirection: "row",
justifyContent: "space-between",
// alignItems: "center",
flex: 1,
backgroundColor: "#fff",
// borderWidth: 1,
borderColor: "#dddddd",
margin: 15,
marginTop: 0,
padding: 15
},
topicLeft: {
width: width - 210,
marginRight: 10
},
topicRight: {
backgroundColor: "#f5f5f5",
width: 140,
height: 140,
padding: 15
},
topicTitle: {
color: "#000",
fontSize: 16,
fontWeight: "700",
lineHeight: 28
},
topicContext: {
color: "#999",
fontSize: 12,
lineHeight: 18,
marginTop: 10
},
topicNum: {
fontSize: 14,
marginTop: 20
},
topicRightText: {
fontSize: 14,
color: "#666"
}
});
export default class RecycleTestComponent extends Component {
constructor(props) {
super(props);
this.dataProvider = new DataProvider((r1, r2) => {
return r1 !== r2;
});
let { width } = Dimensions.get("window");
this._layoutProvider = new LayoutProvider(
(index) => {
return ViewTypes.FULL;
},
(type, dim) => {
dim.width = width;
dim.height = 190;
}
);
this.state = {
pagenum: 1,
infoList: [],
loading: false,
isLoadMore: false
};
}
getInfo = () => {
let num = this.state.pagenum;
let info = this.state.infoList;
WBCST.getFetch("http://app.58.com/api/community/aggregatepage/tabs/topic", {
pagesize: 20,
pagenum: num
}).then((res) => {
if (res) {
let loadMore = false;
if (num == 1) {
if (res.data.questions.length == 20) {
loadMore = true;
}
this.setState({
isLoadMore: loadMore,
infoList: res.data.questions
});
} else {
// info.concat(res.data.questions);
if (res.data.questions.length < 20) {
loadMore = false;
} else {
loadMore = true;
}
this.setState({
isLoadMore: loadMore,
infoList: this.state.infoList.concat(res.data.questions)
});
}
}
});
};
_rowRenderer = (type, data) => {
return (
<View style={styles.container}>
<View style={styles.topicLeft}>
<Text numberOfLines={2} style={styles.topicTitle}>
{data.topic.title}
</Text>
<Text numberOfLines={2} style={styles.topicContext}>
{data.topic.context}
</Text>
<Text style={styles.topicNum}>
{data.topic.pn}
人参与此话题
</Text>
</View>
<View style={styles.topicRight}>
<Text style={styles.topicRightText}>{data.user.name}</Text>
<Text style={[{ marginTop: 10 }, styles.topicRightText]}>{data.title}</Text>
</View>
</View>
);
};
_renderFooter = () => {
return (
<View>
<Text>上拉加载更多</Text>
</View>
);
};
_onLoadMore = () => {
// Alert.alert(JSON.stringify("num"));
if (!this.state.isLoadMore) {
return;
}
let num = this.state.pagenum;
num = num + 1;
this.setState(
{
pagenum: num
},
() => {
// Alert.alert(JSON.stringify(num));
this.getInfo();
}
);
};
componentDidMount = () => {
this.getInfo();
};
render() {
return (
<RecyclerListView
layoutProvider={this._layoutProvider}
dataProvider={this.dataProvider.cloneWithRows(this.state.infoList)}
rowRenderer={this._rowRenderer}
extendedState={this.state}
onEndReached={this._onLoadMore}
onEndReachedThreshold={50}
// renderFooter={this._renderFooter}
scrollViewProps={{
refreshControl: (
<RefreshControl
refreshing={this.state.loading}
onRefresh={async () => {
this.setState({ loading: true });
// analytics.logEvent("Event_Stagg_pull_to_refresh");
await this.getInfo();
this.setState({ loading: false });
}}
/>
)
}}
/>
);
}
}
效果图:
RecyclerListView所有属性
Prop | Required | Params Type | Description | |||
---|---|---|---|---|---|---|
layoutProvider | Yes | BaseLayoutProvider | Constructor function that defines the layout (height / width) of each element | |||
dataProvider | Yes | DataProvider | 构造函数,定义列表数据 | |||
contextProvider | No | ContextProvider | 用于在视图被破坏的情况下保持滚动位置,这通常在后退导航时发生 | |||
rowRenderer | Yes | (type: string \ | number, data: any, index: number) => JSX.Element \ | JSX.Element[] \ | null | 渲染列表视图 |
initialOffset | No | number | 要从中开始渲染的初始偏移量; 如果您想跨页面滚动上下文,这非常有用。 | |||
renderAheadOffset | No | number | 指定要呈现视图的提前像素数。 增加此值有助于减少空白;但是要尽可能的填写较低的数字,较高的值会增加重新渲染的计算 | |||
isHorizontal | No | boolean | true水平布局,默认垂直布局 | |||
onScroll | No | rawEvent: ScrollEvent, offsetX: number, offsetY: number) => void | 列表滚动时触发 | |||
onRecreate | No | (params: OnRecreateParams) => void | 回收视图是执行 | |||
externalScrollView | No | { new (props: ScrollViewDefaultProps): BaseScrollView } | Use this to pass your on implementation of BaseScrollView | |||
onEndReached | No | () => void | 列表触底是执行 | |||
onEndReachedThreshold | No | number | 列表距离底部多大距离时触发onEndReached的回调,填写具体像素值 | |||
onVisibleIndexesChanged | No | TOnItemStatusChanged | 可见元素,滚动时实时触发 | |||
renderFooter | No | () => JSX.Element \ | JSX.Element[] \ | null | Provide this method if you want to render a footer. Helpful in showing a loader while doing incremental loads | |
initialRenderIndex | No | number | 指定渲染开始的item index如果同时设置了initialOffset优先执行initialOffset | |||
scrollThrottle | No | number | iOS特有 | |||
canChangeSize | No | boolean | Specify if size can change | |||
distanceFromWindow | No | number | Web only; Specify how far away the first list item is from window top | |||
useWindowScroll | No | boolean | Web only; Layout Elements in window instead of a scrollable div | |||
disableRecycling | No | boolean | Turns off recycling | |||
forceNonDeterministicRendering | No | boolean | Default is false; if enabled dimensions provided in layout provider will not be strictly enforced. Use this if item dimensions cannot be accurately determined | |||
extendedState | No | object | 在更新目前列表渲染以外的数据时,可以使用此属性更新状态,以便绘制出新的列表,并且不再重新渲染以前的列表数据 | |||
itemAnimator | No | ItemAnimator | Enables animating RecyclerListView item cells (shift, add, remove, etc) | |||
optimizeForInsertDeleteAnimations | No | boolean | Enables you to utilize layout animations better by unmounting removed items | |||
style | No | object | To pass down style to inner ScrollView | |||
scrollViewProps | No | object | For all props that need to be proxied to inner/external scrollview. Put them in an object and they'll be spread and passed down. |
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。