React Native组件之FlatList

在过去的一年中React Native经历了从v0.40到v0.52的十几次的版本迭代,可以看到,特别是0.50之后,React Native的组件库在不断地壮大,React Native也正在越来越稳定。

随着版本的升级,React Native引进了一些新的组件中,如FlatList、SectionList等具有更高性能的列表组件,也有与时俱进的用于适配全屏幕的SafeAreaView组件,同时一些性能比较差、无法适应React Native发展的一些老的组件也逐渐被抛弃,如:ListView、Navigator等组件。

下面是一张说明图,来自于网络: 
这里写图片描述

FlatList简介

总所周知,为了实现列表的效果,React Native提供了ListView组件,并且通过对ListView进行简单的封装,ListView还可以实现下拉刷新和上拉加载的功能。 
但是如果对ListView比较了解的同学都会发现,ListView的性能是非常差的,所以React Native在0.43版本推出了FlatList,FlatList自带上拉下拉的功能,用于替换ListView。

FlatList功能简介

FlatList支持如下功能:

  • 完全跨平台;
  • 支持水平布局模式;
  • 行组件显示或隐藏时可配置回调事件;
  • 支持单独的头部组件;
  • 支持单独的尾部组件;
  • 支持自定义行间分隔线;
  • 支持下拉刷新;
  • 支持上拉加载更多;
  • 支持跳转到指定行(ScrollToIndex)。

注:如果需要实现分组/类/区的效果,请使用<SectionList>组件。

FlatList实例

FlatList的使用也非常的简单,下面是

<FlatList
  data={[{key: 'a'}, {key: 'b'}]}
  renderItem={({item}) => <Text>{item.key}</Text>}
/>
  • 1
  • 2
  • 3
  • 4

可以看出,FlatList跟之前的ListView很像,但是其中少了dataSource,我们只需要传递数据,其它的都交给FlatList处理好了。之前ListView组件的使用如下,让我们对比来看:

<ListView 
     style={styles.listView}
     dataSource={this.state.dataSource}
     renderRow={this.renderCell.bind(this)}
/>
  • 1
  • 2
  • 3
  • 4
  • 5

属性说明

  • ItemSeparatorComponent 
    行与行之间的分隔线组件,不会出现在第一行之前和最后一行之后。
  • ListEmptyComponent 
    列表为空时渲染该组件,可以是React Component,也可以是一个render函数, 或者渲染好的element。
  • ListFooterComponent 
    尾部组件。
  • ListHeaderComponent 
    头部组件。
  • columnWrapperStyle 
    如果设置了多列布局(即将numColumns 
    值设为大于1的整数),则可以额外指定此样式作用在每行容器上。
  • data 
    为了简化起见,data属性目前只支持普通数组,如果需要使用其他特殊数据结构,例如immutable数组,请使用更底层的VirtualizedList组件。
  • extraData 
    如果有除data以外的数据作用在列表中(不论是用在renderItem 
    还是Header或者Footer中),请在此属性中指定作用的对象。同时此数据在修改时也需要先修改其引用地址(比如先复制到一个新的Object或者数组中),然后再修改其值,否则界面很可能不会刷新。
  • getItem 
    获取每个Item。
  • getItemCount 
    获取Item个数。
  • getItemLayout 
    getItemLayout是一个可选的优化,用于避免动态测量内容尺寸带来的开销,不过前提是你可以提前知道内容的高度。也就是说,如果你的行高是固定的,那么getItemLayout用起来就既高效又简单,类似下面这样: 
    getItemLayout={(data, index) => ( {length: 行高, offset: 行高 * index, index} )} 
    注意:如果你指定了SeparatorComponent,请把分隔线的尺寸也考虑到offset的计算之中。
  • horizontal 
    当此属性设置为true时,则变为水平布局模式。
  • initialNumToRender 
    指定一开始渲染的元素数量,最好刚刚够填满一个屏幕,这样保证了用最短的时间给用户呈现可见的内容。注意这第一批次渲染的元素不会在滑动过程中被卸载,这样是为了保证用户执行返回顶部的操作时,不需要重新渲染首批元素。
  • initialScrollIndex 
    指定渲染开始的item index。
  • inverted 
    翻转滚动方向,实质是将scale变换设置为-1。
  • keyExtractor 
    此函数用于为给定的item生成一个不重复的key。Key的作用是使React能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。若不指定此函数,则默认抽取item.key作为key值,若item.key也不存在,则使用数组下标。
  • legacyImplementation 
    设置为true则使用旧的ListView的实现。
  • numColumns 
    多列布局只能在非水平模式下使用,即必须是horizontal={false} 
    时。此时组件内元素会从左到右从上到下按Z字形排列,类似启用了flexWrap的布局,且组件内元素必须是等高的,暂时还无法支持瀑布流布局。
  • onEndReached 
    当列表被滚动到距离内容最底部不足onEndReachedThreshold的距离时调用。
  • onEndReachedThreshold 
    决定当距离内容最底部还有多远时触发onEndReached回调,注意此参数是一个比值而非像素单位。比如,0.5表示距离内容最底部的距离为当前列表可见长度的一半时触发。
  • onRefresh 
    如果设置了此选项,则会在列表头部添加一个标准的RefreshControl 
    控件,以便实现“下拉刷新”的功能,同时你需要正确设置refreshing 
    属性。
  • onViewableItemsChanged 
    在可见行元素变化时调用,可见范围和变化频率等参数的配置请设置viewabilityconfig属性。
  • refreshing 
    在等待加载新数据时将此属性设为true,列表就会显示出一个正在加载的符号。
  • renderItem 
    根据行数据data,渲染每一行的组件。
  • viewabilityConfig 
    请参考ViewabilityHelper的源码来了解具体的配置。

方法

  • scrollToEnd 
    滚动到底部,如果不设置getItemLayout属性的话,可能会比较卡。
  • scrollToIndex 
    滚动到指定index的item,如果不设置getItemLayout属性的话,无法跳转到当前可视区域以外的位置。
  • scrollToItem 
    滚动到指定item,如果不设置getItemLayout属性的话,可能会比较卡。
  • scrollToOffset 
    滚动指定距离。

