天天看点

React Native学习五- FlatList

高性能的简单列表组件,支持下面这些常用的功能:

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

如果需要分组/类/区(section),请使用

<SectionList>

一个最简单的例子:

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

下面是一个较复杂的例子,其中演示了如何利用

PureComponent

来进一步优化性能和减少bug产生的可能。

  • By binding the 

    onPressItem

     handler, the props will remain 

    ===

     and 

    PureComponent

     will prevent wasteful re-renders unless the actual 

    id

    selected

    , or 

    title

     props change, even if the inner 

    SomeOtherWidget

     has no such optimizations.
  • By passing 

    extraData={this.state}

     to 

    FlatList

     we make sure 

    FlatList

     itself will re-render when the 

    state.selected

     changes. Without setting this prop, 

    FlatList

     would not know it needs to re-render any items because it is also a 

    PureComponent

     and the prop comparison will not show any changes.
  • keyExtractor

     tells the list to use the ids for the react keys.
class MyListItem extends React.PureComponent {
  _onPress = () => {
    this.props.onPressItem(this.props.id);
  };

  render() {
    return (
      <SomeOtherWidget
        {...this.props}
        onPress={this._onPress}
      />
    )
  }
}

class MyList extends React.PureComponent {
  state = {selected: (new Map(): Map<string, boolean>)};

  _keyExtractor = (item, index) => item.id;

  _onPressItem = (id: string) => {
    // updater functions are preferred for transactional updates
    this.setState((state) => {
      // copy the map rather than modifying state.
      const selected = new Map(state.selected);
      selected.set(id, !state.get(id)); // toggle
      return {selected};
    });
  };

  _renderItem = ({item}) => (
    <MyListItem
      id={item.id}
      onPressItem={this._onPressItem}
      selected={!!this.state.selected.get(item.id)}
      title={item.title}
    />
  );

  render() {
    return (
      <FlatList
        data={this.props.data}
        extraData={this.state}
        keyExtractor={this._keyExtractor}
        renderItem={this._renderItem}
      />
    );
  }
}      

本组件实质是基于

<VirtualizedList>

组件的封装,因此也有下面这些需要注意的事项:

  • 当某行滑出渲染区域之外后,其内部状态将不会保留。请确保你在行组件以外的地方保留了数据。
  • 为了优化内存占用同时保持滑动的流畅,列表内容会在屏幕外异步绘制。这意味着如果用户滑动的速度超过渲染的速度,则会先看到空白的内容。这是为了优化不得不作出的妥协,而我们也在设法持续改进。
  • 本组件继承自

    PureComponent

    而非通常的

    Component

    ,这意味着如果其

    props

    浅比较

    中是相等的,则不会重新渲染。所以请先检查你的

    renderItem

    函数所依赖的

    props

    数据(包括

    data

    属性以及可能用到的父组件的state),如果是一个引用类型(Object或者数组都是引用类型),则需要先修改其引用地址(比如先复制到一个新的Object或者数组中),然后再修改其值,否则界面很可能不会刷新。(译注:这一段不了解的朋友建议先学习下js中的基本类型和引用类型。)
  • 默认情况下每行都需要提供一个不重复的key属性。你也可以提供一个

    keyExtractor

    函数来生成key。

注意:

removeClippedSubviews

属性目前是不必要的,而且可能会引起问题。如果你在某些场景碰到内容不渲染的情况(比如使用

LayoutAnimation

时),尝试设置

removeClippedSubviews={false}

。我们可能会在将来的版本中修改此属性的默认值。

属性

ItemSeparatorComponent?: 

?ReactClass<any>

 #

行与行之间的分隔线组件。不会出现在第一行之前和最后一行之后。�

ListFooterComponent?: 

?ReactClass<any>

 #

尾部组件

ListHeaderComponent?: 

?ReactClass<any>

 #

头部组件

columnWrapperStyle?: 

StyleObj

 #

如果设置了多列布局(即将

numColumns

值设为大于1的整数),则可以额外指定此样式作用在每行容器上。

data: 

?Array<ItemT>

 #

为了简化起见,data属性目前只支持普通数组。如果需要使用其他特殊数据结构,例如immutable数组,请直接使用更底层的

VirtualizedList

组件。

extraData?: any #

A marker property for telling the list to re-render (since it implements 

PureComponent

). If any of your 

renderItem

, Header, Footer, etc. functions depend on anything outside of the 

data

 prop, stick it here and treat it immutably.

getItem?: #

getItemCount?: #

getItemLayout?: 

