React Native学习之ListView组件

ListView组件

  • Android 原生 ListView 实现(MVC方式):ListView Adapter List集合数据
  • iOS 原生 ListView 实现:UITableView(sectionId、rowId、rowData)

ListView组件是React Native中一个比较核心的组件,用途非常的广。该组件设计用来高效的展示垂直滚动的数据列表。

ListView最简单的API就是创建一个ListView.DataSource对象,同时给该对象传入一个简单的数据集合。并且使用数据源(data source)实例化一个ListView组件,定义一个renderRow回调方法(该方法的参数是一个数组),该renderRow方法会返回一个可渲染的组件(即列表的每一行的item)。

由于平台差异性,React Native 中的滚动列表组件 ListView 并没有直接映射为 Android 中的 ListView 或 iOS 中的 UITableView,而是在ScrollView 的基础上使用 JS 做了一次封装。滚动体验部分由 Native 负责,而 React 部分则专注于组件何时渲染、如何渲染等问题。

ListView 数据源结构

数据源默认的格式有三个维度:

  • 第一个维度是 sectionId ,标识属于哪一段, 可以手动指定或隐式地使用数组索引或对象的 key 值;
  • 第二个维度是 rowId ,标识某个数据段下的某一个行,同样可以手动指定或隐式地使用数组索引或对象的 key 值;
  • 第三个维度是具体的数据对象,根据实际的需要而定。

需要注意的是,以上只是默认的数据格式 ,如果不符合实际的需求, 完全可以使用自定义的数据结构。唯一的区别就是需要额外指定给 ListView 数据源中哪些是 id,哪些是 rowData。

ListView数据格式

DataSource 数据源

DataSource 的构造函数接收以下4个参数(都是可选):

  • rowHasChanged:用于在数据变化的时候,计算出变化的部分,在更新时只渲染脏数据;
  • sectionHeaderHasChanged:同理,在列表带分段标题时需要实现;
  • getRowData/getSectionHeaderData: 如果遵循默认的数据源格式,这两个方法就没有必要实现,用内部默认的即可;而当数据源格式是自定义时,需要手动实现这两个方法。

DataSource 的初始化一般在 constructor 方法中

数据源确定后,下一个工作就是列表的渲染。在渲染时发挥重要作用的是 renderRow 属性,它接收数据源中保存的数据对象,并通过返回值确定该行该如何进行展现。我们可以对所有行统一进行展现,也可以根据里面的字段做出不同的展现。在列表包含 sectionHeader 时,还需要实现 renderSectionHeader 方法。

注意:要更新datasource中的数据,请(每次都重新)调用cloneWithRows方法(如果用到了section,则对应调用cloneWithRowsAndSections方法)。数据源中的数据本身是不可修改的,所以请勿直接尝试修改。clone方法会自动提取新数据并进行逐行对比(使用rowHasChanged方法中的策略),这样ListView就会知道哪些行需要重新渲染。

ListView组件基础范例

范例1:创建并初始化ListView方式一

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

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

class RNAPP extends Component {
    // 初始化模拟数据
    constructor(props) {
        super(props);
        const dataSource = new ListView.DataSource({rowHasChanged: (row1, row2) => row1 !== row2});
        this.state = {
            dataSource: dataSource.cloneWithRows([
                'John', 'Joel', 'James', 'Jimmy', 'Jackson', 'Jillian', 'Julie', 'Devin'
            ])
        };
    }

    render() {
        return (
            <View style={{paddingTop: 22}}>
                <ListView
                    dataSource = {this.state.dataSource}
                    renderRow = {(rowData) => <Text>{rowData}</Text>}
                    />
            </View>
        );
    }
}

AppRegistry.registerComponent('RNAPP', () => RNAPP);

范例2:创建并初始化ListView方式二

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 */

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    ScrollView,
    Image,
    ListView,
    TouchableHighlight,
    RecyclerViewBackedScrollView,
    View,
} from 'react-native';

export default class RNAPP extends Component {
    constructor(props) {
        super(props);
        this.state = {
            dataSource:new ListView.DataSource({
                rowHasChanged:(row1,row2)=> row1 !== row2,
            }),
        };
    }

