ReactNative 之FlatList使用及踩坑封装总结


Posted in Javascript onNovember 29, 2017

在RN中FlatList是一个高性能的列表组件,它是ListView组件的升级版,性能方面有了很大的提升,当然也就建议大家在实现列表功能时使用FlatList,尽量不要使用ListView,更不要使用ScrollView。既然说到FlatList,那就先温习一下它支持的功能。

  1. 完全跨平台。
  2. 支持水平布局模式。
  3. 行组件显示或隐藏时可配置回调事件。
  4. 支持单独的头部组件。
  5. 支持单独的尾部组件。
  6. 支持自定义行间分隔线。
  7. 支持下拉刷新。
  8. 支持上拉加载。
  9. 支持跳转到指定行(ScrollToIndex)。

今天的这篇文章不具体介绍如何使用,如果想看如何使用,可以参考我GitHub https://github.com/xiehui999/helloReactNative的一些示例。今天的这篇文章主要介绍我使用过程中感觉比较大的坑,并对FlatList进行的二次封装。

接下来,我们先来一个简单的例子。我们文章也有这个例子开始探讨。

<FlatList
      data={this.state.dataList} extraData={this.state}
      refreshing={this.state.isRefreshing}
      onRefresh={() => this._onRefresh()}
      keyExtractor={(item, index) => item.id}
      ItemSeparatorComponent={() => <View style={{
        height: 1,
        backgroundColor: '#D6D6D6'
      }}/>}
      renderItem={this._renderItem}
      ListEmptyComponent={this.emptyComponent}/>
      
      
  //定义空布局
    emptyComponent = () => {
    return <View style={{
      height: '100%',
      alignItems: 'center',
      justifyContent: 'center',
    }}>
      <Text style={{
        fontSize: 16
      }}>暂无数据下拉刷新</Text>
    </View>
  }

在上面的代码,我们主要看一下ListEmptyComponent,它表示没有数据的时候填充的布局,一般情况我们会在中间显示显示一个提示信息,为了介绍方便就简单展示一个暂无数据下拉刷新。上面代码看起来是暂无数据居中显示,但是运行后,你傻眼了,暂无数据在最上面中间显示,此时高度100%并没有产生效果。当然你尝试使用flex:1,将View的高视图填充剩余全屏,不过依然没有效果。

那为什么设置了没有效果呢,既然好奇,我们就来去源码看一下究竟。源码路径在react-native-->Libraries-->Lists。列表的组件都该目录下。我们先去FlatList文件搜索关键词ListEmptyComponent,发现该组件并没有被使用,那就继续去render

render() {
  if (this.props.legacyImplementation) {
   return (
    <MetroListView
     {...this.props}
     items={this.props.data}
     ref={this._captureRef}
    />
   );
  } else {
   return (
    <VirtualizedList
     {...this.props}
     renderItem={this._renderItem}
     getItem={this._getItem}
     getItemCount={this._getItemCount}
     keyExtractor={this._keyExtractor}
     ref={this._captureRef}
     onViewableItemsChanged={
      this.props.onViewableItemsChanged && this._onViewableItemsChanged
     }
    />
   );
  }
 }

MetroListView(内部实行是ScrollView)是旧的ListView实现方式,VirtualizedList是新的性能比较好的实现。我们去该文件

//省略部分代码
  const itemCount = this.props.getItemCount(data);
  if (itemCount > 0) {
    ....省略部分代码
  } else if (ListEmptyComponent) {
   const element = React.isValidElement(ListEmptyComponent)
    ? ListEmptyComponent // $FlowFixMe
    : <ListEmptyComponent />;
   cells.push(
    /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This
     * comment suppresses an error when upgrading Flow's support for React.
     * To see the error delete this comment and run Flow. */
    <View
     key="$empty"
     onLayout={this._onLayoutEmpty}
     style={inversionStyle}>
     {element}
    </View>,
   );
  }

再此处看到我们定义的ListEmptyComponent外面包了一层view,该view加了样式inversionStyle。

const inversionStyle = this.props.inverted
   ? this.props.horizontal
    ? styles.horizontallyInverted
    : styles.verticallyInverted
   : null;
   
样式:
verticallyInverted: {
  transform: [{scaleY: -1}],
 },
 horizontallyInverted: {
  transform: [{scaleX: -1}],
 },

