ReactNative: 使用列表组件ListView组件

一、简介

在前面介绍过了FlatList列表组件用来展示大量的数据,ListView组件也是同样地功能。虽然ListView组件已经过时,但是作为新手还是有必要了解一下。它们的API差不太多,但是ListView组件使用起来确实要比FlatList列表组件复杂一下。ListView组件是一个垂直滚动列表组件,继承自ScrollView组件,拥有ScrollView组件的全部属性。该组件用简单的数据blob数组填充它,并使用该数据源和`renderRow`回调实例化一个`ListView`组件。 它从数据数组中获取一个Blob并返回可渲染组件。跟iOS中UITableView也非常类似。

 

二、API

该组件的属性如下:

//数据源,必不可少,它是ListViewDataSource的实例
dataSource: PropTypes.instanceOf(ListViewDataSource).isRequired,

//要渲染的分割线,是一个函数,返回一个节点元素
//函数示例:(sectionID, rowID, adjacentRowHighlighted) => renderable
renderSeparator: PropTypes.func,

//要渲染的每一行视图(cell),必不可少,通过函数返回一个节点元素
//函数示例:(rowData, sectionID, rowID, highlightRow) => renderable
renderRow: PropTypes.func.isRequired,

//决定初始渲染的行数,可以用等待全屏渲染出来后再显示
initialListSize: PropTypes.number.isRequired,

//当所有行都已呈现并且列表已滚动到底部的onEndReachedThreshold内时调用。当第一次渲染时,如果数据不足一屏(比如初始值是空的),这个事件也会被触发
onEndReached: PropTypes.func,

//调用onEndReached之前的临界值,单位是像素
onEndReachedThreshold: PropTypes.number.isRequired,

//每次事件循环(每帧)渲染的行数
pageSize: PropTypes.number.isRequired,

//渲染组头和表尾视图,函数返回返回一个节点元素
//示例:() => renderable
renderFooter: PropTypes.func,
renderHeader: PropTypes.func,

//渲染组头,函数返回返回一个节点元素
//示例:(sectionData, sectionID) => renderable
renderSectionHeader: PropTypes.func,

//一个函数,该函数返回在其中呈现列表行的可滚动组件。 默认为返回带有给定props的ScrollView
//示例如下:(props) => renderable
renderScrollComponent: React.PropTypes.func.isRequired,

//开始渲染行之前在屏幕上显示的时间,以像素为单位
scrollRenderAheadDistance: React.PropTypes.number.isRequired,

//当可见行的集合更改时调用。
//可见行:`visibleRows`映射 { sectionID: { rowID: true }}
//可见性改变的行:`changedRows`映射 { sectionID: { rowID: true | false }}
//布尔值:true表示可见,false表示不可见
//示例:(visibleRows, changedRows) => void
onChangeVisibleRows: React.PropTypes.func,

//是否修剪子视图,用于提升大列表的滚动性能,配合hiden使用。默认情况下启用。
removeClippedSubviews: React.PropTypes.bool,

//竖直方向上组头是否吸顶悬浮
stickySectionHeadersEnabled: React.PropTypes.bool,

//一个子视图下标的数组,用于决定哪些成员会在滚动之后固定在屏幕顶端。举个例子,传递stickyHeaderIndices={[0]}会让第一个成员固定在滚动视图顶端。这个属性不能和horizontal={true}一起使用
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number).isRequired,

//指示是否应呈现空节头的标志。 在将来的版本中,默认情况下将呈现空白部分标题,并且该标志将被弃用。如果不希望显示空节,则应将它们的索引从sectionID对象中排除。
enableEmptySections: PropTypes.bool,

该组件的常用方法如下:

//导出一些数据,返回值是一个对象
//包含:内容长度、总行数、渲染行数、可见行数
getMetrics: function() {
    return {
      contentLength: this.scrollProperties.contentLength,
      totalRows: (this.props.enableEmptySections ? this.props.dataSource.getRowAndSectionCount() : this.props.dataSource.getRowCount()),
      renderedRows: this.state.curRenderedRowsCount,
      visibleRows: Object.keys(this._visibleRows).length,
    };
  }

//滚动到执行位置,设置滚动偏移量
scrollTo: function(...args: Array<mixed>)

//滚动到底部
scrollToEnd: function(options?: ?{ animated?: ?boolean })

 

三、ListViewDataSource

ListView组件中的数据源就是ListViewDataSource这个实例对象。开发者可以为节标题和行提供自定义提取和`hasChanged`函数。 如果不存在,系统则将使用defaultGetRowData和defaultGetSectionHeaderData函数提取数据。默认提取器期望以下形式之一的数据:

