React-Native 之 ListView使用

前言

  • 学习本系列内容需要具备一定 HTML 开发基础,没有基础的朋友可以先转至 HTML快速入门(一) 学习

  • 本人接触 React Native 时间并不是特别长,所以对其中的内容和性质了解可能会有所偏差,在学习中如果有错会及时修改内容,也欢迎万能的朋友们批评指出,谢谢

  • 文章第一版出自简书,如果出现图片或页面显示问题,烦请转至 简书 查看 也希望喜欢的朋友可以点赞,谢谢

更新公告:

  • 2017.05.16 —— 根据一些朋友私信我的代码,发现有些错误是文中有一些拼写错误导致,已进行更正,对此造成的不便,请见谅。

ListView组件介绍


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

  • ListView 继承了 ScrollView 的所有属性

  • 使用步骤:

    • 创建一个ListView.DataSource数据源,然后给它传递一个普通的数组数据


        getInitialState(){
            // 初始化数据源(rowHasChanged是优化的一种手段,只有当r1 !== r2的时候才会重新渲染)
            var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
            return{
                // 给dataSource传递一组 数组
                dataSource: ds.cloneWithRows(['内容0', '内容1', '内容2', '内容3', '内容4', '内容5'])
            }
        },
    
    
    • 使用数据源实例化一个ListView组件,定义一个renderRow回调函数,这个函数会接受数组中的每个数据作为参数,并返回一个可渲染的组件(也就是该列表的每一行Item)


        render() {
            return (
                <View style={styles.container}>
                    // 根据数据源实例化一个ListView
                    <ListView style={{backgroundColor:'yellow'}}
                        // 获取数据源
                        dataSource={this.state.dataSource}
                        // 根据数据源创建一个Item
                        // 注:这里的this.renderRow是隐式写法,系统会根据函数的需要,将对应的参数传递过去(共有4个参数:rowData, sectionID, rowID, highlightRow)
                        renderRow={this.renderRow}
                    />
                </View>
            );
        },
        
        // 返回一个Item
        renderRow(rowData,sectionID,rowID) {
            return(
                // 实例化Item
                <View>
                    <Text style={{backgroundColor:'red', height:44}}>内容{rowData},在第{sectionID}组第{rowID}行</Text>
                </View>
            )
        }
    
    

    效果:


    ListView初体验.gif
  • ListView 同样支持一些高级特性,包括设置每一组的粘性的头部、支持设置列表 header 和 footter 视图、当数据列表滑动到最底部的时候支持 onEndReached 方法回调、设备屏幕列表可见的视图数据发生变化的时候回调 onChangeVisibleRows 以及一些性能方面的优化特性

