高性能的简单列表组件,支持下面这些常用的功能:
如果需要分组/类/区(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中的基本类型和引用类型。)keyExtractor
函数来生成key。注意:removeClippedSubviews
属性目前是不必要的,而且可能会引起问题。如果你在某些场景碰到内容不渲染的情况(比如使用LayoutAnimation
时),尝试设置removeClippedSubviews={false}
。我们可能会在将来的版本中修改此属性的默认值。
?ReactClass<any>
# 行与行之间的分隔线组件。不会出现在第一行之前和最后一行之后。
?ReactClass<any> | React.Element<any>
# 列表为空时渲染该组件。可以是React Component, 也可以是一个render函数, 或者渲染好的element。
?ReactClass<any>
# 尾部组件
?ReactClass<any>
# 头部组件
StyleObj
#如果设置了多列布局(即将numColumns
值设为大于1的整数),则可以额外指定此样式作用在每行容器上。
?Array<ItemT>
#为了简化起见,data属性目前只支持普通数组。如果需要使用其他特殊数据结构,例如immutable数组,请直接使用更底层的VirtualizedList
组件。
如果有除data
以外的数据用在列表中(不论是用在renderItem
还是Header或者Footer中),请在此属性中指定。同时此数据在修改时也需要先修改其引用地址(比如先复制到一个新的Object或者数组中),然后再修改其值,否则界面很可能不会刷新。
(data: ?Array<ItemT>, index: number) => {length: number, offset: number, index: number}
#getItemLayout
是一个可选的优化,用于避免动态测量内容尺寸的开销,不过前提是你可以提前知道内容的高度。如果你的行高是固定的,getItemLayout
用起来就既高效又简单,类似下面这样:
注意如果你指定了SeparatorComponent
,请把分隔线的尺寸也考虑到offset的计算之中。
?boolean
#设置为true则变为水平布局模式。
指定一开始渲染的元素数量,最好刚刚够填满一个屏幕,这样保证了用最短的时间给用户呈现可见的内容。注意这第一批次渲染的元素不会在滑动过程中被卸载,这样是为了保证用户执行返回顶部的操作时,不需要重新渲染首批元素。
开始时屏幕顶端的元素是列表中的第 initialScrollIndex
个元素, 而不是第一个元素。设置这个属性会关闭对“滚动到顶端”这个动作的优化(参见VirtualizedList
的 initialNumToRender
属性)。位于 initialScrollIndex
位置的元素总是会被立刻渲染。需要先设置 getItemLayout
属性。
翻转滚动方向。实质是将scale变换设置为-1。
(item: ItemT, index: number) => string
# 此函数用于为给定的item生成一个不重复的key。Key的作用是使React能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。若不指定此函数,则默认抽取item.key
作为key值。若item.key
也不存在,则使用数组下标。
?boolean
#设置为true则使用旧的ListView的实现。
number
# 多列布局只能在非水平模式下使用,即必须是horizontal={false}
。此时组件内元素会从左到右从上到下按Z字形排列,类似启用了flexWrap
的布局。组件内元素必须是等高的——暂时还无法支持瀑布流布局。
?(info: {distanceFromEnd: number}) => void
# 当列表被滚动到距离内容最底部不足onEndReachedThreshold
的距离时调用。
决定当距离内容最底部还有多远时触发onEndReached
回调。注意此参数是一个比值而非像素单位。比如,0.5表示距离内容最底部的距离为当前列表可见长度的一半时触发。
?() => void
#如果设置了此选项,则会在列表头部添加一个标准的RefreshControl
控件,以便实现“下拉刷新”的功能。同时你需要正确设置refreshing
属性。
?(info: {viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void
#在可见行元素变化时调用。可见范围和变化频率等参数的配置请设置viewabilityconfig
属性
?boolean
#在等待加载新数据时将此属性设为true,列表就会显示出一个正在加载的符号。
(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
# 请参考ViewabilityHelper
的源码来了解具体的配置。
当需要在指定的偏移内显示加载指示器的时候,就可以设置这个值。
滚动到底部。如果不设置getItemLayout
属性的话,可能会比较卡。
将位于指定位置的元素滚动到可视区的指定位置,当 viewPosition
为 0 时将它滚动到屏幕顶部,为 1 时将它滚动到屏幕底部,为 0.5 时将它滚动到屏幕中央。
如果不设置getItemLayout
属性的话,无法跳转到当前可视区域以外的位置。
这个方法会顺序遍历元素。尽可能使用 scrollToIndex
。 如果不设置getItemLayout
属性的话,可能会比较卡。
滚动列表到指定的偏移(以像素为单位),等同于 ScrollView
的 scrollTo
方法。
主动通知列表发生了一个事件,以使列表重新计算可视区域。比如说当waitForInteractions
为 true 并且用户没有滚动列表时,就可以调用这个方法。不过一般来说,当用户点击了一个列表项,或发生了一个导航动作时,我们就可以调用这个方法。
短暂地显示滚动指示器。
'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;