//对象嵌套对象,每一组sectionID有多个行rowID, 每一个rowID对应一个数据rowData
{ sectionID_1: { rowID_1: <rowData1>, ... }, ... }

//对象嵌套数组,每一组sectionID对应一个数组,数组中包含该组所有行的数据rowData
{ sectionID_1: [ <rowData1>, <rowData2>, ... ], ... }

//数组嵌套数组,每一个元素是一个数组,这个数组就是当前组的所有行的数据rowData
[ [ <rowData1>, <rowData2>, ... ], ... ] 

使用ListViewDataSource进行实例化时,构造函数内部会根据传入的数据源进行不同组件数据的提取和渲染配置。如下:

//获取行数据
//dataBlob:传入的数据源
//sectionID: 组标识
//rowID:行标识
getRowData(dataBlob, sectionID, rowID);

//获取组头数据
//dataBlob:传入的数据源
//sectionID: 组标识
getSectionHeaderData(dataBlob, sectionID);

//决定state发生改变时渲染每一行的条件
rowHasChanged(prevRowData, nextRowData);

//决定state发生改变时渲染每一组的条件
sectionHeaderHasChanged(prevSectionData, nextSectionData);

这几个函数都是在构造函数内调用的,那么构造函数接收的参数类型必须满足它们对数据的提取,参数是一个对象,格式如下:

//默认的行数据
function defaultGetRowData(
    dataBlob: any,
    sectionID: number | string,
    rowID: number | string
): any {
    return dataBlob[sectionID][rowID];
}

//默认的组头数据
function defaultGetSectionHeaderData(
    dataBlob: any,
    sectionID: number | string
): any {
    return dataBlob[sectionID];
}

//箭头函数,返回布尔值,决定数据变化时页面渲染的条件
type differType = (data1: any, data2: any) => bool;

//参数是一个对象
type ParamType = {
    rowHasChanged: differType,
    getRowData?: ?typeof defaultGetRowData,
    sectionHeaderHasChanged?: ?differType,
    getSectionHeaderData?: ?typeof defaultGetSectionHeaderData,
}

要更新数据源中的数据,请使用cloneWithRows或者cloneWithRowsAndSections)。数据源中的数据是不可变的,所以您不能直接修改它。克隆方法吸收新数据并计算每行的差异,以便ListView知道是否重新呈现它。在这个例子中,一个组件接收数据块,由其处理_onDataArrived,将新数据连接到旧数据并更新数据源。我们concat用来创建一个新的数组this._data例如,使用变异this._data.push(newRowData)将是一个错误。_rowHasChanged了解行数据的形状并知道如何有效地比较它。

//比较元素,设置页面默认刷新
getInitialState: function() {
  var ds = new ListView.DataSource({rowHasChanged: this._rowHasChanged});
  return {ds};
},

//接收数据并更新state
_onDataArrived(newData) {
  this._data = this._data.concat(newData);
  this.setState({
    ds: this.state.ds.cloneWithRows(this._data)
  });
}

 

四、使用

说实话,对我这个新手来说,这个组件真TMD的难用,远比FlatList组件复杂,API太费事了,前前后后看了几遍还是云里雾里的。还好会简单使用一下,示例如下:

(1)不分组的列表【ListTableView.js】

import React, { Component } from 'react';
import {
    View,
    StyleSheet,
    ListView,
    Image,
    Text,
    TouchableOpacity
} from 'react-native';

const data = [
    {
        icon:require('../image/flower1.png'),
        title:'玫瑰',
        desc:'送给情人的象征'
    },
    {
        icon:require('../image/flower2.png'),
        title:'青草芽',
        desc:'代表蓬勃的生命力'
    },
    {
        icon:require('../image/flower3.png'),
        title:'向日葵',
        desc:'永远那么积极向上,追求太阳'
    },
    {
        icon:require('../image/flower1.png'),
        title:'玫瑰',
        desc:'送给情人的象征'
    },
    {
        icon:require('../image/flower2.png'),
        title:'青草芽',
        desc:'代表蓬勃的生命力'
    },
    {
        icon:require('../image/flower3.png'),
        title:'向日葵',
        desc:'永远那么积极向上,追求太阳'
    }
];

export default class ListTableView extends Component{

    //数据源初始化
    //rowHasChanged表示:如果state发生改变,界面是否需要更新,true表示需要更新
    //cloneWithRows表示:将数据源克隆一份,data可以是本地数据,也可以是网络数据
    constructor(){
        super();
        const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
        this.state = {
            dataSource: ds.cloneWithRows(data),
        };
    }