ListView常用属性


  • ScrollView 全部属性

  • dataSource:设置ListView的数据源

  • initialListSize:指定在组件刚挂载的时候渲染多少行数据。用这个属性来确保首屏显示合适数量的数据,而不是花费太多帧逐步显示出来

  • onChangeVisibleRows:((visibleRows, changedRows) => void)当可见的行的集合变化的时候调用此回调函数。visibleRows 以 { sectionID: { rowID: true }}的格式包含了所有可见行,而changedRows 以{ sectionID: { rowID: true | false }}的格式包含了所有刚刚改变了可见性的行,其中如果值为true表示一个行变得可见,而为false表示行刚刚离开可视区域而变得不可见

  • onEndReached:当所有的数据都已经渲染过,并且列表被滚动到距离最底部不足onEndReachedThreshold个像素的距离时调用。原生的滚动事件会被作为参数传递。译注:当第一次渲染时,如果数据不足一屏(比如初始值是空的),这个事件也会被触发

  • onEndReachedThreshold:调用onEndReached之前的临界值,单位是像素

  • pageSize:每次事件循环(每帧)渲染的行数

  • removeClippedSubviews:用于提升大列表的滚动性能。需要给行容器添加样式overflow:'hidden'。(Android已默认添加此样式)此属性默认开启

  • renderFooter:(() => renderable)页头与页脚会在每次渲染过程中都重新渲染(如果提供了这些属性)。如果它们重绘的性能开销很大,把他们包装到一个StaticContainer或者其它恰当的结构中。页脚会永远在列表的最底部,而页头会在最顶部

  • renderHeader: 在每一次渲染过程中Footer(尾)该会一直在列表的底部,header(头)该会一直在列表的头部

  • renderRow:【(rowData, sectionID, rowID, highlightRow) => renderable

    • 从数据源(Data source)中接受一条数据,以及它和它所在section的ID。返回一个可渲染的组件来为这行数据进行渲染。默认情况下参数中的数据就是放进数据源中的数据本身,不过也可以提供一些转换器
    • 如果某一行正在被高亮(通过调用highlightRow函数),ListView会得到相应的通知。当一行被高亮时,其两侧的分割线会被隐藏。行的高亮状态可以通过调用highlightRow(null)来重置
  • renderScrollComponent:【(props) => renderable】指定一个函数,在其中返回一个可以滚动的组件。ListView将会在该组件内部进行渲染。默认情况下会返回一个包含指定属性的ScrollView

  • renderSectionHeader:【(sectionData, sectionID) => renderable】

    • 如果提供了此函数,会为每个小节(section)渲染一个粘性的标题。
    • 粘性是指当它刚出现时,会处在对应小节的内容顶部;继续下滑当它到达屏幕顶端的时候,它会停留在屏幕顶端,一直到对应的位置被下一个小节的标题占据为止
  • renderSeparator:【(sectionID, rowID, adjacentRowHighlighted) => renderable】

    • 如果提供了此属性,一个可渲染的组件会被渲染在每一行下面,除了小节标题的前面的最后一行。在其上方的小节ID和行ID,以及邻近的行是否被高亮会作为参数传递进来
  • scrollRenderAheadDistance:当一个行接近屏幕范围多少像素之内的时候,就开始渲染这一行

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

方法


  • getMetrics():导出一些用于性能分析的数据

  • scrollTo(...args):滚动到指定的x, y偏移处,可以指定是否加上过渡动画。

    • 参考 ScrollView#scrollTo.

ListView简单优化建议


  • ListView 设计的时候,当需要动态加载非常大量或者渲染复杂的数据时,下面有一些方法可以提高 ListView 的性能
    • 只渲染更新数据变化的那个Item,rowHasChange方法会告诉ListView组件是否需要重新渲染当前Item
    • 选择渲染的频率,默认情况下,每一个event-loop(事件循环)只会渲染一行(可以同pageSize自定义属性设置)这样可以把大工作量进行分隔,提供整体渲染性能

