FlatList.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @flow
  8. * @format
  9. */
  10. 'use strict';
  11. const Platform = require('../Utilities/Platform');
  12. const deepDiffer = require('../Utilities/differ/deepDiffer');
  13. const React = require('react');
  14. const View = require('../Components/View/View');
  15. const VirtualizedList = require('./VirtualizedList');
  16. const StyleSheet = require('../StyleSheet/StyleSheet');
  17. const invariant = require('invariant');
  18. import {type ScrollResponderType} from '../Components/ScrollView/ScrollView';
  19. import type {ScrollViewNativeComponentType} from '../Components/ScrollView/ScrollViewNativeComponentType.js';
  20. import type {ViewStyleProp} from '../StyleSheet/StyleSheet';
  21. import type {
  22. ViewToken,
  23. ViewabilityConfigCallbackPair,
  24. } from './ViewabilityHelper';
  25. import type {RenderItemType, RenderItemProps} from './VirtualizedList';
  26. type RequiredProps<ItemT> = {|
  27. /**
  28. * For simplicity, data is just a plain array. If you want to use something else, like an
  29. * immutable list, use the underlying `VirtualizedList` directly.
  30. */
  31. data: ?$ReadOnlyArray<ItemT>,
  32. |};
  33. type OptionalProps<ItemT> = {|
  34. /**
  35. * Takes an item from `data` and renders it into the list. Example usage:
  36. *
  37. * <FlatList
  38. * ItemSeparatorComponent={Platform.OS !== 'android' && ({highlighted}) => (
  39. * <View style={[style.separator, highlighted && {marginLeft: 0}]} />
  40. * )}
  41. * data={[{title: 'Title Text', key: 'item1'}]}
  42. * renderItem={({item, separators}) => (
  43. * <TouchableHighlight
  44. * onPress={() => this._onPress(item)}
  45. * onShowUnderlay={separators.highlight}
  46. * onHideUnderlay={separators.unhighlight}>
  47. * <View style={{backgroundColor: 'white'}}>
  48. * <Text>{item.title}</Text>
  49. * </View>
  50. * </TouchableHighlight>
  51. * )}
  52. * />
  53. *
  54. * Provides additional metadata like `index` if you need it, as well as a more generic
  55. * `separators.updateProps` function which let's you set whatever props you want to change the
  56. * rendering of either the leading separator or trailing separator in case the more common
  57. * `highlight` and `unhighlight` (which set the `highlighted: boolean` prop) are insufficient for
  58. * your use-case.
  59. */
  60. renderItem?: ?RenderItemType<ItemT>,
  61. /**
  62. * Optional custom style for multi-item rows generated when numColumns > 1.
  63. */
  64. columnWrapperStyle?: ViewStyleProp,
  65. /**
  66. * A marker property for telling the list to re-render (since it implements `PureComponent`). If
  67. * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
  68. * `data` prop, stick it here and treat it immutably.
  69. */
  70. extraData?: any,
  71. /**
  72. * `getItemLayout` is an optional optimizations that let us skip measurement of dynamic content if
  73. * you know the height of items a priori. `getItemLayout` is the most efficient, and is easy to
  74. * use if you have fixed height items, for example:
  75. *
  76. * getItemLayout={(data, index) => (
  77. * {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
  78. * )}
  79. *
  80. * Adding `getItemLayout` can be a great performance boost for lists of several hundred items.
  81. * Remember to include separator length (height or width) in your offset calculation if you
  82. * specify `ItemSeparatorComponent`.
  83. */
  84. getItemLayout?: (
  85. data: ?Array<ItemT>,
  86. index: number,
  87. ) => {
  88. length: number,
  89. offset: number,
  90. index: number,
  91. ...
  92. },
  93. /**
  94. * If true, renders items next to each other horizontally instead of stacked vertically.
  95. */
  96. horizontal?: ?boolean,
  97. /**
  98. * How many items to render in the initial batch. This should be enough to fill the screen but not
  99. * much more. Note these items will never be unmounted as part of the windowed rendering in order
  100. * to improve perceived performance of scroll-to-top actions.
  101. */
  102. initialNumToRender: number,
  103. /**
  104. * Instead of starting at the top with the first item, start at `initialScrollIndex`. This
  105. * disables the "scroll to top" optimization that keeps the first `initialNumToRender` items
  106. * always rendered and immediately renders the items starting at this initial index. Requires
  107. * `getItemLayout` to be implemented.
  108. */
  109. initialScrollIndex?: ?number,
  110. /**
  111. * Reverses the direction of scroll. Uses scale transforms of -1.
  112. */
  113. inverted?: ?boolean,
  114. /**
  115. * Used to extract a unique key for a given item at the specified index. Key is used for caching
  116. * and as the react key to track item re-ordering. The default extractor checks `item.key`, then
  117. * falls back to using the index, like React does.
  118. */
  119. keyExtractor: (item: ItemT, index: number) => string,
  120. /**
  121. * Multiple columns can only be rendered with `horizontal={false}` and will zig-zag like a
  122. * `flexWrap` layout. Items should all be the same height - masonry layouts are not supported.
  123. */
  124. numColumns: number,
  125. /**
  126. * See `ScrollView` for flow type and further documentation.
  127. */
  128. fadingEdgeLength?: ?number,
  129. |};
  130. type FlatListProps<ItemT> = {|
  131. ...RequiredProps<ItemT>,
  132. ...OptionalProps<ItemT>,
  133. |};
  134. type VirtualizedListProps = React.ElementConfig<typeof VirtualizedList>;
  135. export type Props<ItemT> = {
  136. ...$Diff<
  137. VirtualizedListProps,
  138. {
  139. getItem: $PropertyType<VirtualizedListProps, 'getItem'>,
  140. getItemCount: $PropertyType<VirtualizedListProps, 'getItemCount'>,
  141. getItemLayout: $PropertyType<VirtualizedListProps, 'getItemLayout'>,
  142. renderItem: $PropertyType<VirtualizedListProps, 'renderItem'>,
  143. keyExtractor: $PropertyType<VirtualizedListProps, 'keyExtractor'>,
  144. ...
  145. },
  146. >,
  147. ...FlatListProps<ItemT>,
  148. ...
  149. };
  150. const defaultProps = {
  151. ...VirtualizedList.defaultProps,
  152. numColumns: 1,
  153. /**
  154. * Enabling this prop on Android greatly improves scrolling performance with no known issues.
  155. * The alternative is that scrolling on Android is unusably bad. Enabling it on iOS has a few
  156. * known issues.
  157. */
  158. removeClippedSubviews: Platform.OS === 'android',
  159. };
  160. export type DefaultProps = typeof defaultProps;
  161. /**
  162. * A performant interface for rendering simple, flat lists, supporting the most handy features:
  163. *
  164. * - Fully cross-platform.
  165. * - Optional horizontal mode.
  166. * - Configurable viewability callbacks.
  167. * - Header support.
  168. * - Footer support.
  169. * - Separator support.
  170. * - Pull to Refresh.
  171. * - Scroll loading.
  172. * - ScrollToIndex support.
  173. *
  174. * If you need section support, use [`<SectionList>`](docs/sectionlist.html).
  175. *
  176. * Minimal Example:
  177. *
  178. * <FlatList
  179. * data={[{key: 'a'}, {key: 'b'}]}
  180. * renderItem={({item}) => <Text>{item.key}</Text>}
  181. * />
  182. *
  183. * More complex, multi-select example demonstrating `PureComponent` usage for perf optimization and avoiding bugs.
  184. *
  185. * - By binding the `onPressItem` handler, the props will remain `===` and `PureComponent` will
  186. * prevent wasteful re-renders unless the actual `id`, `selected`, or `title` props change, even
  187. * if the components rendered in `MyListItem` did not have such optimizations.
  188. * - By passing `extraData={this.state}` to `FlatList` we make sure `FlatList` itself will re-render
  189. * when the `state.selected` changes. Without setting this prop, `FlatList` would not know it
  190. * needs to re-render any items because it is also a `PureComponent` and the prop comparison will
  191. * not show any changes.
  192. * - `keyExtractor` tells the list to use the `id`s for the react keys instead of the default `key` property.
  193. *
  194. *
  195. * class MyListItem extends React.PureComponent {
  196. * _onPress = () => {
  197. * this.props.onPressItem(this.props.id);
  198. * };
  199. *
  200. * render() {
  201. * const textColor = this.props.selected ? "red" : "black";
  202. * return (
  203. * <TouchableOpacity onPress={this._onPress}>
  204. * <View>
  205. * <Text style={{ color: textColor }}>
  206. * {this.props.title}
  207. * </Text>
  208. * </View>
  209. * </TouchableOpacity>
  210. * );
  211. * }
  212. * }
  213. *
  214. * class MultiSelectList extends React.PureComponent {
  215. * state = {selected: (new Map(): Map<string, boolean>)};
  216. *
  217. * _keyExtractor = (item, index) => item.id;
  218. *
  219. * _onPressItem = (id: string) => {
  220. * // updater functions are preferred for transactional updates
  221. * this.setState((state) => {
  222. * // copy the map rather than modifying state.
  223. * const selected = new Map(state.selected);
  224. * selected.set(id, !selected.get(id)); // toggle
  225. * return {selected};
  226. * });
  227. * };
  228. *
  229. * _renderItem = ({item}) => (
  230. * <MyListItem
  231. * id={item.id}
  232. * onPressItem={this._onPressItem}
  233. * selected={!!this.state.selected.get(item.id)}
  234. * title={item.title}
  235. * />
  236. * );
  237. *
  238. * render() {
  239. * return (
  240. * <FlatList
  241. * data={this.props.data}
  242. * extraData={this.state}
  243. * keyExtractor={this._keyExtractor}
  244. * renderItem={this._renderItem}
  245. * />
  246. * );
  247. * }
  248. * }
  249. *
  250. * This is a convenience wrapper around [`<VirtualizedList>`](docs/virtualizedlist.html),
  251. * and thus inherits its props (as well as those of `ScrollView`) that aren't explicitly listed
  252. * here, along with the following caveats:
  253. *
  254. * - Internal state is not preserved when content scrolls out of the render window. Make sure all
  255. * your data is captured in the item data or external stores like Flux, Redux, or Relay.
  256. * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow-
  257. * equal. Make sure that everything your `renderItem` function depends on is passed as a prop
  258. * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on
  259. * changes. This includes the `data` prop and parent component state.
  260. * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously
  261. * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see
  262. * blank content. This is a tradeoff that can be adjusted to suit the needs of each application,
  263. * and we are working on improving it behind the scenes.
  264. * - By default, the list looks for a `key` prop on each item and uses that for the React key.
  265. * Alternatively, you can provide a custom `keyExtractor` prop.
  266. *
  267. * Also inherits [ScrollView Props](docs/scrollview.html#props), unless it is nested in another FlatList of same orientation.
  268. */
  269. class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
  270. static defaultProps: DefaultProps = defaultProps;
  271. props: Props<ItemT>;
  272. /**
  273. * Scrolls to the end of the content. May be janky without `getItemLayout` prop.
  274. */
  275. scrollToEnd(params?: ?{animated?: ?boolean, ...}) {
  276. if (this._listRef) {
  277. this._listRef.scrollToEnd(params);
  278. }
  279. }
  280. /**
  281. * Scrolls to the item at the specified index such that it is positioned in the viewable area
  282. * such that `viewPosition` 0 places it at the top, 1 at the bottom, and 0.5 centered in the
  283. * middle. `viewOffset` is a fixed number of pixels to offset the final target position.
  284. *
  285. * Note: cannot scroll to locations outside the render window without specifying the
  286. * `getItemLayout` prop.
  287. */
  288. scrollToIndex(params: {
  289. animated?: ?boolean,
  290. index: number,
  291. viewOffset?: number,
  292. viewPosition?: number,
  293. ...
  294. }) {
  295. if (this._listRef) {
  296. this._listRef.scrollToIndex(params);
  297. }
  298. }
  299. /**
  300. * Requires linear scan through data - use `scrollToIndex` instead if possible.
  301. *
  302. * Note: cannot scroll to locations outside the render window without specifying the
  303. * `getItemLayout` prop.
  304. */
  305. scrollToItem(params: {
  306. animated?: ?boolean,
  307. item: ItemT,
  308. viewPosition?: number,
  309. ...
  310. }) {
  311. if (this._listRef) {
  312. this._listRef.scrollToItem(params);
  313. }
  314. }
  315. /**
  316. * Scroll to a specific content pixel offset in the list.
  317. *
  318. * Check out [scrollToOffset](docs/virtualizedlist.html#scrolltooffset) of VirtualizedList
  319. */
  320. scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) {
  321. if (this._listRef) {
  322. this._listRef.scrollToOffset(params);
  323. }
  324. }
  325. /**
  326. * Tells the list an interaction has occurred, which should trigger viewability calculations, e.g.
  327. * if `waitForInteractions` is true and the user has not scrolled. This is typically called by
  328. * taps on items or by navigation actions.
  329. */
  330. recordInteraction() {
  331. if (this._listRef) {
  332. this._listRef.recordInteraction();
  333. }
  334. }
  335. /**
  336. * Displays the scroll indicators momentarily.
  337. *
  338. * @platform ios
  339. */
  340. flashScrollIndicators() {
  341. if (this._listRef) {
  342. this._listRef.flashScrollIndicators();
  343. }
  344. }
  345. /**
  346. * Provides a handle to the underlying scroll responder.
  347. */
  348. getScrollResponder(): ?ScrollResponderType {
  349. if (this._listRef) {
  350. return this._listRef.getScrollResponder();
  351. }
  352. }
  353. /**
  354. * Provides a reference to the underlying host component
  355. */
  356. getNativeScrollRef():
  357. | ?React.ElementRef<typeof View>
  358. | ?React.ElementRef<ScrollViewNativeComponentType> {
  359. if (this._listRef) {
  360. return this._listRef.getScrollRef();
  361. }
  362. }
  363. getScrollableNode(): any {
  364. if (this._listRef) {
  365. return this._listRef.getScrollableNode();
  366. }
  367. }
  368. setNativeProps(props: {[string]: mixed, ...}) {
  369. if (this._listRef) {
  370. this._listRef.setNativeProps(props);
  371. }
  372. }
  373. constructor(props: Props<ItemT>) {
  374. super(props);
  375. this._checkProps(this.props);
  376. if (this.props.viewabilityConfigCallbackPairs) {
  377. this._virtualizedListPairs = this.props.viewabilityConfigCallbackPairs.map(
  378. pair => ({
  379. viewabilityConfig: pair.viewabilityConfig,
  380. onViewableItemsChanged: this._createOnViewableItemsChanged(
  381. pair.onViewableItemsChanged,
  382. ),
  383. }),
  384. );
  385. } else if (this.props.onViewableItemsChanged) {
  386. this._virtualizedListPairs.push({
  387. /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an
  388. * error found when Flow v0.63 was deployed. To see the error delete
  389. * this comment and run Flow. */
  390. viewabilityConfig: this.props.viewabilityConfig,
  391. onViewableItemsChanged: this._createOnViewableItemsChanged(
  392. this.props.onViewableItemsChanged,
  393. ),
  394. });
  395. }
  396. }
  397. componentDidUpdate(prevProps: Props<ItemT>) {
  398. invariant(
  399. prevProps.numColumns === this.props.numColumns,
  400. 'Changing numColumns on the fly is not supported. Change the key prop on FlatList when ' +
  401. 'changing the number of columns to force a fresh render of the component.',
  402. );
  403. invariant(
  404. prevProps.onViewableItemsChanged === this.props.onViewableItemsChanged,
  405. 'Changing onViewableItemsChanged on the fly is not supported',
  406. );
  407. invariant(
  408. !deepDiffer(prevProps.viewabilityConfig, this.props.viewabilityConfig),
  409. 'Changing viewabilityConfig on the fly is not supported',
  410. );
  411. invariant(
  412. prevProps.viewabilityConfigCallbackPairs ===
  413. this.props.viewabilityConfigCallbackPairs,
  414. 'Changing viewabilityConfigCallbackPairs on the fly is not supported',
  415. );
  416. this._checkProps(this.props);
  417. }
  418. _listRef: ?React.ElementRef<typeof VirtualizedList>;
  419. _virtualizedListPairs: Array<ViewabilityConfigCallbackPair> = [];
  420. _captureRef = ref => {
  421. this._listRef = ref;
  422. };
  423. _checkProps(props: Props<ItemT>) {
  424. const {
  425. // $FlowFixMe this prop doesn't exist, is only used for an invariant
  426. getItem,
  427. // $FlowFixMe this prop doesn't exist, is only used for an invariant
  428. getItemCount,
  429. horizontal,
  430. numColumns,
  431. columnWrapperStyle,
  432. onViewableItemsChanged,
  433. viewabilityConfigCallbackPairs,
  434. } = props;
  435. invariant(
  436. !getItem && !getItemCount,
  437. 'FlatList does not support custom data formats.',
  438. );
  439. if (numColumns > 1) {
  440. invariant(!horizontal, 'numColumns does not support horizontal.');
  441. } else {
  442. invariant(
  443. !columnWrapperStyle,
  444. 'columnWrapperStyle not supported for single column lists',
  445. );
  446. }
  447. invariant(
  448. !(onViewableItemsChanged && viewabilityConfigCallbackPairs),
  449. 'FlatList does not support setting both onViewableItemsChanged and ' +
  450. 'viewabilityConfigCallbackPairs.',
  451. );
  452. }
  453. _getItem = (data: Array<ItemT>, index: number) => {
  454. const {numColumns} = this.props;
  455. if (numColumns > 1) {
  456. const ret = [];
  457. for (let kk = 0; kk < numColumns; kk++) {
  458. const item = data[index * numColumns + kk];
  459. if (item != null) {
  460. ret.push(item);
  461. }
  462. }
  463. return ret;
  464. } else {
  465. return data[index];
  466. }
  467. };
  468. _getItemCount = (data: ?Array<ItemT>): number => {
  469. if (data) {
  470. const {numColumns} = this.props;
  471. return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length;
  472. } else {
  473. return 0;
  474. }
  475. };
  476. _keyExtractor = (items: ItemT | Array<ItemT>, index: number) => {
  477. const {keyExtractor, numColumns} = this.props;
  478. if (numColumns > 1) {
  479. invariant(
  480. Array.isArray(items),
  481. 'FlatList: Encountered internal consistency error, expected each item to consist of an ' +
  482. 'array with 1-%s columns; instead, received a single item.',
  483. numColumns,
  484. );
  485. return items
  486. .map((it, kk) => keyExtractor(it, index * numColumns + kk))
  487. .join(':');
  488. } else {
  489. // $FlowFixMe Can't call keyExtractor with an array
  490. return keyExtractor(items, index);
  491. }
  492. };
  493. _pushMultiColumnViewable(arr: Array<ViewToken>, v: ViewToken): void {
  494. const {numColumns, keyExtractor} = this.props;
  495. v.item.forEach((item, ii) => {
  496. invariant(v.index != null, 'Missing index!');
  497. const index = v.index * numColumns + ii;
  498. arr.push({...v, item, key: keyExtractor(item, index), index});
  499. });
  500. }
  501. _createOnViewableItemsChanged(
  502. onViewableItemsChanged: ?(info: {
  503. viewableItems: Array<ViewToken>,
  504. changed: Array<ViewToken>,
  505. ...
  506. }) => void,
  507. ) {
  508. return (info: {
  509. viewableItems: Array<ViewToken>,
  510. changed: Array<ViewToken>,
  511. ...
  512. }) => {
  513. const {numColumns} = this.props;
  514. if (onViewableItemsChanged) {
  515. if (numColumns > 1) {
  516. const changed = [];
  517. const viewableItems = [];
  518. info.viewableItems.forEach(v =>
  519. this._pushMultiColumnViewable(viewableItems, v),
  520. );
  521. info.changed.forEach(v => this._pushMultiColumnViewable(changed, v));
  522. onViewableItemsChanged({viewableItems, changed});
  523. } else {
  524. onViewableItemsChanged(info);
  525. }
  526. }
  527. };
  528. }
  529. _renderer = () => {
  530. const {
  531. ListItemComponent,
  532. renderItem,
  533. numColumns,
  534. columnWrapperStyle,
  535. } = this.props;
  536. let virtualizedListRenderKey = ListItemComponent
  537. ? 'ListItemComponent'
  538. : 'renderItem';
  539. const renderer = (props): React.Node => {
  540. if (ListItemComponent) {
  541. // $FlowFixMe Component isn't valid
  542. return <ListItemComponent {...props} />;
  543. } else if (renderItem) {
  544. return renderItem(props);
  545. } else {
  546. return null;
  547. }
  548. };
  549. return {
  550. /* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment suppresses an
  551. * error found when Flow v0.111 was deployed. To see the error, delete
  552. * this comment and run Flow. */
  553. [virtualizedListRenderKey]: (info: RenderItemProps<ItemT>) => {
  554. if (numColumns > 1) {
  555. const {item, index} = info;
  556. invariant(
  557. Array.isArray(item),
  558. 'Expected array of items with numColumns > 1',
  559. );
  560. return (
  561. <View
  562. style={StyleSheet.compose(
  563. styles.row,
  564. columnWrapperStyle,
  565. )}>
  566. {item.map((it, kk) => {
  567. const element = renderer({
  568. item: it,
  569. index: index * numColumns + kk,
  570. separators: info.separators,
  571. });
  572. return element != null ? (
  573. <React.Fragment key={kk}>{element}</React.Fragment>
  574. ) : null;
  575. })}
  576. </View>
  577. );
  578. } else {
  579. return renderer(info);
  580. }
  581. },
  582. };
  583. };
  584. render(): React.Node {
  585. const {numColumns, columnWrapperStyle, ...restProps} = this.props;
  586. return (
  587. <VirtualizedList
  588. {...restProps}
  589. getItem={this._getItem}
  590. getItemCount={this._getItemCount}
  591. keyExtractor={this._keyExtractor}
  592. ref={this._captureRef}
  593. viewabilityConfigCallbackPairs={this._virtualizedListPairs}
  594. {...this._renderer()}
  595. />
  596. );
  597. }
  598. }
  599. const styles = StyleSheet.create({
  600. row: {flexDirection: 'row'},
  601. });
  602. module.exports = FlatList;