    componentWillMount() {
        this.dsfetchData();
    }

    render() {
        return(
            <ListView
                dataSource = {this.state.dataSource}
                renderRow = {(rowData) => <Text>{rowData}</Text>}
                style = {styles.listView}
                />
        );
    }

    dsfetchData(){
        this.setState({
            dataSource: this.state.dataSource.cloneWithRows(['John', 'Joel', 'James', 'Jimmy', 'Jackson', 'Jillian', 'Julie', 'Devin']),
        });
    }
}

const styles = StyleSheet.create({
    listView: {
        paddingTop: 20,
        backgroundColor: '#F5FCFF',
    },
});

AppRegistry.registerComponent('RNAPP', () => RNAPP);

范例3:ListView数据源结构

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 */

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    ScrollView,
    Image,
    ListView,
    TouchableHighlight,
    RecyclerViewBackedScrollView,
    View,
} from 'react-native';

export default class RNAPP extends Component{
    constructor(props) {
        super(props);
        this.state = {
            dataSource:new ListView.DataSource({
                rowHasChanged:(row1,row2)=> row1 !== row2,
            }),
        };
    }

    render() {
        return(
            <ListView
                dataSource = {this.state.dataSource}
                renderRow = {this._renderRow}
                style = {styles.listView}
                />
        );
    }

    _renderRow(rowData, sectionID, rowID) {
        return (
            <View>
                <Text>sectionID: {sectionID}</Text>
                <Text>rowID: {rowID}</Text>
                <Text>rowData: {rowData}</Text>
            </View>
        );
    }

    componentDidMount() {
        this.dsfetchData();
    }

    dsfetchData() {
        this.setState({
            dataSource: this.state.dataSource.cloneWithRows(['John', 'Joel', 'James', 'Jimmy', 'Jackson', 'Jillian', 'Julie', 'Devin']),
        });
    }
}

const styles = StyleSheet.create({
    listView: {
        paddingTop: 20,
        backgroundColor: '#F5FCFF',
    },
});

AppRegistry.registerComponent('RNAPP', () => RNAPP);

范例4:ListView数据填充

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 */

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    ScrollView,
    Image,
    ListView,
    TouchableHighlight,
    RecyclerViewBackedScrollView,
    View,
} from 'react-native';

//var MOCKED_MOVIES_DATA = [
//    {title: '标题', year: '2015', posters: {thumbnail: 'http://i.imgur.com/UePbdph.jpg'}},
//];

/**
 * 为了避免骚扰,使用一个样例数据来替代Rotten Tomatoes的API请求,这个样例数据放在React Native的Github库中。
 */
var REQUEST_URL = 'https://raw.githubusercontent.com/facebook/react-native/master/docs/MoviesExample.json';

class RNAPP extends Component{

    constructor(props) {
        super(props);
        this.state = {
            loaded: false,
            dataSource: new ListView.DataSource({
                rowHasChanged: (row1,row2) => row1 !== row2,
            }),
        };
    }

    render() {
        if (!this.state.loaded) {
            //如果movies==null,即初始情况,则渲染加载视图
            return this.renderLoadingView();
        }

        //从网络上获取数据的情况
        //var movie = this.state.movies[0];
        //return this.renderMovie(movie);

        return(
            <ListView
                dataSource = {this.state.dataSource}
                renderRow = {this.renderMovie}
                style = {styles.listView}
                />
        );
    }

    renderLoadingView() {
        return (
            <View style={styles.container}>
                <Text>
                    正在网络上获取电影数据……
                </Text>
            </View>
        );
    }

    //渲染一个电影信息
    renderMovie(movie) {
        return (
            <View style={styles.container}>
                <Image
                    source={{uri: movie.posters.thumbnail}}
                    style={styles.thumbnail}
                    />
                <View style={styles.rightContainer}>
                    <Text style={styles.title}>{movie.title}</Text>
                    <Text style={styles.year}>{movie.year}</Text>
                </View>
            </View>
        );
    }

    componentDidMount() {
        this.fetchData();
    }