    //渲染cell组件,rowData为每一行cell的数据
    _renderRow(rowData){
        return (
            <View style={styles.row}>
                <TouchableOpacity style={styles.flex} onPress={this.clickRowEvent.bind(this,rowData)}>
                    <Image style={styles.icon} source={rowData.icon}/>
                    <View>
                        <Text  style={styles.title}>{rowData.title}</Text>
                        <Text  style={styles.des}>{rowData.desc}</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }

    //渲染分割线
    //sectionID:每一组的标识
    //rowID:某一组每一行的标识
    //adjacentRowHighlighted:相邻的行是否高亮
    _renderSeparator(sectionID, rowID, adjacentRowHighlighted){
        return (
            <View key={rowID} style={styles.line}/>
        )
    }

    //渲染表头
    _renderHeader(){
        return (
            <View style={[styles.header,styles.center]}>
                <Text style={styles.tipFont}>表头</Text>
            </View>
        )
    }

    //渲染表尾
    _renderFooter(){
        return (
            <View style={[styles.footer,styles.center]}>
                <Text style={styles.tipFont}>表尾</Text>
            </View>
        )
    }

    //获取点击时行的数据
    clickRowEvent(rowData) {
        alert('title:'+rowData.title+" "+"desc:"+rowData.desc)
    }

    //渲染ListView
    render(){
        return (
            <ListView
                dataSource={this.state.dataSource}
                renderRow={(rowData) => this._renderRow(rowData)}
                //initialListSize={2}
                renderSeparator={ (sectionID, rowID, adjacentRowHighlighted) =>
                    this._renderSeparator(sectionID,rowID,adjacentRowHighlighted) }
                renderHeader={ () => this._renderHeader() }
                renderFooter={ () => this._renderFooter() }
            />
        )
    }
}

const styles = StyleSheet.create({
    flex: {
        display: 'flex',
        flexDirection: 'row',
        flex: 1
    },
    row: {
        height: 80
    },
    icon: {
        marginTop: 10,
        marginLeft: 20,
        width: 60,
        height: 60,
        borderRadius: 5
    },
    title: {
        marginTop: 15,
        marginLeft:10,
        fontSize: 20,
        color: 'black'
    },
    des: {
        marginTop: 10,
        marginLeft: 10,
        fontSize: 15,
        color: 'black'
    },
    line: {
        height: 1,
        backgroundColor: '#F0F0F0'
    },
    header: {
        height: 100,
        backgroundColor: 'red'
    },
    footer: {
        height: 100,
        backgroundColor: 'purple'
    },
    tipFont: {
        fontSize: 30,
        color: 'white'
    },
    center: {
        justifyContent: 'center',
        alignItems: 'center'
    }
});
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';

import {
    AppRegistry,
    StyleSheet,
    View
} from 'react-native';

import ListTableView from './src/ListTableView'

export default class ReactNativeDemo extends Component {

    render() {
        return (
            <View style={[styles.flex,styles.bgColor]}>
                <ListTableView/>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    flex: {
        flex: 1
    },
    bgColor: {
      backgroundColor: 'white'
    },
    center: {
        alignItems: 'center',
        justifyContent: 'center',
    }
});

AppRegistry.registerComponent('ReactNativeDemo', () => ReactNativeDemo);
View Code

 

(2)分组的列表【ListTableView.js】

import React, { Component } from 'react';
import {
    View,
    StyleSheet,
    ListView,
    Image,
    Text,
    TouchableOpacity
} from 'react-native';

const data = [
    [
        {
            icon:require('../image/flower1.png'),
            title:'玫瑰',
            desc:'送给情人的象征'
        },
        {
            icon:require('../image/flower2.png'),
            title:'青草芽',
            desc:'代表蓬勃的生命力'
        },
        {
            icon:require('../image/flower3.png'),
            title:'向日葵',
            desc:'永远那么积极向上,追求太阳'
        }
    ],
    [
        {
            icon:require('../image/flower1.png'),
            title:'玫瑰',
            desc:'送给情人的象征'
        },
        {
            icon:require('../image/flower2.png'),
            title:'青草芽',
            desc:'代表蓬勃的生命力'
        },
        {
            icon:require('../image/flower3.png'),
            title:'向日葵',
            desc:'永远那么积极向上,追求太阳'
        }
    ]
];

export default class ListTableView extends Component{

