RN-RecyclerListView使用详解

4次阅读

共计 11161 个字符,预计需要花费 28 分钟才能阅读完成。

安装

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 设置 celldim.widthdim.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.
正文完
 0