    //在React的工作机制下,setState实际上会触发一次重新渲染的流程,此时render函数被触发,发现this.state.movies不再是null
    fetchData() {
        fetch(REQUEST_URL)
            .then((response) => response.json())
            .then((responseData) => {
                this.setState({
                    dataSource: this.state.dataSource.cloneWithRows(responseData.movies),
                    loaded: true,
                });
            })
            .done();
        //调用done(),可以抛出异常而不是简单忽略
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        flexDirection: 'row',
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
    },
    listView: {
        paddingTop: 20,
        backgroundColor: '#F5FCFF',
    },
    thumbnail: {
        width: 53,
        height: 81,
    },
    //使rightContainer在父容器中占据Image之外剩下的全部空间
    rightContainer: {
        flex: 1,
    },
    title: {
        fontSize: 20,
        marginBottom: 8,
        textAlign: 'left',
    },
    year: {
        textAlign: 'left',
    },
});

AppRegistry.registerComponent('RNAPP', () => RNAPP);

ListView组件自定义数据结构

dataBlob 数据

dataBlob:The dataBlob is a data structure (generally an object) that holds all the data powering the ListView

dataBlob 包含ListView所需的所有数据(section header 和 rows),在ListView渲染数据时,使用 getSectionData 和 getRowData 来渲染每一行数据。dataBlob 的 key 值包含 sectionID 和 rowId。

DataBlob

sectionIDs[]:用于标识每组section

sectionIDs

rowIDs[]:用于描述每个 section 里的每行数据的位置及是否需要渲染。在ListView渲染时,会先遍历 rowIDs 获取到对应的 dataBlob 数据。

rowData 的索引方式:dataBlob[sectionId:rowId]

rowIDs

React Native ListView sticky效果实现

Android 片段头sticky失效原因:

  • 1) Found this out by accident, but if you put your ListView inside a ScrollView, the headers will not be sticky
  • 2) iOS RCTScrollView implements sticky headers,而Android没有
  • 3) ListView的Android版本不支持sticky-header特性
  • 4) Android 原生sticky效果控件可使用StickyScrollViewItems替代:https://github.com/emilsjolander/StickyScrollViewItems
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 */

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    ScrollView,
    Image,
    ListView,
    TouchableHighlight,
    RecyclerViewBackedScrollView,
    View,
} from 'react-native';

class RNAPP extends Component{
    constructor(props) {
        super(props);
        var getSectionData = (dataBlob, sectionID) => {
            return dataBlob[sectionID];
        };

        var getRowData = (dataBlob, sectionID, rowID) => {
            return dataBlob[sectionID + ':' + rowID];
        };

        this.state = {
            loaded: false,
            dataSource: new ListView.DataSource({
                getSectionData: getSectionData,
                getRowData: getRowData,
                rowHasChanged: (row1,row2)=> row1 !== row2,
                sectionHeaderHasChanged: (s1, s2) => s1 !== s2
            }),
        };
    }

    render() {
        if (!this.state.loaded) {
            return this.renderLoadingView();
        }

        return(
            <View style={styles.container}>
                <View style={styles.header}>
                    <Text style={styles.headerText}>User List</Text>
                </View>

                <ListView
                    dataSource = {this.state.dataSource}
                    style = {styles.listview}
                    renderRow = {this.renderRow}
                    renderSectionHeader = {this.renderSectionHeader}
                    />
            </View>
        );

    }

    renderSectionHeader(sectionData, sectionID) {
        return (
            <View style={styles.section}>
                <Text style={styles.text}>片头-公司名:{sectionData}</Text>
            </View>
        );
    }

    renderRow (rowData, sectionID, rowID) {
        return (
            <View style={styles.rowStyle}>
                <Text style={styles.rowText}>{rowData.name.title} {rowData.name.first} {rowData.name.last}-{sectionID}-{rowID}</Text>
            </View>
        );
    }

    renderLoadingView() {
        return (
            <View style={styles.loadingpage}>
                <Text>
                    正在网络上获取电影数据……
                </Text>
            </View>
        );
    }

    componentDidMount() {
        this.fetchData();
    }