(data: ?Array<ItemT>, index: number) => {length: number, offset: number, index: number}

 #

getItemLayout

是一个可选的优化,用于避免动态测量内容尺寸的开销,不过前提是你可以提前知道内容的高度。如果你的行高是固定的,

getItemLayout

用起来就既高效又简单,类似下面这样:

getItemLayout={(data, index) => ( {length: 行高, offset: 行高 * index, index} )}

注意如果你指定了

SeparatorComponent

,请把分隔线的尺寸也考虑到offset的计算之中。

horizontal?: 

?boolean

 #

设置为true则变为水平布局模式。

initialNumToRender: number #

How many items to render in the initial batch. This should be enough to fill the screen but not much more. Note these items will never be unmounted as part of the windowed rendering in order to improve perceived performance of scroll-to-top actions.

keyExtractor: 

(item: ItemT, index: number) => string

 #

此函数用于为给定的item生成一个不重复的key。Key的作用是使React能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。若不指定此函数,则默认抽取

item.key

作为key值。若

item.key

也不存在,则使用数组下标。

legacyImplementation?: 

?boolean

 #

设置为true则使用旧的ListView的实现。

numColumns: 

number

 #

多列布局只能在非水平模式下使用,即必须是

horizontal={false}

。此时组件内元素会从左到右从上到下按Z字形排列,类似启用了

flexWrap

的布局。组件内元素必须是等高的——暂时还无法支持瀑布流布局。

onEndReached?: 

?(info: {distanceFromEnd: number}) => void

 #

当所有的数据都已经渲染过,并且列表被滚动到距离最底部不足

onEndReachedThreshold

个像素的距离时调用。

onEndReachedThreshold?: 

?number

 #

onRefresh?: 

?() => void

 #

如果设置了此选项,则会在列表头部添加一个标准的

RefreshControl

控件,以便实现“下拉刷新”的功能。同时你需要正确设置

refreshing

属性。

onViewableItemsChanged?: 

?(info: {viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void

 #

Called when the viewability of rows changes, as defined by the 

viewablePercentThreshold

 prop.

refreshing?: 

?boolean

 #

Set this true while waiting for new data from a refresh.

renderItem: 

(info: {item: ItemT, index: number}) => ?React.Element<any>

 #

根据行数据

data

渲染每一行的组件。典型用法:

_renderItem = ({item}) => ( <TouchableOpacity onPress={() => this._onPress(item)}> <Text>{item.title}}</Text> </TouchableOpacity> ); ... <FlatList data={[{title: 'Title Text', key: 'item1'}]} renderItem={this._renderItem} />

data

外还有第二个参数

index

可供使用。

viewabilityConfig?: 

ViewabilityConfig

 #

请参考

ViewabilityHelper

的源码来了解具体的配置类型。

方法

scrollToEnd(params?: object) #

滚动到底部。如果不设置

getItemLayout

属性的话,可能会比较卡。

scrollToIndex(params: object) #

Scrolls to the item at a the specified index such that it is positioned in the viewable area such that 

viewPosition

 0 places it at the top, 1 at the bottom, and 0.5 centered in the middle.

如果不设置

getItemLayout

属性的话,可能会比较卡。

scrollToItem(params: object) #

Requires linear scan through data - use 

scrollToIndex

 instead if possible. 如果不设置

getItemLayout

属性的话,可能会比较卡。

scrollToOffset(params: object) #

Scroll to a specific content pixel offset, like a normal 

ScrollView

.

recordInteraction() #

Tells the list an interaction has occured, which should trigger viewability calculations, e.g. if 

waitForInteractions

 is true and the user has not scrolled. This is typically called by taps on items or by navigation actions.

例子

'use strict';

const React = require('react');
const ReactNative = require('react-native');
const {
  Animated,
  FlatList,
  StyleSheet,
  View,
} = ReactNative;

const UIExplorerPage = require('./UIExplorerPage');

const infoLog = require('infoLog');

const {
  FooterComponent,
  HeaderComponent,
  ItemComponent,
  PlainInput,
  SeparatorComponent,
  Spindicator,
  genItemData,
  getItemLayout,
  pressItem,
  renderSmallSwitchOption,
} = require('./ListExampleShared');

const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);

const VIEWABILITY_CONFIG = {
  minimumViewTime: 3000,
  viewAreaCoveragePercentThreshold: 100,
  waitForInteraction: true,
};

class FlatListExample extends React.PureComponent {
  static title = '<FlatList>';
  static description = 'Performant, scrollable list of data.';

