FlatList
高性能的简单列表组件,支持下面这些常用的功能:
- 完全跨平台。
- 支持水平布局模式。
- 行组件显示或隐藏时可配置回调事件。
- 支持单独的头部组件。
- 支持单独的尾部组件。
- 支持自定义行间分隔线。
- 支持下拉刷新。
- 支持上拉加载。
- 支持跳转到指定行(ScrollToIndex)。
如果需要分组/类/区(section),请使用<SectionList>。
一个最简单的例子:
<FlatList
data={[{key: 'a'}, {key: 'b'}]}
renderItem={({item}) => <Text>{item.key}</Text>}
/>
下面是一个较复杂的例子,其中演示了如何利用PureComponent来进一步优化性能和减少bug产生的可能(以下这段文字需要你深刻理解shouldComponentUpdate的机制,以及Component和PureComponent的不同,所以如果不了解就先跳过吧)。
- 对于
MyListItem组件来说,其onPressItem属性使用箭头函数而非bind的方式进行绑定,使其不会在每次列表重新render时生成一个新的函数,从而保证了props的不变性(当然前提是id、selected和title也没变),不会触发自身无谓的重新render。换句话说,如果你是用bind来绑定onPressItem,每次都会生成一个新的函数,导致props在===比较时返回false,从而触发自身的一次不必要的重新render。 - 给
FlatList指定extraData={this.state}属性,是为了保证state.selected变化时,能够正确触发FlatList的更新。如果不指定此属性,则FlatList不会触发更新,因为它是一个PureComponent,其props在===比较中没有变化则不会触发更新。 keyExtractor属性指定使用id作为列表每一项的key。
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, !selected.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> #
行与行之间的分隔线组件。不会出现在第一行之前和最后一行之后。
ListEmptyComponent?: ?ReactClass<any> | React.Element<any> #
列表为空时渲染该组件。可以是React Component, 也可以是一个render函数, 或者渲染好的element。
ListFooterComponent?: ?ReactClass<any> #
尾部组件
ListHeaderComponent?: ?ReactClass<any> #
头部组件
columnWrapperStyle?: StyleObj #
如果设置了多列布局(即将numColumns值设为大于1的整数),则可以额外指定此样式作用在每行容器上。
data: ?Array<ItemT>
#
为了简化起见,data属性目前只支持普通数组。如果需要使用其他特殊数据结构,例如immutable数组,请直接使用更底层的VirtualizedList组件。
extraData?: any #
如果有除data以外的数据用在列表中(不论是用在renderItem还是Header或者Footer中),请在此属性中指定。同时此数据在修改时也需要先修改其引用地址(比如先复制到一个新的Object或者数组中),然后再修改其值,否则界面很可能不会刷新。
getItem?: #
getItemCount?: #
getItemLayout?: (data: ?Array<ItemT>, index: number) =>
{length: number, offset: number, index: number} #
getItemLayout是一个可选的优化,用于避免动态测量内容尺寸的开销,不过前提是你可以提前知道内容的高度。如果你的行高是固定的,getItemLayout用起来就既高效又简单,类似下面这样:
注意如果你指定了SeparatorComponent,请把分隔线的尺寸也考虑到offset的计算之中。
horizontal?: ?boolean
#
设置为true则变为水平布局模式。
initialNumToRender: number #
指定一开始渲染的元素数量,最好刚刚够填满一个屏幕,这样保证了用最短的时间给用户呈现可见的内容。注意这第一批次渲染的元素不会在滑动过程中被卸载,这样是为了保证用户执行返回顶部的操作时,不需要重新渲染首批元素。
initialScrollIndex?: ?number #
开始时屏幕顶端的元素是列表中的第 initialScrollIndex 个元素, 而不是第一个元素。设置这个属性会关闭对“滚动到顶端”这个动作的优化(参见VirtualizedList 的 initialNumToRender 属性)。位于 initialScrollIndex 位置的元素总是会被立刻渲染。需要先设置 getItemLayout 属性。
inverted?: ?boolean #
翻转滚动方向。实质是将scale变换设置为-1。
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 #
决定当距离内容最底部还有多远时触发onEndReached回调。注意此参数是一个比值而非像素单位。比如,0.5表示距离内容最底部的距离为当前列表可见长度的一半时触发。
onRefresh?: ?() => void #
如果设置了此选项,则会在列表头部添加一个标准的RefreshControl控件,以便实现“下拉刷新”的功能。同时你需要正确设置refreshing属性。
onViewableItemsChanged?:
?(info: {viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void
#
在可见行元素变化时调用。可见范围和变化频率等参数的配置请设置viewabilityconfig属性
refreshing?: ?boolean
#
在等待加载新数据时将此属性设为true,列表就会显示出一个正在加载的符号。
renderItem: (info: {item: ItemT, index: number}) => ?React.Element<any> #
根据行数据data渲染每一行的组件。典型用法:
<FlatList
ItemSeparatorComponent={Platform.OS !== 'android' && ({highlighted}) => (<View style={[style.separator, highlighted && {marginLeft: 0}]} /> )}
data={[{title: 'Title Text', key: 'item1'}]}
renderItem={({item, separators}) => (
<TouchableHighlight
onPress={() => this._onPress(item)}
onShowUnderlay={separators.highlight}
onHideUnderlay={separators.unhighlight}>
<View style={{backgroundColor: 'white'}}>
<Text>{item.title}}</Text>
</View>
</TouchableHighlight>
)}
/>
如果需要的话,你可以设置 index 属性的值。同样,如果 highlight 和 unhightlight(这两个方法设置 highlighted: boolean 属性) 不能满足你的要求的话,你也可以提供一个更加通用的 separators.updateProps 方法。通过这个方法,你可以设置一些属性来改变列表的顶部分隔符和底部分隔符的样式。
viewabilityConfig?: ViewabilityConfig #
请参考ViewabilityHelper的源码来了解具体的配置。
androidprogressViewOffset?: number #
当需要在指定的偏移内显示加载指示器的时候,就可以设置这个值。
方法
scrollToEnd(params?: object) #
滚动到底部。如果不设置getItemLayout属性的话,可能会比较卡。
scrollToIndex(params: object) #
将位于指定位置的元素滚动到可视区的指定位置,当 viewPosition 为 0 时将它滚动到屏幕顶部,为 1 时将它滚动到屏幕底部,为 0.5 时将它滚动到屏幕中央。
如果不设置getItemLayout属性的话,无法跳转到当前可视区域以外的位置。
scrollToItem(params: object) #
这个方法会顺序遍历元素。尽可能使用 scrollToIndex 。 如果不设置getItemLayout属性的话,可能会比较卡。
scrollToOffset(params: object) #
滚动列表到指定的偏移(以像素为单位),等同于 ScrollView 的 scrollTo 方法。
recordInteraction() #
主动通知列表发生了一个事件,以使列表重新计算可视区域。比如说当waitForInteractions 为 true 并且用户没有滚动列表时,就可以调用这个方法。不过一般来说,当用户点击了一个列表项,或发生了一个导航动作时,我们就可以调用这个方法。
flashScrollIndicators() #
短暂地显示滚动指示器。
例子
'use strict';
const React = require('react');
const ReactNative = require('react-native');
const {
Animated,
FlatList,
StyleSheet,
View,
} = ReactNative;
const RNTesterPage = require('./RNTesterPage');
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 (
<RNTesterPage
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}
/>
</RNTesterPage>
);
}
_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;