    fetchData () {
        var responseData={
            "results" : [
                {
                    "organization":"阿里巴巴",
                    "id": 1234812,
                    "users": [{"user":{"gender":"female","name":{"title":"title1","first":"marga","last":"seegers"},"md5":"589a574553250be33f3b1170624ad2d1"}},
                              {"user":{"gender":"female","name":{"title":"title2","first":"kanne","last":"telar"},"md5":"5cbc80332e3523dd9d90cc116daf9d8e"}},
                              {"user":{"gender":"female","name":{"title":"title3","first":"livia","last":"stout"},"md5":"7d8c37997f09c122794651b1ec841491"}},
                              {"user":{"gender":"female","name":{"title":"title4","first":"dian","last":"grinwis"},"md5":"3956e950cbbf896533aef74c2c82cd89"}},
                              {"user":{"gender":"female","name":{"title":"title5","first":"sky","last":"vander"},"md5":"8d3c0c7c2338280d383815eeb93509f6"}},
                              {"user":{"gender":"male","name":{"title":"title6","first":"davi","last":"kero"},"md5":"c84ceab40f165ace6d8e91232a6905ce"}},
                              {"user":{"gender":"male","name":{"title":"title7","first":"jurren","last":"kempen"},"md5":"c56b46820b77e86cbfb7aaa519e9c4dc"}},
                              {"user":{"gender":"male","name":{"title":"title8","first":"alfons","last":"opden"},"md5":"8ad86f9082ffc5d59d97ad65da740b76"}},
                              {"user":{"gender":"male","name":{"title":"title9","first":"oene","last":"stroo"},"md5":"e756d75093bb3a65e5a6561eb04e11a1"}},
                              {"user":{"gender":"male","name":{"title":"title10","first":"yanick","last":"leuveren"},"md5":"dbcdaf6a92e48e2dd0b87053a6c0a460"}}]
                },
                {
                    "organization": "腾讯",
                    "id": 1235513,
                    "users": [{"user":{"gender":"female","name":{"title":"title1","first":"nicole","last":"sullivan"},"md5":"9bc08bb89d4b7163734e5a82bc1c913f"}},
                              {"user":{"gender":"male","name":{"title":"title2","first":"danny","last":"ellis"},"md5":"5084736a5dc54b3d01ab466cbe5a2bfd"}},
                              {"user":{"gender":"male","name":{"title":"title3","first":"richard","last":"morgan"},"md5":"5cf586edf1c296d439fc8c760ce0da78"}},
                              {"user":{"gender":"male","name":{"title":"title4","first":"adrian","last":"davies"},"md5":"17ae93548bf33c9fa6765339802eb9f7"}},
                              {"user":{"gender":"male","name":{"title":"title5","first":"charlie","last":"snyder"},"md5":"62bfb188b82a370c81d70c41b08f44fa"}},
                              {"user":{"gender":"male","name":{"title":"title6","first":"donald","last":"fitzpatrick"},"md5":"9ee42fe6081031aa5d798a1afcfeee19"}},
                              {"user":{"gender":"male","name":{"title":"title7","first":"adrian","last":"hawkins"},"md5":"f466f40834aa260d6ab86bee912fc443"}},
                              {"user":{"gender":"female","name":{"title":"title8","first":"caitlin","last":"robertson"},"md5":"10eaa195e29753ca6686755fceddef53"}},
                              {"user":{"gender":"female","name":{"title":"title9","first":"grace","last":"johnston"},"md5":"d91f66fe034218a2975ebd7db4738a12"}},
                              {"user":{"gender":"female","name":{"title":"title10","first":"rosie","last":"douglas"},"md5":"648b75b00311daa164439d898be76cf1"}}]
                },
                {
                    "organization": "百度",
                    "id": 1237141,
                    "users": [{"user":{"gender":"female","name":{"title":"title1","first":"joyce","last":"little"},"md5":"5b3318e717934073039028c9e5125896"}},
                              {"user":{"gender":"female","name":{"title":"title2","first":"ann","last":"gonzales"},"md5":"bc7a5338eb89da8b3044326277e7a9f1"}},
                              {"user":{"gender":"female","name":{"title":"title3","first":"genesis","last":"olson"},"md5":"f296f1cdcf1a901fc0d819e87f9db287"}},
                              {"user":{"gender":"female","name":{"title":"title4","first":"peyton","last":"simmmons"},"md5":"9835af99ec882eb8f29a5620d12181d6"}},
                              {"user":{"gender":"female","name":{"title":"title5","first":"ruby","last":"palmer"},"md5":"752c778c0f0b6d68cd61b636c75d1ddd"}},
                              {"user":{"gender":"female","name":{"title":"title6","first":"robin","last":"perkins"},"md5":"343d690344f146b8b9b0fecd0470e8bc"}},
                              {"user":{"gender":"male","name":{"title":"title7","first":"steve","last":"dean"},"md5":"353ec55e24648680ee30caeb32a8eb82"}},
                              {"user":{"gender":"male","name":{"title":"title8","first":"jessie","last":"barnett"},"md5":"8405ec6770a49f42fa00f94c2938d963"}},
                              {"user":{"gender":"female","name":{"title":"title9","first":"ann","last":"berry"},"md5":"140c33a7c143de515466ae0d6ff08b3a"}},
                              {"user":{"gender":"female","name":{"title":"title10","first":"hailey","last":"cunningham"},"md5":"7b8d57609043dc4947d1a65993ae6833"}}]
                },
                {
                    "organization": "小米",
                    "id": 1727272,
                    "users": [{"user":{"gender":"male","name":{"title":"title1","first":"tommy","last":"stanley"},"md5":"2155c170ed25a160f6202289bbb26aa5"}},
                              {"user":{"gender":"female","name":{"title":"title2","first":"rose","last":"gregory"},"md5":"ea3043ea9b36202218760e1e20c1a698"}},
                              {"user":{"gender":"male","name":{"title":"title3","first":"gilbert","last":"jennings"},"md5":"b9cec0adf70102f2f2864babd802c52c"}},
                              {"user":{"gender":"male","name":{"title":"title4","first":"alexander","last":"howell"},"md5":"1e27d0c330372b8dc2e88dc6f34d9ebf"}},
                              {"user":{"gender":"female","name":{"title":"title5","first":"eliza","last":"perry"},"md5":"c9b82b35a31e2b117fd8fe296af9a436"}},
                              {"user":{"gender":"female","name":{"title":"title6","first":"leah","last":"gregory"},"md5":"99561739ed756ef2bd633f7c22d5db77"}},
                              {"user":{"gender":"female","name":{"title":"title7","first":"catherine","last":"lee"},"md5":"78a29d0a21a66bb3e816d96ddb896c47"}},
                              {"user":{"gender":"female","name":{"title":"title8","first":"amelia","last":"obrien"},"md5":"76111906ba52da4ef50ac73871fa0da1"}},
                              {"user":{"gender":"female","name":{"title":"title9","first":"fiona","last":"may"},"md5":"9a35923e434be652e5441a2c49509dc6"}},
                              {"user":{"gender":"male","name":{"title":"title10","first":"earl","last":"banks"},"md5":"8071292ec92ad705c6be09b573d8bd8c"}}]
                }
            ]
        };

        var organizations = responseData.results,
            length = organizations.length,
            dataBlob = {},
            sectionIDs = [],
            rowIDs = [],
            organization,
            users,
            userLength,
            user,
            i,
            j;

        for (i = 0; i < length; i++) {
            organization = organizations[i];
            sectionIDs.push(organization.id);

            dataBlob[organization.id] = organization.organization;

            users = organization.users;
            userLength = users.length;

            rowIDs[i] = [];  //rowIDs是一个二维数组

            for(j = 0; j < userLength; j++) {
                user = users[j].user;
                rowIDs[i].push(user.md5);  //二维数组放的都是 md5
                dataBlob[organization.id + ':' + user.md5] = user;
            }
        }

        this.setState({
            dataSource: this.state.dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
            loaded: true
        });
    }
}