上面的样式就是添加了一个动画,并没有设置高度,所以我们在ListEmptyComponent使用height:'100%'或者flex:1都没有效果,都没有撑起高度。

为了实现我们想要的效果,我们需要将height设置为具体的值。那么该值设置多大呢?你如果给FlatList设置一个样式,背景属性设置一个颜色,发现FlatList是默认有占满剩余屏的高度的(flex:1)。那么我们可以将ListEmptyComponent中view的高度设置为FlatList的高度,要获取FlatList的高度,我们可以通过onLayout获取。

代码调整:

//创建变量
fHeight = 0;

    <FlatList
      data={this.state.dataList} extraData={this.state}
      refreshing={this.state.isRefreshing}
      onRefresh={() => this._onRefresh()}
      keyExtractor={(item, index) => item.id}
      ItemSeparatorComponent={() => <View style={{
        height: 1,
        backgroundColor: '#D6D6D6'
      }}/>}
      renderItem={this._renderItem}
      onLayout={e => this.fHeight = e.nativeEvent.layout.height}
      ListEmptyComponent={this.emptyComponent}/>
      
      
  //定义空布局
    emptyComponent = () => {
    return <View style={{
      height: this.fHeight,
      alignItems: 'center',
      justifyContent: 'center',
    }}>
      <Text style={{
        fontSize: 16
      }}>暂无数据</Text>
    </View>
  }

通过上面的调整发现在Android上运行时达到我们想要的效果了,但是在iOS上,不可控,偶尔居中显示,偶尔又显示到最上面。原因就是在iOS上onLayout调用的时机与Android略微差别(iOS会出现emptyComponent渲染时onLayout还没有回调,此时fHeight还没有值)。

所以为了将变化后的值作用到emptyComponent,我们将fHeight设置到state中

state={
  fHeight:0
}

onLayout={e => this.setState({fHeight: e.nativeEvent.layout.height})}

这样设置后应该完美了吧,可是....在android上依然能完美实现我们要的效果,在iOS上出现了来回闪屏的的问题。打印log发现值一直是0和测量后的值来回转换。在此处我们仅仅需要是测量的值,所以我们修改onLayout

onLayout={e => {
             let height = e.nativeEvent.layout.height;
             if (this.state.fHeight < height) {
               this.setState({fHeight: height})
             }
           }}

经过处理后,在ios上终于完美的实现我们要的效果了。

除了上面的坑之外,个人感觉还有一个坑就是onEndReached,如果我们实现下拉加载功能,都会用到这个属性,提到它我们当然就要提到onEndReachedThreshold,在FlatList中onEndReachedThreshold是一个number类型,是一个他表示具体底部还有多远时触发onEndReached,需要注意的是FlatList和ListView中的onEndReachedThreshold表示的含义是不同的,在ListView中onEndReachedThreshold表示具体底部还有多少像素时触发onEndReached,默认值是1000。而FlatList中表示的是一个倍数(也称比值,不是像素),默认值是2。

那么按照常规我们看下面实现

<FlatList
        data={this.state.dataList}
        extraData={this.state}
        refreshing={this.state.isRefreshing}
        onRefresh={() => this._onRefresh()}
        ItemSeparatorComponent={() => <View style={{
          height: 1,
          backgroundColor: '#D6D6D6'
        }}/>}
        renderItem={this._renderItem}
        ListEmptyComponent={this.emptyComponent}
        onEndReached={() => this._onEndReached()}
        onEndReachedThreshold={0.1}/>

然后我们在componentDidMount中加入下面代码

componentDidMount() {
    this._onRefresh()
  }

也就是进入开始加载第一页数据,下拉的执行onEndReached加载更多数据,并更新数据源dataList。看起来是完美的,不过.....运行后你会发现onEndReached一直循环调用(或多次执行),有可能直到所有数据加载完成,原因可能大家也能猜到了,因为_onRefresh加载数据需要时间,在数据请求到之前render方法执行,由于此时没有数据,onEndReached方法执行一次,那么此时相当于加载了两次数据。

至于onEndReached执行多少次就需要onEndReachedThreshold的值来定了,所以我们一定要慎重设置onEndReachedThreshold,如果你要是理解成了设置像素,设置成了一个比较大的数,比如100,那完蛋了....个人感觉设置0.1是比较好的值。