ListView 基本布局


  • 这边我们就按照下图中的布局实现一个简单的列表数据展示


    基本布局
  • 分析上图整体布局,我这边就将其划分为几个模块,首先需要有个 大的View 来包装内部所有的内容,其次再给标题部分分配一个 小View 以方便维护,具体如下图

    基本布局分析

  • 接下来就可以开始干活啦~

    • 首先,ListView 需要数据源,那么我们就先来自定义一下数据源的 Json 数据,


        [
            {"title" : "icon", "img" : "icon"},
            {"title" : "lufei", "img" : "lufei"},
            {"title" : "icon", "img" : "icon"},
            {"title" : "lufei", "img" : "lufei"},
            {"title" : "icon", "img" : "icon"},
            {"title" : "lufei", "img" : "lufei"},
            {"title" : "icon", "img" : "icon"},
            {"title" : "lufei", "img" : "lufei"},
            {"title" : "icon", "img" : "icon"},
            {"title" : "lufei", "img" : "lufei"},
            {"title" : "icon", "img" : "icon"},
            {"title" : "lufei", "img" : "lufei"},
            {"title" : "icon", "img" : "icon"},
            {"title" : "lufei", "img" : "lufei"}
        ]
        
    
    • 有了数据后,我们就可以根据数据来实例化 ListView

      • 获取数据


          var newData = require('./Data/localData.json');
      
      
      • 初始化数据源


          getInitialState(){
              var ds = new ListView.DataSource({rowHasChanged:(r1, r2) => r1 != r2});
                  return{
                      // 将获得的数组传递给dataSource
                      dataSource : ds.cloneWithRows(newData)
              }
          },
      
      
      • 接着就是根据数据源实例化 ListView

        • 视图部分


            render(){
                return(
                    <View style={styles.container}>
                        <ListView
                            dataSource={this.state.dataSource}
                            renderRow={this.renderRow}
                        />
                    </View>
                );
            },
            
            // 返回一个Item
            renderRow(rowData){
                return(
                    <View style={styles.itemStyle}>
                        <Image source={{uri:rowData.img}} style={styles.imageStyle}/>
                        <View style={styles.subItemStyle}>
                            <Text style={{marginTop:5, fontSize:17}}>{rowData.title}</Text>
                            <Text style={{marginBottom:5, fontSize:13, color:'green'}}>简介</Text>
                        </View>
                    </View>
                );
            }
        
        
        • 样式部分


            var styles = StyleSheet.create({
                container: {
                    flex:1
                },
        
                itemStyle: {
                    // 主轴方向
                    flexDirection:'row',
                    // 下边框
                    borderBottomWidth:1,
                    borderBottomColor:'gray'
                },
        
                imageStyle: {
                    // 尺寸
                    width:60,
                    height:60,
                    // 边距
                    marginLeft:10,
                    margin:10
                },
        
                subItemStyle: {
                    // 对齐方式
                    justifyContent:'space-around'
                }
            });
            
        

    效果:


    ListView案例一.gif

ListView 九宫格布局实现


  • 先来看下大概的布局