const styles = StyleSheet.create({
    loadingpage:{
        flex: 1,
        flexDirection: 'row',
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
    },
    container: {
        flex: 1
    },
    header: {
        height: 60,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#3F51B5',
        flexDirection: 'column',
        paddingTop: 25
    },
    headerText: {
        fontWeight: 'bold',
        fontSize: 20,
        color: 'white'
    },
    text: {
        color: 'white',
        paddingHorizontal: 8,
        fontSize: 16
    },
    rowStyle: {
        paddingVertical: 20,
        paddingLeft: 16,
        borderTopColor: 'white',
        borderLeftColor: 'white',
        borderRightColor: 'white',
        borderBottomColor: '#E0E0E0',
        borderWidth: 1
    },
    rowText: {
        color: '#212121',
        fontSize: 16
    },
    subText: {
        fontSize: 14,
        color: '#757575'
    },
    section: {
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'flex-start',
        padding: 6,
        backgroundColor: '#2196F3'
    }
});

AppRegistry.registerComponent('RNAPP', () => RNAPP);

ListView组件高级特性

分页

当数据量很大的时候如何分页加载,这种情形分两种情况考虑:

  • 数据一次性拿到,边滚动边加载
  • 数据不是一次性拿到,而是有可能分屏取数据

对于第一种情况,在 ListView 内部其实已经做了分页的处理:

