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 toggle()设置CSS样式
Nov 05 Javascript
JavaScript获取onclick、onchange等事件值的代码
Jul 22 Javascript
JS关键字球状旋转效果的实例代码
Nov 29 Javascript
json字符串之间的相互转换示例代码
Aug 21 Javascript
情人节单身的我是如何在敲完代码之后收到12束玫瑰的(javascript)
Aug 21 Javascript
学习JavaScript设计模式之策略模式
Jan 12 Javascript
vue实现商品加减计算总价的实例代码
Aug 12 Javascript
Vue 3.x+axios跨域方案的踩坑指南
Jul 04 Javascript
浅谈JavaScript中的“!!”作用
Aug 03 Javascript
Postman无法正常返回结果问题解决
Aug 28 Javascript
使用vue3重构拼图游戏的实现示例
Jan 25 Vue.js
js实现模拟购物商城案例
May 18 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
修改了一个很不错的php验证码(支持中文)
2007/02/14 PHP
php 远程图片保存到本地的函数类
2008/12/08 PHP
PHP教程 变量定义
2009/10/23 PHP
php获取mysql数据库中的所有表名的代码
2011/04/23 PHP
10条php编程小技巧
2015/07/07 PHP
javascript编程起步(第一课)
2007/01/10 Javascript
番茄的表单验证类代码修改版
2008/07/18 Javascript
只需20行代码就可以写出CSS覆盖率测试脚本
2013/04/24 Javascript
jQuery中prevAll()方法用法实例
2015/01/08 Javascript
javascript实现控制浏览器全屏
2015/03/30 Javascript
JavaScript中判断数据类型的方法总结
2016/05/24 Javascript
Angular2学习笔记——详解路由器模型(Router)
2016/12/02 Javascript
Angular中响应式表单的三种更新值方法详析
2017/08/22 Javascript
Node.js学习之查询字符串解析querystring详解
2017/09/28 Javascript
使用clipboard.js实现复制功能的示例代码
2017/10/16 Javascript
vue的.vue文件是怎么run起来的(vue-loader)
2018/12/10 Javascript
Vue项目从webpack3.x升级webpack4不完全指南
2019/04/28 Javascript
webpack实践之DLLPlugin 和 DLLReferencePlugin的使用教程
2019/06/10 Javascript
Vue实现按钮级权限方案
2019/11/21 Javascript
Python3中FuzzyWuzzy库实例用法
2020/11/18 Python
CSS3绘制有活力的链接下划线
2016/07/14 HTML / CSS
令人啧啧称赞的经理推荐信
2013/11/07 职场文书
拓展培训心得体会
2014/01/04 职场文书
消防安全汇报材料
2014/02/08 职场文书
保护环境建议书300字
2014/05/13 职场文书
工作会议方案
2014/05/21 职场文书
大学生村官考核材料
2014/05/23 职场文书
秋季运动会演讲稿
2014/09/16 职场文书
求职推荐信范文
2015/03/27 职场文书
生产车间管理制度
2015/08/04 职场文书
教师节随笔
2015/08/15 职场文书
人生感悟经典句子
2019/08/20 职场文书
祝福语集锦:朋友新店开业祝福语
2019/12/10 职场文书
Spring Boot 启动、停止、重启、状态脚本
2021/06/26 Java/Android
mysql 乱码 字符集latin1转UTF8
2022/04/19 MySQL
利用Redis实现点赞功能的示例代码
2022/06/28 Redis