欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-06-25 13:30:03
在rn中flatlist是一个高性能的列表组件,它是listview组件的升级版,性能方面有了很大的提升,当然也就建议大家在实现列表功能时使用flatlist,尽量不要使用...

在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)。当然上面的代码也很简单,相信很容易看懂,其它就不多介绍了。以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。