ListView 内部通过 curRenderedRowsCount 状态保存已渲染的行数;初始状态下,要加载的数据条数等于 initialListSize (默认为 10 条);在滚动时检测当前滚动的位置和最底部的距离,如果小于 scrollRenderAheadDistance (默认为 1000),就更新 curRenderedRowsCount,在它原有值基础上加 pageSize 个(默认为 1 条);由于属性变化,触发 ListView 重新 render。在渲染过程中,curRenderedRowsCount 起到截断数据的作用,React 的 diff 算法使得只有新加入的数据才会渲染到了界面上。整个过程类似于 Web 端懒加载机制,即 每次在和底部的距离达到一个阈值时,加载接下来的 pageSize 个数据 。

对于第二种情况,ListView 提供了相关的属性:

onEndReachedThreshold:在滚动即将到达底部时触发;
onEndReached:在已经到达底部时触发;

我们可以在这两个方法中调用接口去拿数据,取到数据后再更新数据源。

多列 (Grid 效果)

很多页面中的列表并非单列的,通过布局即可达到网格效果。ListView 并没有强制要求一个 rowData 在展示时一定要占满一行,在多列的情况下,我们适时调整每个 rowData 占据的宽度即可。

由于 React Native 使用 Flexbox 进行布局,给ListView设置属性contentContainerStyle;在实现多列时,主要用到的是 flexWrap: wrap 属性:它的效果类似于 float,即水平地排列每一项,当放不下时进行折行处理。在设置每行视图占据一半宽度后即可达到两列的效果,多列的类似。

ListView多列-Grid效果

Grid 效果范例:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 */

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    ScrollView,
    Image,
    ListView,
    TouchableHighlight,
    RecyclerViewBackedScrollView,
    View,
} from 'react-native';

/**
 * 为了避免骚扰,使用一个样例数据来替代Rotten Tomatoes的API请求,这个样例数据放在React Native的Github库中。
 */
var REQUEST_URL = 'https://raw.githubusercontent.com/facebook/react-native/master/docs/MoviesExample.json';

class HomeUI extends Component{
    constructor(props) {
        super(props);
        this.state = {
            loaded: false,
            dataSource: new ListView.DataSource({
                rowHasChanged: (row1,row2) => row1 !== row2,
            }),
        };
    }

    render() {
        if (!this.state.loaded) {
            return this.renderLoadingView();
        }

        //从网络上获取数据的情况
        //var movie = this.state.movies[0];
        //return this.renderMovie(movie);

        return(
            <ListView
                dataSource = {this.state.dataSource}
                renderRow = {this.renderMovie}
                contentContainerStyle = {styles.list}
                style = {styles.listView}
                />
        );
    }

    renderLoadingView() {
        return (
            <View style={styles.container1}>
                <Text>
                    正在网络上获取电影数据……
                </Text>
            </View>
        );
    }

    _pressText() {
        alert('点击时间');
    }