  state = {
    data: genItemData(100),
    debug: false,
    horizontal: false,
    filterText: '',
    fixedHeight: true,
    logViewable: false,
    virtualized: true,
  };

  _onChangeFilterText = (filterText) => {
    this.setState({filterText});
  };

  _onChangeScrollToIndex = (text) => {
    this._listRef.getNode().scrollToIndex({viewPosition: 0.5, index: Number(text)});
  };

  _scrollPos = new Animated.Value(0);
  _scrollSinkX = Animated.event(
    [{nativeEvent: { contentOffset: { x: this._scrollPos } }}],
    {useNativeDriver: true},
  );
  _scrollSinkY = Animated.event(
    [{nativeEvent: { contentOffset: { y: this._scrollPos } }}],
    {useNativeDriver: true},
  );

  componentDidUpdate() {
    this._listRef.getNode().recordInteraction(); // e.g. flipping logViewable switch
  }

  render() {
    const filterRegex = new RegExp(String(this.state.filterText), 'i');
    const filter = (item) => (
      filterRegex.test(item.text) || filterRegex.test(item.title)
    );
    const filteredData = this.state.data.filter(filter);
    return (
      <UIExplorerPage
        noSpacer={true}
        noScroll={true}>
        <View style={styles.searchRow}>
          <View style={styles.options}>
            <PlainInput
              onChangeText={this._onChangeFilterText}
              placeholder="Search..."
              value={this.state.filterText}
            />
            <PlainInput
              onChangeText={this._onChangeScrollToIndex}
              placeholder="scrollToIndex..."
            />
          </View>
          <View style={styles.options}>
            {renderSmallSwitchOption(this, 'virtualized')}
            {renderSmallSwitchOption(this, 'horizontal')}
            {renderSmallSwitchOption(this, 'fixedHeight')}
            {renderSmallSwitchOption(this, 'logViewable')}
            {renderSmallSwitchOption(this, 'debug')}
            <Spindicator value={this._scrollPos} />
          </View>
        </View>
        <SeparatorComponent />
        <AnimatedFlatList
          ItemSeparatorComponent={SeparatorComponent}
          ListHeaderComponent={HeaderComponent}
          ListFooterComponent={FooterComponent}
          data={filteredData}
          debug={this.state.debug}
          disableVirtualization={!this.state.virtualized}
          getItemLayout={this.state.fixedHeight ?
            this._getItemLayout :
            undefined
          }
          horizontal={this.state.horizontal}
          key={(this.state.horizontal ? 'h' : 'v') +
            (this.state.fixedHeight ? 'f' : 'd')
          }
          legacyImplementation={false}
          numColumns={1}
          onEndReached={this._onEndReached}
          onRefresh={this._onRefresh}
          onScroll={this.state.horizontal ? this._scrollSinkX : this._scrollSinkY}
          onViewableItemsChanged={this._onViewableItemsChanged}
          ref={this._captureRef}
          refreshing={false}
          renderItem={this._renderItemComponent}
          viewabilityConfig={VIEWABILITY_CONFIG}
        />
      </UIExplorerPage>
    );
  }
  _captureRef = (ref) => { this._listRef = ref; };
  _getItemLayout = (data: any, index: number) => {
    return getItemLayout(data, index, this.state.horizontal);
  };
  _onEndReached = () => {
    this.setState((state) => ({
      data: state.data.concat(genItemData(100, state.data.length)),
    }));
  };
  _onRefresh = () => alert('onRefresh: nothing to refresh :P');
  _renderItemComponent = ({item}) => {
    return (
      <ItemComponent
        item={item}
        horizontal={this.state.horizontal}
        fixedHeight={this.state.fixedHeight}
        onPress={this._pressItem}
      />
    );
  };
  // This is called when items change viewability by scrolling into or out of
  // the viewable area.
  _onViewableItemsChanged = (info: {
      changed: Array<{
        key: string,
        isViewable: boolean,
        item: any,
        index: ?number,
        section?: any,
      }>
    }
  ) => {
    // Impressions can be logged here
    if (this.state.logViewable) {
      infoLog(
        'onViewableItemsChanged: ',
        info.changed.map((v) => ({...v, item: '...'})),
      );
    }
  };
  _pressItem = (key: number) => {
    this._listRef.getNode().recordInteraction();
    pressItem(this, key);
  };
  _listRef: FlatList<*>;
}


const styles = StyleSheet.create({
  options: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    alignItems: 'center',
  },
  searchRow: {
    paddingHorizontal: 10,
  },
});

module.exports = FlatListExample;      

继续阅读