九宫格概念布局
  • 从上面可以看出,这个案例是为了实现类似 CollectionView 效果(比如常见的瀑布流),通常情况下,ListView 是纵向排列的,而此案例我们需要它横向排列,那么就需要使用到上面提到的 contentContainerStyle 属性,向里面添加 flexDirection:'row'和flexWrap:'wrap'` 两个属性

        contentViewStyle: {
            // 主轴方向
            flexDirection:'row',
            // 换行
            flexWrap:'wrap'
        },
    
    
  • 当然了,我们还是需要自定义一组数据供 ListView 使用,这边就使用上面案例的数据

  • 根据数据实例化 ListView ,参考上面案例,这里只粘贴 Item部分,其它的就不重复了

    • 视图部分


        var ListViewDemo = React.createClass({
            getInitialState(){
                // 初始化数据源
                var ds = new ListView.DataSource({rowHasChanged:(r1, r2) => r1 != r2});
                return{
                    dataSource : ds.cloneWithRows(newData)
                }
            },
    
            render(){
                return(
                    <ListView
                        dataSource={this.state.dataSource}
                        renderRow={this.renderRow}
                        // 设置contentContainerStyle
                        contentContainerStyle={styles.contentViewStyle}
                    />
                );
            },
    
            // 返回一个Item
            renderRow(rowData){
                return(
                {/* 实例化Item */}
                <View style={styles.itemStyle}>
                    <Image source={{uri:rowData.img}} style={styles.itemImageStyle}/>
                    <Text>{rowData.title}</Text>
                </View>
                );
            }
        });
    
    
    • 样式部分


        var styles = StyleSheet.create({
    
            contentViewStyle: {
                // 主轴方向
                flexDirection:'row',
                // 换行
                flexWrap:'wrap'
            },
    
            itemStyle: {
                // 对齐方式
                alignItems:'center',
                justifyContent:'center',
                // 尺寸
                width:itemWH,
                height:itemWH,
                // 左边距
                marginLeft:vMargin,
                marginTop:hMargin
            },
    
            itemImageStyle: {
                // 尺寸
                width:60,
                height:60,
                // 间距
                marginBottom:5
            }
        });
    
    

    效果:


    ListView九宫格布局

ListView 分组样式的实现分析


  • 在移动设备里面,经常会看到 sticky效果,比如常见的通讯录

  • 在React Native中,ScrollView组件要实现 sticky效果 很简单,只需要使用
    stickyHeaderIndices 就可以了,但对于 ListView 来说,stickyHeaderIndices是无效的,下面我们就来分析怎样才能使 ListView 实现吸顶效果

  • 首先,ListView要实现 sticky效果 需要使用到 cloneWithRowsAndSections 方法将 dataBlob(object), sectionIDs (array), rowIDs (array) 三个值传递出去

    • dataBlob:包含ListView所需的所有数据(section header 和 rows),在ListView渲染数据时,使用getSectionData 和 getRowData 来渲染每一行数据。 dataBlob 的 key 值包含 sectionID + rowId,参考下面模拟的数据结构


        var dataBlob = {
            'sectionID1' : {section1 data},
            'sectionID1:rowID0' : {row0 data},
            'sectionID1:rowID1' : {row1 data},
            'sectionID2' : {section1 data},
            'sectionID2:rowID0' : {row0 data},
            'sectionID2:rowID1' : {row1 data},
            'sectionID2:rowID2' : {row2 data},
            ...
        };
    
    
    • sectionIDs:sectionIDs 用于标识每组section,参考下面模拟的数据结构


        var sectionIDs = ['sectionID0','sectionID1','sectionID2', ...];
    
    
    • rowIDs:rowIDs 用于描述每个 section 里的每行数据的位置及是否需要渲染。在ListView渲染时,会先遍历 rowIDs 获取到对应的 dataBlob 数据,参考下面模拟的数据结构


        var rowIDs = [['rowID0', 'rowID1', 'rowID2'...], ['rowID0', 'rowID1', ...], ['rowID0', 'rowID1'], ...];
    
    

ListView 分组样式实现


  • 上面我们大概地分析了下 ListView 实现分组的原理,接下来就根据上面的分析加上实际的案例,来更直观地体验下 ListView分组功能的实现

  • 首先,因为要分组,所以数据肯定比之前的案例使用到的要复杂,但是不用担心,这边会尽量详细地将数组的处理表述出来,先来看下我们需要使用到的数据

        {
            "data":[
                {
                    "title":"A",
                    "icons":[
                        {
                            "icon" : "icon"
                            },
                        {
                            "icon" : "lufei"
                        },
                        {
                            "icon" : "icon"
                        }
                    ]
                },
                {
                    "title":"B",
                    "icons":[
                        {
                            "icon" : "icon"
                        },
                        {
                            "icon" : "lufei"
                        },
                        {
                            "icon" : "icon"
                        },
                        {
                            "icon" : "lufei"
                        }
                    ]
                },
                {
                    "title":"C",
                    "icons":[
                        {
                            "icon" : "icon"
                        },
                        {
                            "icon" : "lufei"
                        }
                    ]
                },
                {
                    "title":"D",
                    "icons":[
                        {
                            "icon" : "icon"
                        },
                        {
                            "icon" : "lufei"
                        },
                        {
                            "icon" : "icon"
                        },
                        {
                            "icon" : "lufei"
                        }
                    ]
                },
                {
                    "title":"E",
                    "icons":[
                        {
                            "icon" : "icon"
                        },
                        {
                            "icon" : "lufei"
                        },
                        {
                            "icon" : "lufei"
                        }
                    ]
                },
                {
                    "title":"F",
                    "icons":[
                        {
                            "icon" : "icon"
                        },
                        {
                            "icon" : "lufei"
                        }
                    ]
                }
        
            ]
        }
    
    
  • 结合上面的分析,我们先来初始化数据源

        getInitialState(){
            // 初始化getSectionData
            var getSectionData = (dataBlob, sectionID) => {
                return dataBlob[sectionID];
            };
            
            // 初始化getRowData
            var getRowData = (dataBlob, sectionID, rowID) => {
                return dataBlob[sectionID + ':' + rowID];
            };
    
            return {
                // 初始化数据源
                dataSource: new ListView.DataSource({
                    getSectionData : getSectionData,
                    getRowData : getRowData,
                    rowHasChanged : (r1, r2) => r1 !== r2,
                    sectionHeaderHasChanged : (s1, s2) => s1 !== s2
                })
            }
        },
        
    
  • 接着是数组的解析,然后将解析好的数据提供给 dataSource 进行更新,需要注意的是在实际开发中,数据的复杂程度远远要大于我们上面的数据,这是比较耗时的操作,所以我们会选择在异步线程中执行,之前的文章中也提到过 —— 在React Native中,我们一般将耗时复杂的操作放到 componentDidMount 中执行

        // 耗时、复杂操作放到这里处理
        componentDidMount(){
            // 加载数据
            this.loadData();
        },
        
        // 加载数据
        loadData(){
            // 拿到json数据中的数组
            var jsonData = iconData.data;
            // 定义变量
            var dataBlob = {},
                sectionIDs = [],
                rowIDs = [],
                icons = [];
            // 遍历数组中对应的数据并存入变量内
            for (var i = 0; i<jsonData.length; i++){
                // 将组号存入 sectionIDs 中
                sectionIDs.push(i);
                // 将每组头部需要显示的内容存入 dataBlob 中
                dataBlob[i] = jsonData[i].title;
                // 取出该组所有的 icon
                icons = jsonData[i].icons;
                rowIDs[i] = [];
                // 遍历所有 icon
                for (var j = 0; j<icons.length; j++){
                    // 设置标识
                    rowIDs[i].push(j);
                    // 根据标识,将数据存入 dataBlob
                    dataBlob[i + ':' + j] = icons[j];
                }
            }
            // 刷新dataSource状态
            this.setState({             dataSource:this.state.dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs)
            });
        }
    
    
  • 最后,就是设置样式,将布局调到我们想要的效果就可以了

    • 视图部分


        render(){
            return(
                <View style={styles.container}>
                    // 实例化顶部View
                    <View style={styles.topViewStyle}>
                        <Text style={{fontSize:21}}>分组样式</Text>
                    </View>
                    // 实例化ListView
                    <ListView
                        dataSource={this.state.dataSource}
                        renderRow={this.renderRow}
                        renderSectionHeader={this.renderSectionHeader}
                    />
                    </View>
            );
        },
        
        // 返回一个Item
        renderRow(rowData, sectionID, rowID){
            return(
                <View style={styles.itemStyle}>
                    <Image source={{uri:rowData.icon}} style={{width: 60, height:60, marginTop:10, marginLeft:10}}></Image>
                    <Text style={{marginTop:15, marginLeft:10}}>示例</Text>
                </View>
            );
        },
        
        // 返回一个SectionHeader
        renderSectionHeader(sectionData, sectionID){
            return(
                <Text style={{backgroundColor:'yellow'}}>{sectionData}</Text>
            );
        },
    
    
    • 样式部分


        var styles = StyleSheet.create({
            container:{
                flex:1
            },
    
            topViewStyle: {
                // 尺寸
                height:44,
                // 边距
                marginTop:20,
                // 对齐方式
                justifyContent:'center',
                alignItems:'center'
            },
    
            itemStyle: {
                // 尺寸
                height:80,
                // 主轴方向
                flexDirection:'row',
                // 下边框
                borderBottomWidth:1,
                borderBottomColor:'gray'
            },
        });
    
    

    效果:


    ListView分组样式
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容