    //渲染一个电影信息
    renderMovie(movie) {
        return (
            <View style={styles.container}>
                <Image
                    source={{uri: movie.posters.thumbnail}}
                    style={styles.thumbnail}
                    />
                <Text style={styles.year} onPress={this._pressText}>年份{movie.year}</Text>
            </View>
        );
    }

    componentDidMount() {
        this.fetchData();
    }

    //在React的工作机制下,setState实际上会触发一次重新渲染的流程,此时render函数被触发,发现this.state.movies不再是null
    fetchData() {
        fetch(REQUEST_URL)
            .then((response) => response.json())
            .then((responseData) => {
                this.setState({
                    dataSource: this.state.dataSource.cloneWithRows(responseData.movies),
                    loaded: true,
                });
            })
            .done();
        //调用done(),可以抛出异常而不是简单忽略
    }
}

const styles = StyleSheet.create({
    list: {
        justifyContent: 'flex-start',
        flexDirection: 'row',
        flexWrap: 'wrap'
    },
    container: {
        width: 100,
        height: 100,
        backgroundColor: '#F5FCFF',
        margin:5,
        alignItems:'center',
    },
    listView: {
        paddingTop: 20,
        backgroundColor: '#F5FCFF',
    },
    thumbnail: {
        width: 80,
        height: 80,
        borderRadius:16,
    },
    //使rightContainer在父容器中占据Image之外剩下的全部空间
    container1: {
        flex: 1,
        justifyContent:'center',
        alignItems:'center',
    },
    title: {
        fontSize: 14,
        marginBottom: 8,
    },
    year: {
        fontSize: 14,
    },
});

AppRegistry.registerComponent('RNAPP', () => RNAPP);

滚动

ListView 只是整合了数据和展现,但实际滚动的功能还是由 ScrollView 全权负责。ScrollView 实现完全和平台相关:在 iOS 上,它映射为 RCTScrollView ;在 Android 上,它映射为 RCTScrollView 和 AndroidHorizontalScrollView 。

React Native 使不同平台的技术融合在一起,同时也给开发人员提出了更高的要求。以 ScrollView 为例,大量的属性其实原封不动映射给了 UIScrollView ,这就意味着如果想再深入地研究下去,必须对客户端相关技术有足够了解。无论是前端还是客户端,跳出自己熟悉的那片领域也许才是更进一步的关键。

谈到滚动,有一点不得不说的就是 列表的无限加载 ,这牵涉到滚动的性能。

Github 上的这个 issue 对此展开了热烈的讨论。其中有人就提到,数据量很大情况下,ListView 在加载时所占用的 CPU 和内存会大大增加,滚动到最后就会导致应用 crash。

为此,ListView 中新添加了一个实验性的属性:removeClippedSubviews ,它能在滚动时及时删掉列表中处于视窗的之外的行,以此达到降低内存消耗的目的。不幸的是,即使设置了这个属性,程序虽然各项指标占用减少了不少,但还是没避免崩溃的命运。处于好奇,我也在最新版的 ListView 基础上做了简单尝试,不断加载一个无限大的列表,但并没有出现崩溃的情况:

即使加载了 3000、4000 行,Android 真机、iOS 真机和 iOS 模拟器上都没有崩溃;
Android 上明显感到数据加载有阶段性的延时 ,即滚动一定程度后,再次滚动数据始终加载不出来或要等一段时间才加载出来,体验较差;iOS 相比要流畅的多;
但不崩溃并非最终的目的,很多 React Native 使用者都在试图改进 ListView 的性能表现,相比于直接使用 Native 端的组件,ListView 性能还是差强人意,有很大优化空间。

总结

ListView 并没有创造出新的东西,它只是集各家所长,很好地将 React 的视图渲染和 Native 端很成熟的滚动机制融合在了一起,使用起来和其他组件无差,静态地定义View、动态地组织数据,是给人带来的直观感受。

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/02/25/react-native-learning-listview-component/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
React Native学习之ListView组件
ListView组件 Android 原生 ListView 实现(MVC方式):ListView Adapter List集合数据 iOS 原生 ListView 实现:UITableView(sectionId、rowId、rowData) ……
<<上一篇
下一篇>>
文章目录
关闭
目 录