通过上面的分析,个人感觉有必要对FlatList进行一次二次封装了,根据自己的需求我进行了一次二次封装

import React, {
  Component,
} from 'react'
import {
  FlatList,
  View,
  StyleSheet,
  ActivityIndicator,
  Text
} from 'react-native'
import PropTypes from 'prop-types';

export const FlatListState = {
  IDLE: 0,
  LoadMore: 1,
  Refreshing: 2
};
export default class Com extends Component {
  static propTypes = {
    refreshing: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
  };
  state = {
    listHeight: 0,
  }

  render() {
    var {ListEmptyComponent,ItemSeparatorComponent} = this.props;
    var refreshing = false;
    var emptyContent = null;
    var separatorComponent = null
    if (ListEmptyComponent) {
      emptyContent = React.isValidElement(ListEmptyComponent) ? ListEmptyComponent : <ListEmptyComponent/>
    } else {
      emptyContent = <Text style={styles.emptyText}>暂无数据下拉刷新</Text>;
    }
    if (ItemSeparatorComponent) {
      separatorComponent = React.isValidElement(ItemSeparatorComponent) ? ItemSeparatorComponent :
        <ItemSeparatorComponent/>
    } else {
      separatorComponent = <View style={{height: 1, backgroundColor: '#D6D6D6'}}/>;
    }
    if (typeof this.props.refreshing === "number") {
      if (this.props.refreshing === FlatListState.Refreshing) {
        refreshing = true
      }
    } else if (typeof this.props.refreshing === "boolean") {
      refreshing = this.props.refreshing
    } else if (typeof this.props.refreshing !== "undefined") {
      refreshing = false
    }
    return <FlatList
      {...this.props}
      onLayout={(e) => {
        let height = e.nativeEvent.layout.height;
        if (this.state.listHeight < height) {
          this.setState({listHeight: height})
        }
      }
      }
      ListFooterComponent={this.renderFooter}
      onRefresh={this.onRefresh}
      onEndReached={this.onEndReached}
      refreshing={refreshing}
      onEndReachedThreshold={this.props.onEndReachedThreshold || 0.1}
      ItemSeparatorComponent={()=>separatorComponent}
      keyExtractor={(item, index) => index}
      ListEmptyComponent={() => <View
        style={{
          height: this.state.listHeight,
          width: '100%',
          alignItems: 'center',
          justifyContent: 'center'
        }}>{emptyContent}</View>}
    />
  }

  onRefresh = () => {
    console.log("FlatList:onRefresh");
    if ((typeof this.props.refreshing === "boolean" && !this.props.refreshing) ||
      typeof this.props.refreshing === "number" && this.props.refreshing !== FlatListState.LoadMore &&
      this.props.refreshing !== FlatListState.Refreshing
    ) {
      this.props.onRefresh && this.props.onRefresh()
    }

  };
  onEndReached = () => {
    console.log("FlatList:onEndReached");
    if (typeof this.props.refreshing === "boolean" || this.props.data.length == 0) {
      return
    }
    if (!this.props.pageSize) {
      console.warn("pageSize must be set");
      return
    }
    if (this.props.data.length % this.props.pageSize !== 0) {
      return
    }
    if (this.props.refreshing === FlatListState.IDLE) {
      this.props.onEndReached && this.props.onEndReached()
    }
  };


  renderFooter = () => {
    let footer = null;
    if (typeof this.props.refreshing !== "boolean" && this.props.refreshing === FlatListState.LoadMore) {
      footer = (
        <View style={styles.footerStyle}>
          <ActivityIndicator size="small" color="#888888"/>
          <Text style={styles.footerText}>数据加载中…</Text>
        </View>
      )
    }
    return footer;
  }
}
const styles = StyleSheet.create({
  footerStyle: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    padding: 10,
    height: 44,
  },
  footerText: {
    fontSize: 14,
    color: '#555555',
    marginLeft: 7
  },
  emptyText: {
    fontSize: 17,
    color: '#666666'
  }
})

propTypes中我们使用了oneOfType对refreshing类型进行限定,如果ListEmptyComponent有定义,就是使用自定义分View,同理ItemSeparatorComponent也可以自定义。