    //数据源初始化
    constructor(props){
        super(props);

        //定义变量:组头标识数组、行标识数组、组头和行数据(根据标识存储)
        let sectionIDs = [];
        let rowIDs = [];
        let dataBlob = [];

        // 遍历每一组
        for (let i = 0; i<data.length; i++){
            
            //存储组头标识和数据
            sectionIDs.push(i);
            dataBlob[i] = `第${i}组`;

            // 遍历每一行
            const sectionData = data[i];
            rowIDs[i] = [];
            for (let j = 0; j<sectionData.length; j++){
                
                //存储行标识和数据
                rowIDs[i].push(j);
                dataBlob[i + ':' + j] = sectionData[j];
            }
        }

        //ListViewDataSource实例化对象
        //分别提取组头数据和行数据、设置state更改时页面自动渲染
        const ds = new ListView.DataSource({
            getSectionHeaderData : (dataBlob, sectionID) => dataBlob[sectionID],
            getRowData : (dataBlob, sectionID, rowID) => dataBlob[sectionID + ':' + rowID],
            rowHasChanged: (r1, r2) => r1 !== r2,
            sectionHeaderHasChanged : (s1, s2) => s1 !== s2
        });

        //设置state
        this.state = {
            dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
        };
    }

    //渲染cell组件,rowData为每一行cell的数据
    _renderRow(rowData, sectionID, rowID){
        return (
            <View style={styles.row}>
                <TouchableOpacity style={styles.flex} onPress={this.clickRowEvent.bind(this, rowData, sectionID, rowID)}>
                    <Image style={styles.icon} source={rowData.icon}/>
                    <View>
                        <Text  style={styles.title}>{rowData.title}</Text>
                        <Text  style={styles.des}>{rowData.desc}</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }

    //渲染分割线
    //sectionID:每一组的标识
    //rowID:某一组每一行的标识
    //adjacentRowHighlighted:相邻的行是否高亮
    _renderSeparator(sectionID, rowID, adjacentRowHighlighted){
        return (
            <View key={rowID} style={styles.line}/>
        )
    }

    //渲染表头
    _renderHeader(){
        return (
            <View style={[styles.header,styles.center]}>
                <Text style={styles.header_footer_Font}>表头</Text>
            </View>
        )
    }

    //渲染表尾
    _renderFooter(){
        return (
            <View style={[styles.footer,styles.center]}>
                <Text style={styles.header_footer_Font}>表尾</Text>
            </View>
        )
    }

    //渲染组头
    _renderSectionHeader(sectionData, sectionID){
        return(
            <View style={[styles.flex,styles.sectionHeader,styles.center]}>
                <Text style={styles.section_header_Font}>{sectionData}</Text>
            </View>
        );
    }

    //获取点击时行的数据
    clickRowEvent(rowData, sectionID, rowID) {
        alert('title:'+rowData.title+" "+"desc:"+rowData.desc)
    }

    //渲染ListView
    render(){
        return (
            <ListView
                dataSource={this.state.dataSource}
                renderRow={(rowData, sectionID, rowID) => this._renderRow(rowData, sectionID, rowID)}
                renderSeparator={ (sectionID, rowID, adjacentRowHighlighted) =>
                    this._renderSeparator(sectionID,rowID,adjacentRowHighlighted) }
                renderHeader={ () => this._renderHeader() }
                renderFooter={ () => this._renderFooter() }
                renderSectionHeader={ (sectionData, sectionID) => this._renderSectionHeader(sectionData, sectionID) }

            />
        )
    }
}

const styles = StyleSheet.create({
    flex: {
        display: 'flex',
        flexDirection: 'row',
        flex: 1
    },
    row: {
        height: 80
    },
    icon: {
        marginTop: 10,
        marginLeft: 20,
        width: 60,
        height: 60,
        borderRadius: 5
    },
    title: {
        marginTop: 15,
        marginLeft:10,
        fontSize: 20,
        color: 'black'
    },
    des: {
        marginTop: 10,
        marginLeft: 10,
        fontSize: 15,
        color: 'black'
    },
    line: {
        height: 1,
        backgroundColor: '#F0F0F0'
    },
    header: {
        height: 100,
        backgroundColor: 'red'
    },
    footer: {
        height: 100,
        backgroundColor: 'purple'
    },
    header_footer_Font: {
        fontSize: 30,
        color: 'white'
    },
    center: {
        justifyContent: 'center',
        alignItems: 'center'
    },
    sectionHeader: {
        height: 50,
        backgroundColor: '#EEE'
    },
    section_header_Font: {
        fontSize: 25,
        color: 'purple',
        paddingLeft: 10
    }
});
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';

import {
    AppRegistry,
    StyleSheet,
    View
} from 'react-native';

import ListTableView from './src/ListTableView'

export default class ReactNativeDemo extends Component {

    render() {
        return (
            <View style={[styles.flex,styles.bgColor]}>
                <ListTableView/>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    flex: {
        flex: 1
    },
    bgColor: {
      backgroundColor: 'white'
    },
    center: {
        alignItems: 'center',
        justifyContent: 'center',
    }
});

AppRegistry.registerComponent('ReactNativeDemo', () => ReactNativeDemo);
View Code

 

posted @ 2020-01-03 16:39  XYQ全哥  阅读(629)  评论(0编辑  收藏  举报