复杂实例

下面是一个复杂的实例,效果如图: 
这里写图片描述

源码如下:

import React,{Component} from 'react';
import {
    View,Text,FlatList
} from 'react-native';

const data = [
    {key:'a',text:'jiangzhixi'},
    {key:'b',text:'jiangdonlin'},
    {key:'c',text:'huqianlong'},
];
const dataone = [{key: 'a',title:'aa'}, {key: 'b',title:'ba'}];
class Separator extends Component{
    render(){
        return (
            <Text>   ---</Text>
        );
    }
}

export default class FlatListView extends Component{

    renderItem(item){
        return (
            <View key={item}>
                <Text key={item}>{item.text}</Text>
            </View>
        )
    }

    renderSeparator(){
        return <Separator />;
    }

    renderHeadComp(){
        return <Text>我是头</Text>
    }

    renderEndComp(){
        return <Text>我是end</Text>
    }

    keyExtractor(item, index){
        console.log(item);//这里的item就是data里的每一项
        console.log(index);//index就是每一项的索引
    }

    render(){
        return (
            <View>
                <FlatList
                    data={data}
                    horizontal={false}
                    initialNumToRender={3}
                    ItemSeparatorComponent={this.renderSeparator}
                    ListHeaderComponent={this.renderHeadComp}
                    ListFooterComponent={this.renderEndComp}
                    refreshing={true}
                    renderItem={({item}) => this.renderItem(item)}
                    keyExtractor={this.keyExtractor}
                />
            </View>
        );
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

FlatList项目实战

首先我们看一下实现的效果,这也是我之前《React Native移动开发实战》中的一个项目。 
这里写图片描述 
这里写图片描述

上拉加载下拉刷新控件封装

源代码如下:

/**
 * 基于FlatList实现的上拉加载和下拉刷新组件
 */

import React, {PureComponent} from 'react'
import {View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, ViewPropTypes} from 'react-native'

export const RefreshState = {
    Idle: 0,
    HeaderRefreshing: 1,
    FooterRefreshing: 2,
    NoMoreData: 3,
    Failure: 4,
    EmptyData: 5,
}

const DEBUG = false
const log = (text: string) => {DEBUG && console.log(text)}

type Props = {
    refreshState: number,
    onHeaderRefresh: Function,
    onFooterRefresh?: Function,
    data: Array<any>,

    footerContainerStyle?: ViewPropTypes.style,
    footerTextStyle?: ViewPropTypes.style,

    listRef?: any,

    footerRefreshingText?: string,
    footerFailureText?: string,
    footerNoMoreDataText?: string,
    footerEmptyDataText?: string,

    renderItem: Function,
}

type State = {

}

export default class RefreshListView extends PureComponent<Props, State> {

    static defaultProps = {
        footerRefreshingText: '数据加载中…',
        footerFailureText: '点击重新加载',
        footerNoMoreDataText: '已加载全部数据',
        footerEmptyDataText: '暂时没有相关数据',
    }

    componentWillReceiveProps(nextProps: Props) {
        log('[RefreshListView]  RefreshListView componentWillReceiveProps ' + nextProps.refreshState)
    }

    componentDidUpdate(prevProps: Props, prevState: State) {
        log('[RefreshListView]  RefreshListView componentDidUpdate ' + prevProps.refreshState)
    }

    onHeaderRefresh = () => {
        log('[RefreshListView]  onHeaderRefresh')

        if (this.shouldStartHeaderRefreshing()) {
            log('[RefreshListView]  onHeaderRefresh')
            this.props.onHeaderRefresh(RefreshState.HeaderRefreshing)
        }
    }

    onEndReached = (info: {distanceFromEnd: number}) => {
        log('[RefreshListView]  onEndReached   ' + info.distanceFromEnd)

        if (this.shouldStartFooterRefreshing()) {
            log('[RefreshListView]  onFooterRefresh')
            this.props.onFooterRefresh && this.props.onFooterRefresh(RefreshState.FooterRefreshing)
        }
    }

    shouldStartHeaderRefreshing = () => {
        log('[RefreshListView]  shouldStartHeaderRefreshing')

        if (this.props.refreshState == RefreshState.HeaderRefreshing ||
            this.props.refreshState == RefreshState.FooterRefreshing) {
            return false
        }

        return true
    }

    shouldStartFooterRefreshing = () => {
        log('[RefreshListView]  shouldStartFooterRefreshing')

        let {refreshState, data} = this.props
        if (data.length == 0) {
            return false
        }

        return (refreshState == RefreshState.Idle)
    }

    render() {
        log('[RefreshListView]  render')

        let {renderItem, ...rest} = this.props

        return (
            <FlatList
                ref={this.props.listRef}
                onEndReached={this.onEndReached}
                onRefresh={this.onHeaderRefresh}
                refreshing={this.props.refreshState == RefreshState.HeaderRefreshing}
                ListFooterComponent={this.renderFooter}
                onEndReachedThreshold={0.1}

                renderItem={renderItem}

                {...rest}
            />
        )
    }

    renderFooter = () => {
        let footer = null

        let footerContainerStyle = [styles.footerContainer, this.props.footerContainerStyle]
        let footerTextStyle = [styles.footerText, this.props.footerTextStyle]
        let {footerRefreshingText, footerFailureText, footerNoMoreDataText, footerEmptyDataText} = this.props

        switch (this.props.refreshState) {
            case RefreshState.Idle:
                footer = (<View style={footerContainerStyle} />)
                break
            case RefreshState.Failure: {
                footer = (
                    <TouchableOpacity
                        style={footerContainerStyle}
                        onPress={() => {
                            this.props.onFooterRefresh && this.props.onFooterRefresh(RefreshState.FooterRefreshing)
                        }}
                    >
                        <Text style={footerTextStyle}>{footerFailureText}</Text>
                    </TouchableOpacity>
                )
                break
            }
            case RefreshState.EmptyData: {
                footer = (
                    <TouchableOpacity
                        style={footerContainerStyle}
                        onPress={() => {
                            this.props.onFooterRefresh && this.props.onFooterRefresh(RefreshState.FooterRefreshing)
                        }}
                    >
                        <Text style={footerTextStyle}>{footerEmptyDataText}</Text>
                    </TouchableOpacity>
                )
                break
            }
            case RefreshState.FooterRefreshing: {
                footer = (
                    <View style={footerContainerStyle} >
                        <ActivityIndicator size="small" color="#888888" />
                        <Text style={[footerTextStyle, {marginLeft: 7}]}>{footerRefreshingText}</Text>
                    </View>
                )
                break
            }
            case RefreshState.NoMoreData: {
                footer = (
                    <View style={footerContainerStyle} >
                        <Text style={footerTextStyle}>{footerNoMoreDataText}</Text>
                    </View>
                )
                break
            }
        }

        return footer
    }
}

const styles = StyleSheet.create({
    footerContainer: {
        flex: 1,
        flexDirection: 'row',
        justifyContent: 'center',
        alignItems: 'center',
        padding: 10,
        height: 44,
    },
    footerText: {
        fontSize: 14,
        color: '#555555'
    }
})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196

编写子布局

/**
 * 列表的Item
 */
import React, {PureComponent} from 'react'
import {View, Text, StyleSheet, TouchableOpacity, Image, PixelRatio} from 'react-native'

const color = {
    theme: '#06C1AE',
    border: '#e0e0e0',
    background: '#f3f3f3'
}

export default class ItemCell extends PureComponent {

    render() {
        let {info} = this.props

        let imageUrl = info.imageUrl.replace('w.h', '160.0').replace('http','https');

        return (
            <TouchableOpacity style={styles.container} onPress={() => this.props.onPress()}>
                <Image source={{ uri: imageUrl }} style={styles.icon} />

                <View style={styles.rightContainer}>
                    <Text>{info.title}</Text>
                    <View>
                    </View>
                    <Text style={styles.p} numberOfLines={0} >{info.subtitle}</Text>
                    <View style={{ flex: 1, justifyContent: 'flex-end' }}>
                        <Text style={styles.price}>{info.price}元</Text>
                    </View>
                </View>
            </TouchableOpacity>
        )
    }
}

const styles = StyleSheet.create({

    container: {
        flex:1,
        flexDirection: 'row',
        padding: 10,
        borderBottomWidth: 1,
        borderColor: '#e9e9e9',
        backgroundColor: 'white',
    },
    icon: {
        width: 80,
        height: 80,
        borderRadius: 5,
    },
    rightContainer: {
        flex: 1,
        paddingLeft: 20,
        paddingRight: 10,
    },
    price: {
        color: color.theme
    },
    h1: {
        fontSize: 15,
        fontWeight: 'bold',
        color: '#222222',
    },
    p: {
        fontSize: 13,
        color: '#777777',
        marginTop: 8,

    },
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

编写主界面

为了方便我们直接使用本地的数据,主界面的代码如下:

/**
 * 加载本地测试数据
 */

import React, {Component} from 'react'
import {View, StyleSheet, Platform, Dimensions} from 'react-native'
import RefreshListView, {RefreshState} from '../src/view/RefreshListView'
import Cell from '../src/ItemCell'
import data from '../src/config/data'

const {width} = Dimensions.get('window');

export default class FlatListLocalView extends Component {
    state: {
        dataList: Array<any>,
        refreshState: number,
    }

    constructor(props) {
        super(props)
        this.state = {
            dataList: [],
            refreshState: RefreshState.Idle,
        }
    }

    componentDidMount() {
        this.onHeaderRefresh()
    }

    onHeaderRefresh = () => {
        this.setState({refreshState: RefreshState.HeaderRefreshing})
        // 模拟网络请求
        setTimeout(() => {
            // 模拟网络加载失败的情况
            if (Math.random() < 0.2) {
                this.setState({refreshState: RefreshState.Failure})
                return
            }
            //获取测试数据
            let dataList = this.getTestList(true)

            this.setState({
                dataList: dataList,
                refreshState: dataList.length < 1 ? RefreshState.EmptyData : RefreshState.Idle,
            })
        }, 2000)
    }

    onFooterRefresh = () => {
        this.setState({refreshState: RefreshState.FooterRefreshing})

        // 模拟网络请求
        setTimeout(() => {
            // 模拟网络加载失败的情况
            if (Math.random() < 0.2) {
                this.setState({refreshState: RefreshState.Failure})
                return
            }
            //获取测试数据
            let dataList = this.getTestList(false)

            this.setState({
                dataList: dataList,
                refreshState: dataList.length > 50 ? RefreshState.NoMoreData : RefreshState.Idle,
            })
        }, 2000)
    }

    // 获取测试数据
    getTestList(isReload: boolean): Array<Object> {
        // fetch(api.recommend)
        //     .then((response) => response.json())
        //     .then((json) => {
        //         console.log(JSON.stringify(json));
        //
        //         let newList = json.data.map((info) => {
        //             return {
        //                 id: info.id,
        //                 imageUrl: info.squareimgurl,
        //                 title: info.mname,
        //                 subtitle: `[${info.range}]${info.title}`,
        //                 price: info.price
        //             }
        //         })
        //         return isReload ? newList: [...this.state.dataList, ...newList]
        //     })

        let newList = data.map((data) => {
            return {
                imageUrl: data.squareimgurl,
                title: data.mname,
                subtitle: `[${data.range}]${data.title}`,
                price: data.price,
            }
        })
        return isReload ? (Math.random() < 0.2 ? [] : newList) : [...this.state.dataList, ...newList]
    }

    keyExtractor = (item: any, index: number) => {
        return index
    }

    renderCell = (info: Object) => {
        return <Cell info={info.item}/>
    }

    render() {
        return (
            <View style={styles.container}>
                <RefreshListView
                    data={this.state.dataList}
                    keyExtractor={this.keyExtractor}
                    renderItem={this.renderCell}
                    refreshState={this.state.refreshState}
                    onHeaderRefresh={this.onHeaderRefresh}
                    onFooterRefresh={this.onFooterRefresh}

                    // 可选
                    footerRefreshingText='玩命加载中...'
                    footerFailureText='我擦嘞,居然加载失败了...'
                    footerNoMoreDataText='我是有底线的...'
                    footerEmptyDataText='好像什么东西都没有...'
                />
            </View>
        )
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        width: width,
        marginTop: Platform.OS == 'ios' ? 20 : 0,
    },
    title: {
        fontSize: 18,
        height: 84,
        textAlign: 'center'
    }
})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143

数据的话,大家可以参考下面的格式:

export default
[
    {
        "rating": 4.6,
        "range": "150店通用",
        "mname": "吉野家",
        "title": "卤肉饭+乌龙茶(小)1份",
        "price": 10,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/5911c9d9235036c6fc11fcb1dbcb5bce27954.jpg@87_0_266_266a%7C267h_267w_2e_100Q",
    },
    {
        "rating": 4.4,
        "range": "北京等",
        "mname": "真功夫",
        "title": "冬(香)菇鸡腿肉饭\t+\t卤蛋1份",
        "price": 15,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/15c8885d14f18774938a88752f08bb1e49194.jpg@118_0_466_466a%7C267h_267w_2e_90Q",
    },
    {
        "rating": 4.2,
        "range": "46店通用",
        "mname": "京八珍",
        "title": "50元代金券1张,可叠加",
        "price": 65,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/d57d5f0644256a3013469edfc1406e8022163.jpg",
    },
    {
        "rating": 4.2,
        "range": "2店通用",
        "mname": "麻里麻里",
        "title": "2人餐,提供免费WiFi",
        "price": 78,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/f436e044254128059f055f2275eadbb837054.jpg",
    },
    {
        "rating": 4.4,
        "range": "2店通用",
        "mname": "东来顺饭庄",
        "title": "4-5人套餐,百年老字号",
        "price": 168,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/416d01cbc4b8a2871b3c260615b5998088199.jpg",
    },
    {

        "rating": 4.2,
        "range": "12店通用",
        "mname": "果麦de鲜饮创作",
        "title": "饮品3选1,提供免费WiFi",
        "price": 7.99,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/d72d34a7038e8cca2d09406ec7dc5c83133480.jpg@0_297_1332_1332a%7C267h_267w_2e_90Q",
    },
    {
        "rating": 4,
        "range": "4店通用",
        "mname": "夹拣成厨麻辣烫",
        "title": "50元代金券1张,可叠加",
        "price": 39,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/712801d4f3562706f596cd366376889f25073.jpg@71_0_444_444a%7C267h_267w_2e_90Q",
    },
    {
        "rating": 4.6,
        "range": "150店通用",
        "mname": "吉野家",
        "title": "卤肉饭+乌龙茶(小)1份",
        "price": 10,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/5911c9d9235036c6fc11fcb1dbcb5bce27954.jpg@87_0_266_266a%7C267h_267w_2e_100Q",
    },
    {
        "rating": 4.4,
        "range": "北京等",
        "mname": "真功夫",
        "title": "冬(香)菇鸡腿肉饭\t+\t卤蛋1份",
        "price": 15,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/15c8885d14f18774938a88752f08bb1e49194.jpg@118_0_466_466a%7C267h_267w_2e_90Q",
    },
    {
        "rating": 4.2,
        "range": "46店通用",
        "mname": "京八珍",
        "title": "50元代金券1张,可叠加",
        "squareimgurl": "http://p0.meituan.net/w.h/deal/d57d5f0644256a3013469edfc1406e8022163.jpg",
    },
    {
        "rating": 4.2,
        "range": "2店通用",
        "mname": "麻里麻里",
        "title": "2人餐,提供免费WiFi",
        "price": 78,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/f436e044254128059f055f2275eadbb837054.jpg",
    },
    {
        "rating": 4.4,
        "range": "2店通用",
        "mname": "东来顺饭庄",
        "title": "4-5人套餐,百年老字号",
        "price": 168,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/416d01cbc4b8a2871b3c260615b5998088199.jpg",
    },
    {

        "rating": 4.2,
        "range": "12店通用",
        "mname": "果麦de鲜饮创作",
        "title": "饮品3选1,提供免费WiFi",
        "price": 7.99,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/d72d34a7038e8cca2d09406ec7dc5c83133480.jpg@0_297_1332_1332a%7C267h_267w_2e_90Q",
    },
    {
        "rating": 4,
        "range": "4店通用",
        "mname": "夹拣成厨麻辣烫",
        "title": "50元代金券1张,可叠加",
        "price": 39,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/712801d4f3562706f596cd366376889f25073.jpg@71_0_444_444a%7C267h_267w_2e_90Q",
    },
]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116

网络请求

上面是使用的是本地的Json数据,那么如果加载网络的数据又该怎么做呢?下面给一个简单的参考实例。 
首先,我们在componentDidMount()函数中发起请求,代码如下:

componentDidMount() {
       this.getTestList()
    }
  • 1
  • 2
  • 3

然后我们使用fetch来真正的发起请求,该段代码如下:

getTestList() {
        fetch(api.recommend)
            .then((response) => response.json())
            .then((json) => {
                //打印返回数据
                console.log("response"+JSON.stringify(json));

                let list = json.data.map((info) => {
                    return {
                        imageUrl: info.squareimgurl,
                        title: info.mname,
                        subtitle: `[${info.range}]${info.title}`,
                        price: info.price
                    }
                })
                this.setState({refreshState: RefreshState.Idle})
                console.log("newList"+list);
                this.setState({
                    dataList: list,
                    refreshState: list.length <1 ?  RefreshState.EmptyData : RefreshState.Idle,
                })

            }).catch((error) => {
            this.setState({refreshState: RefreshState.Failure})
        })

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

根据异步返回的结果,我们来改变RefreshListView控件的不同显示状态,RefreshListView的显示状态又:

Idle: 0,    //默认状态
HeaderRefreshing: 1,   //下拉刷新
FooterRefreshing: 2,   //上拉加载更多
NoMoreData: 3,         //暂无数据
Failure: 4,            //加载失败,如网络原因
EmptyData: 5,          //空页面
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当然,为了方便查看数据的返回情况,我们一般会使用调试来观察数据,进而进行数据的相关处理。如下图: 
这里写图片描述

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiangzhihong8/article/details/80616294

在过去的一年中React Native经历了从v0.40到v0.52的十几次的版本迭代,可以看到,特别是0.50之后,React Native的组件库在不断地壮大,React Native也正在越来越稳定。

随着版本的升级,React Native引进了一些新的组件中,如FlatList、SectionList等具有更高性能的列表组件,也有与时俱进的用于适配全屏幕的SafeAreaView组件,同时一些性能比较差、无法适应React Native发展的一些老的组件也逐渐被抛弃,如:ListView、Navigator等组件。

下面是一张说明图,来自于网络: 
这里写图片描述

FlatList简介

总所周知,为了实现列表的效果,React Native提供了ListView组件,并且通过对ListView进行简单的封装,ListView还可以实现下拉刷新和上拉加载的功能。 
但是如果对ListView比较了解的同学都会发现,ListView的性能是非常差的,所以React Native在0.43版本推出了FlatList,FlatList自带上拉下拉的功能,用于替换ListView。

FlatList功能简介

FlatList支持如下功能:

  • 完全跨平台;
  • 支持水平布局模式;
  • 行组件显示或隐藏时可配置回调事件;
  • 支持单独的头部组件;
  • 支持单独的尾部组件;
  • 支持自定义行间分隔线;
  • 支持下拉刷新;
  • 支持上拉加载更多;
  • 支持跳转到指定行(ScrollToIndex)。

注:如果需要实现分组/类/区的效果,请使用<SectionList>组件。

FlatList实例

FlatList的使用也非常的简单,下面是

<FlatList
  data={[{key: 'a'}, {key: 'b'}]}
  renderItem={({item}) => <Text>{item.key}</Text>}
/>
  • 1
  • 2
  • 3
  • 4

可以看出,FlatList跟之前的ListView很像,但是其中少了dataSource,我们只需要传递数据,其它的都交给FlatList处理好了。之前ListView组件的使用如下,让我们对比来看:

<ListView 
     style={styles.listView}
     dataSource={this.state.dataSource}
     renderRow={this.renderCell.bind(this)}
/>
  • 1
  • 2
  • 3
  • 4
  • 5

属性说明

  • ItemSeparatorComponent 
    行与行之间的分隔线组件,不会出现在第一行之前和最后一行之后。
  • ListEmptyComponent 
    列表为空时渲染该组件,可以是React Component,也可以是一个render函数, 或者渲染好的element。
  • ListFooterComponent 
    尾部组件。
  • ListHeaderComponent 
    头部组件。
  • columnWrapperStyle 
    如果设置了多列布局(即将numColumns 
    值设为大于1的整数),则可以额外指定此样式作用在每行容器上。
  • data 
    为了简化起见,data属性目前只支持普通数组,如果需要使用其他特殊数据结构,例如immutable数组,请使用更底层的VirtualizedList组件。
  • extraData 
    如果有除data以外的数据作用在列表中(不论是用在renderItem 
    还是Header或者Footer中),请在此属性中指定作用的对象。同时此数据在修改时也需要先修改其引用地址(比如先复制到一个新的Object或者数组中),然后再修改其值,否则界面很可能不会刷新。
  • getItem 
    获取每个Item。
  • getItemCount 
    获取Item个数。
  • getItemLayout 
    getItemLayout是一个可选的优化,用于避免动态测量内容尺寸带来的开销,不过前提是你可以提前知道内容的高度。也就是说,如果你的行高是固定的,那么getItemLayout用起来就既高效又简单,类似下面这样: 
    getItemLayout={(data, index) => ( {length: 行高, offset: 行高 * index, index} )} 
    注意:如果你指定了SeparatorComponent,请把分隔线的尺寸也考虑到offset的计算之中。
  • horizontal 
    当此属性设置为true时,则变为水平布局模式。
  • initialNumToRender 
    指定一开始渲染的元素数量,最好刚刚够填满一个屏幕,这样保证了用最短的时间给用户呈现可见的内容。注意这第一批次渲染的元素不会在滑动过程中被卸载,这样是为了保证用户执行返回顶部的操作时,不需要重新渲染首批元素。
  • initialScrollIndex 
    指定渲染开始的item index。
  • inverted 
    翻转滚动方向,实质是将scale变换设置为-1。
  • keyExtractor 
    此函数用于为给定的item生成一个不重复的key。Key的作用是使React能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。若不指定此函数,则默认抽取item.key作为key值,若item.key也不存在,则使用数组下标。
  • legacyImplementation 
    设置为true则使用旧的ListView的实现。
  • numColumns 
    多列布局只能在非水平模式下使用,即必须是horizontal={false} 
    时。此时组件内元素会从左到右从上到下按Z字形排列,类似启用了flexWrap的布局,且组件内元素必须是等高的,暂时还无法支持瀑布流布局。
  • onEndReached 
    当列表被滚动到距离内容最底部不足onEndReachedThreshold的距离时调用。
  • onEndReachedThreshold 
    决定当距离内容最底部还有多远时触发onEndReached回调,注意此参数是一个比值而非像素单位。比如,0.5表示距离内容最底部的距离为当前列表可见长度的一半时触发。
  • onRefresh 
    如果设置了此选项,则会在列表头部添加一个标准的RefreshControl 
    控件,以便实现“下拉刷新”的功能,同时你需要正确设置refreshing 
    属性。
  • onViewableItemsChanged 
    在可见行元素变化时调用,可见范围和变化频率等参数的配置请设置viewabilityconfig属性。
  • refreshing 
    在等待加载新数据时将此属性设为true,列表就会显示出一个正在加载的符号。
  • renderItem 
    根据行数据data,渲染每一行的组件。
  • viewabilityConfig 
    请参考ViewabilityHelper的源码来了解具体的配置。

方法

  • scrollToEnd 
    滚动到底部,如果不设置getItemLayout属性的话,可能会比较卡。
  • scrollToIndex 
    滚动到指定index的item,如果不设置getItemLayout属性的话,无法跳转到当前可视区域以外的位置。
  • scrollToItem 
    滚动到指定item,如果不设置getItemLayout属性的话,可能会比较卡。
  • scrollToOffset 
    滚动指定距离。

复杂实例

下面是一个复杂的实例,效果如图: 
这里写图片描述

源码如下:

import React,{Component} from 'react';
import {
    View,Text,FlatList
} from 'react-native';

const data = [
    {key:'a',text:'jiangzhixi'},
    {key:'b',text:'jiangdonlin'},
    {key:'c',text:'huqianlong'},
];
const dataone = [{key: 'a',title:'aa'}, {key: 'b',title:'ba'}];
class Separator extends Component{
    render(){
        return (
            <Text>   ---</Text>
        );
    }
}

export default class FlatListView extends Component{

    renderItem(item){
        return (
            <View key={item}>
                <Text key={item}>{item.text}</Text>
            </View>
        )
    }

    renderSeparator(){
        return <Separator />;
    }

    renderHeadComp(){
        return <Text>我是头</Text>
    }

    renderEndComp(){
        return <Text>我是end</Text>
    }

    keyExtractor(item, index){
        console.log(item);//这里的item就是data里的每一项
        console.log(index);//index就是每一项的索引
    }

    render(){
        return (
            <View>
                <FlatList
                    data={data}
                    horizontal={false}
                    initialNumToRender={3}
                    ItemSeparatorComponent={this.renderSeparator}
                    ListHeaderComponent={this.renderHeadComp}
                    ListFooterComponent={this.renderEndComp}
                    refreshing={true}
                    renderItem={({item}) => this.renderItem(item)}
                    keyExtractor={this.keyExtractor}
                />
            </View>
        );
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

FlatList项目实战

首先我们看一下实现的效果,这也是我之前《React Native移动开发实战》中的一个项目。 
这里写图片描述 
这里写图片描述

上拉加载下拉刷新控件封装

源代码如下:

/**
 * 基于FlatList实现的上拉加载和下拉刷新组件
 */

import React, {PureComponent} from 'react'
import {View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, ViewPropTypes} from 'react-native'

export const RefreshState = {
    Idle: 0,
    HeaderRefreshing: 1,
    FooterRefreshing: 2,
    NoMoreData: 3,
    Failure: 4,
    EmptyData: 5,
}

const DEBUG = false
const log = (text: string) => {DEBUG && console.log(text)}

type Props = {
    refreshState: number,
    onHeaderRefresh: Function,
    onFooterRefresh?: Function,
    data: Array<any>,

    footerContainerStyle?: ViewPropTypes.style,
    footerTextStyle?: ViewPropTypes.style,

    listRef?: any,

    footerRefreshingText?: string,
    footerFailureText?: string,
    footerNoMoreDataText?: string,
    footerEmptyDataText?: string,

    renderItem: Function,
}

type State = {

}

export default class RefreshListView extends PureComponent<Props, State> {

    static defaultProps = {
        footerRefreshingText: '数据加载中…',
        footerFailureText: '点击重新加载',
        footerNoMoreDataText: '已加载全部数据',
        footerEmptyDataText: '暂时没有相关数据',
    }

    componentWillReceiveProps(nextProps: Props) {
        log('[RefreshListView]  RefreshListView componentWillReceiveProps ' + nextProps.refreshState)
    }

    componentDidUpdate(prevProps: Props, prevState: State) {
        log('[RefreshListView]  RefreshListView componentDidUpdate ' + prevProps.refreshState)
    }

    onHeaderRefresh = () => {
        log('[RefreshListView]  onHeaderRefresh')

        if (this.shouldStartHeaderRefreshing()) {
            log('[RefreshListView]  onHeaderRefresh')
            this.props.onHeaderRefresh(RefreshState.HeaderRefreshing)
        }
    }

    onEndReached = (info: {distanceFromEnd: number}) => {
        log('[RefreshListView]  onEndReached   ' + info.distanceFromEnd)

        if (this.shouldStartFooterRefreshing()) {
            log('[RefreshListView]  onFooterRefresh')
            this.props.onFooterRefresh && this.props.onFooterRefresh(RefreshState.FooterRefreshing)
        }
    }

    shouldStartHeaderRefreshing = () => {
        log('[RefreshListView]  shouldStartHeaderRefreshing')

        if (this.props.refreshState == RefreshState.HeaderRefreshing ||
            this.props.refreshState == RefreshState.FooterRefreshing) {
            return false
        }

        return true
    }

    shouldStartFooterRefreshing = () => {
        log('[RefreshListView]  shouldStartFooterRefreshing')

        let {refreshState, data} = this.props
        if (data.length == 0) {
            return false
        }

        return (refreshState == RefreshState.Idle)
    }

    render() {
        log('[RefreshListView]  render')

        let {renderItem, ...rest} = this.props

        return (
            <FlatList
                ref={this.props.listRef}
                onEndReached={this.onEndReached}
                onRefresh={this.onHeaderRefresh}
                refreshing={this.props.refreshState == RefreshState.HeaderRefreshing}
                ListFooterComponent={this.renderFooter}
                onEndReachedThreshold={0.1}

                renderItem={renderItem}

                {...rest}
            />
        )
    }

    renderFooter = () => {
        let footer = null

        let footerContainerStyle = [styles.footerContainer, this.props.footerContainerStyle]
        let footerTextStyle = [styles.footerText, this.props.footerTextStyle]
        let {footerRefreshingText, footerFailureText, footerNoMoreDataText, footerEmptyDataText} = this.props

        switch (this.props.refreshState) {
            case RefreshState.Idle:
                footer = (<View style={footerContainerStyle} />)
                break
            case RefreshState.Failure: {
                footer = (
                    <TouchableOpacity
                        style={footerContainerStyle}
                        onPress={() => {
                            this.props.onFooterRefresh && this.props.onFooterRefresh(RefreshState.FooterRefreshing)
                        }}
                    >
                        <Text style={footerTextStyle}>{footerFailureText}</Text>
                    </TouchableOpacity>
                )
                break
            }
            case RefreshState.EmptyData: {
                footer = (
                    <TouchableOpacity
                        style={footerContainerStyle}
                        onPress={() => {
                            this.props.onFooterRefresh && this.props.onFooterRefresh(RefreshState.FooterRefreshing)
                        }}
                    >
                        <Text style={footerTextStyle}>{footerEmptyDataText}</Text>
                    </TouchableOpacity>
                )
                break
            }
            case RefreshState.FooterRefreshing: {
                footer = (
                    <View style={footerContainerStyle} >
                        <ActivityIndicator size="small" color="#888888" />
                        <Text style={[footerTextStyle, {marginLeft: 7}]}>{footerRefreshingText}</Text>
                    </View>
                )
                break
            }
            case RefreshState.NoMoreData: {
                footer = (
                    <View style={footerContainerStyle} >
                        <Text style={footerTextStyle}>{footerNoMoreDataText}</Text>
                    </View>
                )
                break
            }
        }

        return footer
    }
}

const styles = StyleSheet.create({
    footerContainer: {
        flex: 1,
        flexDirection: 'row',
        justifyContent: 'center',
        alignItems: 'center',
        padding: 10,
        height: 44,
    },
    footerText: {
        fontSize: 14,
        color: '#555555'
    }
})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196

编写子布局

/**
 * 列表的Item
 */
import React, {PureComponent} from 'react'
import {View, Text, StyleSheet, TouchableOpacity, Image, PixelRatio} from 'react-native'

const color = {
    theme: '#06C1AE',
    border: '#e0e0e0',
    background: '#f3f3f3'
}

export default class ItemCell extends PureComponent {

    render() {
        let {info} = this.props

        let imageUrl = info.imageUrl.replace('w.h', '160.0').replace('http','https');

        return (
            <TouchableOpacity style={styles.container} onPress={() => this.props.onPress()}>
                <Image source={{ uri: imageUrl }} style={styles.icon} />

                <View style={styles.rightContainer}>
                    <Text>{info.title}</Text>
                    <View>
                    </View>
                    <Text style={styles.p} numberOfLines={0} >{info.subtitle}</Text>
                    <View style={{ flex: 1, justifyContent: 'flex-end' }}>
                        <Text style={styles.price}>{info.price}元</Text>
                    </View>
                </View>
            </TouchableOpacity>
        )
    }
}

const styles = StyleSheet.create({

    container: {
        flex:1,
        flexDirection: 'row',
        padding: 10,
        borderBottomWidth: 1,
        borderColor: '#e9e9e9',
        backgroundColor: 'white',
    },
    icon: {
        width: 80,
        height: 80,
        borderRadius: 5,
    },
    rightContainer: {
        flex: 1,
        paddingLeft: 20,
        paddingRight: 10,
    },
    price: {
        color: color.theme
    },
    h1: {
        fontSize: 15,
        fontWeight: 'bold',
        color: '#222222',
    },
    p: {
        fontSize: 13,
        color: '#777777',
        marginTop: 8,

    },
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

编写主界面

为了方便我们直接使用本地的数据,主界面的代码如下:

/**
 * 加载本地测试数据
 */

import React, {Component} from 'react'
import {View, StyleSheet, Platform, Dimensions} from 'react-native'
import RefreshListView, {RefreshState} from '../src/view/RefreshListView'
import Cell from '../src/ItemCell'
import data from '../src/config/data'

const {width} = Dimensions.get('window');

export default class FlatListLocalView extends Component {
    state: {
        dataList: Array<any>,
        refreshState: number,
    }

    constructor(props) {
        super(props)
        this.state = {
            dataList: [],
            refreshState: RefreshState.Idle,
        }
    }

    componentDidMount() {
        this.onHeaderRefresh()
    }

    onHeaderRefresh = () => {
        this.setState({refreshState: RefreshState.HeaderRefreshing})
        // 模拟网络请求
        setTimeout(() => {
            // 模拟网络加载失败的情况
            if (Math.random() < 0.2) {
                this.setState({refreshState: RefreshState.Failure})
                return
            }
            //获取测试数据
            let dataList = this.getTestList(true)

            this.setState({
                dataList: dataList,
                refreshState: dataList.length < 1 ? RefreshState.EmptyData : RefreshState.Idle,
            })
        }, 2000)
    }

    onFooterRefresh = () => {
        this.setState({refreshState: RefreshState.FooterRefreshing})

        // 模拟网络请求
        setTimeout(() => {
            // 模拟网络加载失败的情况
            if (Math.random() < 0.2) {
                this.setState({refreshState: RefreshState.Failure})
                return
            }
            //获取测试数据
            let dataList = this.getTestList(false)

            this.setState({
                dataList: dataList,
                refreshState: dataList.length > 50 ? RefreshState.NoMoreData : RefreshState.Idle,
            })
        }, 2000)
    }

    // 获取测试数据
    getTestList(isReload: boolean): Array<Object> {
        // fetch(api.recommend)
        //     .then((response) => response.json())
        //     .then((json) => {
        //         console.log(JSON.stringify(json));
        //
        //         let newList = json.data.map((info) => {
        //             return {
        //                 id: info.id,
        //                 imageUrl: info.squareimgurl,
        //                 title: info.mname,
        //                 subtitle: `[${info.range}]${info.title}`,
        //                 price: info.price
        //             }
        //         })
        //         return isReload ? newList: [...this.state.dataList, ...newList]
        //     })

        let newList = data.map((data) => {
            return {
                imageUrl: data.squareimgurl,
                title: data.mname,
                subtitle: `[${data.range}]${data.title}`,
                price: data.price,
            }
        })
        return isReload ? (Math.random() < 0.2 ? [] : newList) : [...this.state.dataList, ...newList]
    }

    keyExtractor = (item: any, index: number) => {
        return index
    }

    renderCell = (info: Object) => {
        return <Cell info={info.item}/>
    }

    render() {
        return (
            <View style={styles.container}>
                <RefreshListView
                    data={this.state.dataList}
                    keyExtractor={this.keyExtractor}
                    renderItem={this.renderCell}
                    refreshState={this.state.refreshState}
                    onHeaderRefresh={this.onHeaderRefresh}
                    onFooterRefresh={this.onFooterRefresh}

                    // 可选
                    footerRefreshingText='玩命加载中...'
                    footerFailureText='我擦嘞,居然加载失败了...'
                    footerNoMoreDataText='我是有底线的...'
                    footerEmptyDataText='好像什么东西都没有...'
                />
            </View>
        )
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        width: width,
        marginTop: Platform.OS == 'ios' ? 20 : 0,
    },
    title: {
        fontSize: 18,
        height: 84,
        textAlign: 'center'
    }
})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143

数据的话,大家可以参考下面的格式:

export default
[
    {
        "rating": 4.6,
        "range": "150店通用",
        "mname": "吉野家",
        "title": "卤肉饭+乌龙茶(小)1份",
        "price": 10,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/5911c9d9235036c6fc11fcb1dbcb5bce27954.jpg@87_0_266_266a%7C267h_267w_2e_100Q",
    },
    {
        "rating": 4.4,
        "range": "北京等",
        "mname": "真功夫",
        "title": "冬(香)菇鸡腿肉饭\t+\t卤蛋1份",
        "price": 15,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/15c8885d14f18774938a88752f08bb1e49194.jpg@118_0_466_466a%7C267h_267w_2e_90Q",
    },
    {
        "rating": 4.2,
        "range": "46店通用",
        "mname": "京八珍",
        "title": "50元代金券1张,可叠加",
        "price": 65,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/d57d5f0644256a3013469edfc1406e8022163.jpg",
    },
    {
        "rating": 4.2,
        "range": "2店通用",
        "mname": "麻里麻里",
        "title": "2人餐,提供免费WiFi",
        "price": 78,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/f436e044254128059f055f2275eadbb837054.jpg",
    },
    {
        "rating": 4.4,
        "range": "2店通用",
        "mname": "东来顺饭庄",
        "title": "4-5人套餐,百年老字号",
        "price": 168,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/416d01cbc4b8a2871b3c260615b5998088199.jpg",
    },
    {

        "rating": 4.2,
        "range": "12店通用",
        "mname": "果麦de鲜饮创作",
        "title": "饮品3选1,提供免费WiFi",
        "price": 7.99,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/d72d34a7038e8cca2d09406ec7dc5c83133480.jpg@0_297_1332_1332a%7C267h_267w_2e_90Q",
    },
    {
        "rating": 4,
        "range": "4店通用",
        "mname": "夹拣成厨麻辣烫",
        "title": "50元代金券1张,可叠加",
        "price": 39,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/712801d4f3562706f596cd366376889f25073.jpg@71_0_444_444a%7C267h_267w_2e_90Q",
    },
    {
        "rating": 4.6,
        "range": "150店通用",
        "mname": "吉野家",
        "title": "卤肉饭+乌龙茶(小)1份",
        "price": 10,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/5911c9d9235036c6fc11fcb1dbcb5bce27954.jpg@87_0_266_266a%7C267h_267w_2e_100Q",
    },
    {
        "rating": 4.4,
        "range": "北京等",
        "mname": "真功夫",
        "title": "冬(香)菇鸡腿肉饭\t+\t卤蛋1份",
        "price": 15,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/15c8885d14f18774938a88752f08bb1e49194.jpg@118_0_466_466a%7C267h_267w_2e_90Q",
    },
    {
        "rating": 4.2,
        "range": "46店通用",
        "mname": "京八珍",
        "title": "50元代金券1张,可叠加",
        "squareimgurl": "http://p0.meituan.net/w.h/deal/d57d5f0644256a3013469edfc1406e8022163.jpg",
    },
    {
        "rating": 4.2,
        "range": "2店通用",
        "mname": "麻里麻里",
        "title": "2人餐,提供免费WiFi",
        "price": 78,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/f436e044254128059f055f2275eadbb837054.jpg",
    },
    {
        "rating": 4.4,
        "range": "2店通用",
        "mname": "东来顺饭庄",
        "title": "4-5人套餐,百年老字号",
        "price": 168,
        "squareimgurl": "http://p0.meituan.net/w.h/deal/416d01cbc4b8a2871b3c260615b5998088199.jpg",
    },
    {

        "rating": 4.2,
        "range": "12店通用",
        "mname": "果麦de鲜饮创作",
        "title": "饮品3选1,提供免费WiFi",
        "price": 7.99,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/d72d34a7038e8cca2d09406ec7dc5c83133480.jpg@0_297_1332_1332a%7C267h_267w_2e_90Q",
    },
    {
        "rating": 4,
        "range": "4店通用",
        "mname": "夹拣成厨麻辣烫",
        "title": "50元代金券1张,可叠加",
        "price": 39,
        "squareimgurl": "http://p1.meituan.net/w.h/deal/712801d4f3562706f596cd366376889f25073.jpg@71_0_444_444a%7C267h_267w_2e_90Q",
    },
]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116

网络请求

上面是使用的是本地的Json数据,那么如果加载网络的数据又该怎么做呢?下面给一个简单的参考实例。 
首先,我们在componentDidMount()函数中发起请求,代码如下:

componentDidMount() {
       this.getTestList()
    }
  • 1
  • 2
  • 3

然后我们使用fetch来真正的发起请求,该段代码如下:

getTestList() {
        fetch(api.recommend)
            .then((response) => response.json())
            .then((json) => {
                //打印返回数据
                console.log("response"+JSON.stringify(json));

                let list = json.data.map((info) => {
                    return {
                        imageUrl: info.squareimgurl,
                        title: info.mname,
                        subtitle: `[${info.range}]${info.title}`,
                        price: info.price
                    }
                })
                this.setState({refreshState: RefreshState.Idle})
                console.log("newList"+list);
                this.setState({
                    dataList: list,
                    refreshState: list.length <1 ?  RefreshState.EmptyData : RefreshState.Idle,
                })

            }).catch((error) => {
            this.setState({refreshState: RefreshState.Failure})
        })

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

根据异步返回的结果,我们来改变RefreshListView控件的不同显示状态,RefreshListView的显示状态又:

Idle: 0,    //默认状态
HeaderRefreshing: 1,   //下拉刷新
FooterRefreshing: 2,   //上拉加载更多
NoMoreData: 3,         //暂无数据
Failure: 4,            //加载失败,如网络原因
EmptyData: 5,          //空页面
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当然,为了方便查看数据的返回情况,我们一般会使用调试来观察数据,进而进行数据的相关处理。如下图: 
这里写图片描述

猜你喜欢

转载自blog.csdn.net/sinat_17775997/article/details/81030754