在下拉加载数据时定义了一个ListFooterComponent,用于提示用户正在加载数据,refreshing属性如果是boolean的话,表示没有下拉加载功能,如果是number类型,pageSize必须传,数据源长度与pageSize取余是否等于0,判断是否有更多数据(最后一次请求的数据等于pageSize时才有更多数据,小于就不用回调onEndReached)。当然上面的代码也很简单,相信很容易看懂,其它就不多介绍了。以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
基于jQuery实现的百度导航li拖放排列效果,即时更新数据库
Jul 31 Javascript
javascript 全选与全取消功能的实现代码
Dec 23 Javascript
js将字符串转成正则表达式的实现方法
Nov 13 Javascript
Javascript中浮点数相乘的一个解决方法
Jun 03 Javascript
javascript判断数组内是否重复的方法
Apr 21 Javascript
ES6的新特性概览
Mar 10 Javascript
Vue.js第四天学习笔记
Dec 02 Javascript
JS使用面向对象技术实现的tab选项卡效果示例
Feb 28 Javascript
JavaScript评论点赞功能的实现方法
Mar 13 Javascript
微信小程序中hidden不生效原因的解决办法
Apr 26 Javascript
vue单个组件实现无限层级多选菜单功能
Apr 10 Javascript
原生JS实现动态添加新元素、删除元素方法
May 05 Javascript
jQuery图片加载失败替换默认图片方法汇总
Nov 29 #jQuery
jquery animate动画持续运动的实例
Nov 29 #jQuery
angularjs实现时间轴效果的示例代码
Nov 29 #Javascript
bootstrap响应式工具使用详解
Nov 29 #Javascript
JavaScript的setter与getter方法
Nov 29 #Javascript
解析Vue 2.5的Diff算法
Nov 28 #Javascript
javascript+css3开发打气球小游戏完整代码
Nov 28 #Javascript
You might like
Laravel 添加多语言提示信息的方法
2019/09/29 PHP
jQuery对象[0]是什么含义?
2010/07/31 Javascript
Jqyery中同等与js中windows.onload的应用
2011/05/10 Javascript
让你的CSS像Jquery一样做筛选的实现方法
2011/07/10 Javascript
jquery blockUI 遮罩不能消失与不能提交的解决方法
2011/09/17 Javascript
confirm的用法示例用于按钮操作时确定是否执行
2014/06/19 Javascript
浅谈jQuery中setInterval()方法
2015/07/07 Javascript
angularjs创建弹出框实现拖动效果
2020/08/25 Javascript
Node.js 文件夹目录结构创建实例代码
2016/07/08 Javascript
再谈javascript常见错误及解决方法
2016/09/16 Javascript
详解Nodejs的timers模块
2016/12/22 NodeJs
谈谈第三方App接入微信登录 解读
2016/12/27 Javascript
nodejs async异步常用函数总结(推荐)
2017/11/17 NodeJs
浅谈Javascript中的对象和继承
2019/04/19 Javascript
python获取文件后缀名及批量更新目录下文件后缀名的方法
2014/11/11 Python
浅谈Python的文件类型
2016/05/30 Python
python里使用正则表达式的组嵌套实例详解
2017/10/24 Python
Python实现将一个正整数分解质因数的方法分析
2017/12/14 Python
python3+PyQt5 创建多线程网络应用-TCP客户端和TCP服务器实例
2019/06/17 Python
PyTorch实现更新部分网络,其他不更新
2019/12/31 Python
HTML5拖拽文件到浏览器并实现文件上传下载功能代码
2013/06/06 HTML / CSS
BONIA官方网站:国际奢侈品牌和皮革专家
2016/11/27 全球购物
西雅图的买手店:Totokaelo
2019/10/19 全球购物
Kiwi.com中国:找到特价机票并发现新目的地
2019/10/27 全球购物
NYX Professional Makeup英国官网:美国平价专业彩妆品牌
2019/11/13 全球购物
成品仓管员工作职责
2013/12/29 职场文书
致跳高运动员广播稿
2014/01/13 职场文书
保安拾金不昧表扬信
2014/01/15 职场文书
小学教师师德感言
2014/02/10 职场文书
体育运动会广播稿
2014/10/05 职场文书
2014年化验室工作总结
2014/11/21 职场文书
2014年社区宣传工作总结
2014/12/02 职场文书
2015年信息宣传工作总结
2015/05/26 职场文书
天气温馨提示语
2015/07/14 职场文书
python实现局部图像放大
2021/11/17 Python
springboot应用服务启动事件的监听实现
2022/04/06 Java/Android