Add onStartReached and onStartReachedThreshold to VirtualizedList (#35321)

Summary:
Add  `onStartReached` and `onStartReachedThreshold` to `VirtualizedList`. This allows implementing bidirectional paging.

## Changelog

[General] [Added] - Add onStartReached and onStartReachedThreshold to VirtualizedList

Pull Request resolved: https://github.com/facebook/react-native/pull/35321

Test Plan: Tested in the new RN tester example that the callback is triggered when close to the start of the list.

Reviewed By: yungsters

Differential Revision: D41653054

Pulled By: NickGerleman

fbshipit-source-id: 368b357fa0d83a43afb52a3f8df84a2fbbedc132
This commit is contained in:
Janic Duplessis 2023-01-03 14:58:40 -08:00 коммит произвёл Facebook GitHub Bot
Родитель 79e603c5ab
Коммит 7683713264
8 изменённых файлов: 362 добавлений и 45 удалений

13
Libraries/Lists/FlatList.d.ts поставляемый
Просмотреть файл

@ -104,19 +104,6 @@ export interface FlatListProps<ItemT> extends VirtualizedListProps<ItemT> {
*/ */
numColumns?: number | undefined; numColumns?: number | undefined;
/**
* Called once when the scroll position gets within onEndReachedThreshold of the rendered content.
*/
onEndReached?: ((info: {distanceFromEnd: number}) => void) | null | undefined;
/**
* How far from the end (in units of visible length of the list) the bottom edge of the
* list must be from the end of the content to trigger the `onEndReached` callback.
* Thus a value of 0.5 will trigger `onEndReached` when the end of the content is
* within half the visible length of the list.
*/
onEndReachedThreshold?: number | null | undefined;
/** /**
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality.
* Make sure to also set the refreshing prop correctly. * Make sure to also set the refreshing prop correctly.

27
Libraries/Lists/VirtualizedList.d.ts поставляемый
Просмотреть файл

@ -262,8 +262,18 @@ export interface VirtualizedListWithoutRenderItemProps<ItemT>
*/ */
maxToRenderPerBatch?: number | undefined; maxToRenderPerBatch?: number | undefined;
/**
* Called once when the scroll position gets within within `onEndReachedThreshold`
* from the logical end of the list.
*/
onEndReached?: ((info: {distanceFromEnd: number}) => void) | null | undefined; onEndReached?: ((info: {distanceFromEnd: number}) => void) | null | undefined;
/**
* How far from the end (in units of visible length of the list) the trailing edge of the
* list must be from the end of the content to trigger the `onEndReached` callback.
* Thus, a value of 0.5 will trigger `onEndReached` when the end of the content is
* within half the visible length of the list.
*/
onEndReachedThreshold?: number | null | undefined; onEndReachedThreshold?: number | null | undefined;
onLayout?: ((event: LayoutChangeEvent) => void) | undefined; onLayout?: ((event: LayoutChangeEvent) => void) | undefined;
@ -287,6 +297,23 @@ export interface VirtualizedListWithoutRenderItemProps<ItemT>
}) => void) }) => void)
| undefined; | undefined;
/**
* Called once when the scroll position gets within within `onStartReachedThreshold`
* from the logical start of the list.
*/
onStartReached?:
| ((info: {distanceFromStart: number}) => void)
| null
| undefined;
/**
* How far from the start (in units of visible length of the list) the leading edge of the
* list must be from the start of the content to trigger the `onStartReached` callback.
* Thus, a value of 0.5 will trigger `onStartReached` when the start of the content is
* within half the visible length of the list.
*/
onStartReachedThreshold?: number | null | undefined;
/** /**
* Called when the viewability of rows changes, as defined by the * Called when the viewability of rows changes, as defined by the
* `viewabilityConfig` prop. * `viewabilityConfig` prop.

Просмотреть файл

@ -50,7 +50,7 @@ import * as React from 'react';
export type {RenderItemProps, RenderItemType, Separators}; export type {RenderItemProps, RenderItemType, Separators};
const ON_END_REACHED_EPSILON = 0.001; const ON_EDGE_REACHED_EPSILON = 0.001;
let _usedIndexForKey = false; let _usedIndexForKey = false;
let _keylessItemComponentName: string = ''; let _keylessItemComponentName: string = '';
@ -90,11 +90,21 @@ function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) {
return maxToRenderPerBatch ?? 10; return maxToRenderPerBatch ?? 10;
} }
// onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold)
function onStartReachedThresholdOrDefault(onStartReachedThreshold: ?number) {
return onStartReachedThreshold ?? 2;
}
// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold) // onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold)
function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) { function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) {
return onEndReachedThreshold ?? 2; return onEndReachedThreshold ?? 2;
} }
// getScrollingThreshold(visibleLength, onEndReachedThreshold)
function getScrollingThreshold(threshold: number, visibleLength: number) {
return (threshold * visibleLength) / 2;
}
// scrollEventThrottleOrDefault(this.props.scrollEventThrottle) // scrollEventThrottleOrDefault(this.props.scrollEventThrottle)
function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) { function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) {
return scrollEventThrottle ?? 50; return scrollEventThrottle ?? 50;
@ -1114,6 +1124,7 @@ export default class VirtualizedList extends StateSafePureComponent<
zoomScale: 1, zoomScale: 1,
}; };
_scrollRef: ?React.ElementRef<any> = null; _scrollRef: ?React.ElementRef<any> = null;
_sentStartForContentLength = 0;
_sentEndForContentLength = 0; _sentEndForContentLength = 0;
_totalCellLength = 0; _totalCellLength = 0;
_totalCellsMeasured = 0; _totalCellsMeasured = 0;
@ -1301,7 +1312,7 @@ export default class VirtualizedList extends StateSafePureComponent<
} }
this.props.onLayout && this.props.onLayout(e); this.props.onLayout && this.props.onLayout(e);
this._scheduleCellsToRenderUpdate(); this._scheduleCellsToRenderUpdate();
this._maybeCallOnEndReached(); this._maybeCallOnEdgeReached();
}; };
_onLayoutEmpty = (e: LayoutEvent) => { _onLayoutEmpty = (e: LayoutEvent) => {
@ -1410,35 +1421,86 @@ export default class VirtualizedList extends StateSafePureComponent<
return !horizontalOrDefault(this.props.horizontal) ? metrics.y : metrics.x; return !horizontalOrDefault(this.props.horizontal) ? metrics.y : metrics.x;
} }
_maybeCallOnEndReached() { _maybeCallOnEdgeReached() {
const {data, getItemCount, onEndReached, onEndReachedThreshold} = const {
this.props; data,
getItemCount,
onStartReached,
onStartReachedThreshold,
onEndReached,
onEndReachedThreshold,
initialScrollIndex,
} = this.props;
const {contentLength, visibleLength, offset} = this._scrollMetrics; const {contentLength, visibleLength, offset} = this._scrollMetrics;
let distanceFromStart = offset;
let distanceFromEnd = contentLength - visibleLength - offset; let distanceFromEnd = contentLength - visibleLength - offset;
// Especially when oERT is zero it's necessary to 'floor' very small distanceFromEnd values to be 0 // Especially when oERT is zero it's necessary to 'floor' very small distance values to be 0
// since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus // since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus
// be at the "end" of the list with a distanceFromEnd approximating 0 but not quite there. // be at the edge of the list with a distance approximating 0 but not quite there.
if (distanceFromEnd < ON_END_REACHED_EPSILON) { if (distanceFromStart < ON_EDGE_REACHED_EPSILON) {
distanceFromStart = 0;
}
if (distanceFromEnd < ON_EDGE_REACHED_EPSILON) {
distanceFromEnd = 0; distanceFromEnd = 0;
} }
// TODO: T121172172 Look into why we're "defaulting" to a threshold of 2 when oERT is not present // TODO: T121172172 Look into why we're "defaulting" to a threshold of 2px
const threshold = // when oERT is not present (different from 2 viewports used elsewhere)
onEndReachedThreshold != null ? onEndReachedThreshold * visibleLength : 2; const DEFAULT_THRESHOLD_PX = 2;
const startThreshold =
onStartReachedThreshold != null
? onStartReachedThreshold * visibleLength
: DEFAULT_THRESHOLD_PX;
const endThreshold =
onEndReachedThreshold != null
? onEndReachedThreshold * visibleLength
: DEFAULT_THRESHOLD_PX;
const isWithinStartThreshold = distanceFromStart <= startThreshold;
const isWithinEndThreshold = distanceFromEnd <= endThreshold;
// First check if the user just scrolled within the end threshold
// and call onEndReached only once for a given content length,
// and only if onStartReached is not being executed
if ( if (
onEndReached && onEndReached &&
this.state.cellsAroundViewport.last === getItemCount(data) - 1 && this.state.cellsAroundViewport.last === getItemCount(data) - 1 &&
distanceFromEnd <= threshold && isWithinEndThreshold &&
this._scrollMetrics.contentLength !== this._sentEndForContentLength this._scrollMetrics.contentLength !== this._sentEndForContentLength
) { ) {
// Only call onEndReached once for a given content length
this._sentEndForContentLength = this._scrollMetrics.contentLength; this._sentEndForContentLength = this._scrollMetrics.contentLength;
onEndReached({distanceFromEnd}); onEndReached({distanceFromEnd});
} else if (distanceFromEnd > threshold) { }
// If the user scrolls away from the end and back again cause
// an onEndReached to be triggered again // Next check if the user just scrolled within the start threshold
this._sentEndForContentLength = 0; // and call onStartReached only once for a given content length,
// and only if onEndReached is not being executed
else if (
onStartReached != null &&
this.state.cellsAroundViewport.first === 0 &&
isWithinStartThreshold &&
this._scrollMetrics.contentLength !== this._sentStartForContentLength
) {
// On initial mount when using initialScrollIndex the offset will be 0 initially
// and will trigger an unexpected onStartReached. To avoid this we can use
// timestamp to differentiate between the initial scroll metrics and when we actually
// received the first scroll event.
if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) {
this._sentStartForContentLength = this._scrollMetrics.contentLength;
onStartReached({distanceFromStart});
}
}
// If the user scrolls away from the start or end and back again,
// cause onStartReached or onEndReached to be triggered again
else {
this._sentStartForContentLength = isWithinStartThreshold
? this._sentStartForContentLength
: 0;
this._sentEndForContentLength = isWithinEndThreshold
? this._sentEndForContentLength
: 0;
} }
} }
@ -1463,7 +1525,7 @@ export default class VirtualizedList extends StateSafePureComponent<
} }
this._scrollMetrics.contentLength = this._selectLength({height, width}); this._scrollMetrics.contentLength = this._selectLength({height, width});
this._scheduleCellsToRenderUpdate(); this._scheduleCellsToRenderUpdate();
this._maybeCallOnEndReached(); this._maybeCallOnEdgeReached();
}; };
/* Translates metrics from a scroll event in a parent VirtualizedList into /* Translates metrics from a scroll event in a parent VirtualizedList into
@ -1551,7 +1613,7 @@ export default class VirtualizedList extends StateSafePureComponent<
if (!this.props) { if (!this.props) {
return; return;
} }
this._maybeCallOnEndReached(); this._maybeCallOnEdgeReached();
if (velocity !== 0) { if (velocity !== 0) {
this._fillRateHelper.activate(); this._fillRateHelper.activate();
} }
@ -1564,28 +1626,34 @@ export default class VirtualizedList extends StateSafePureComponent<
const {offset, visibleLength, velocity} = this._scrollMetrics; const {offset, visibleLength, velocity} = this._scrollMetrics;
const itemCount = this.props.getItemCount(this.props.data); const itemCount = this.props.getItemCount(this.props.data);
let hiPri = false; let hiPri = false;
const onStartReachedThreshold = onStartReachedThresholdOrDefault(
this.props.onStartReachedThreshold,
);
const onEndReachedThreshold = onEndReachedThresholdOrDefault( const onEndReachedThreshold = onEndReachedThresholdOrDefault(
this.props.onEndReachedThreshold, this.props.onEndReachedThreshold,
); );
const scrollingThreshold = (onEndReachedThreshold * visibleLength) / 2;
// Mark as high priority if we're close to the start of the first item // Mark as high priority if we're close to the start of the first item
// But only if there are items before the first rendered item // But only if there are items before the first rendered item
if (first > 0) { if (first > 0) {
const distTop = const distTop =
offset - this.__getFrameMetricsApprox(first, this.props).offset; offset - this.__getFrameMetricsApprox(first, this.props).offset;
hiPri = hiPri =
hiPri || distTop < 0 || (velocity < -2 && distTop < scrollingThreshold); distTop < 0 ||
(velocity < -2 &&
distTop <
getScrollingThreshold(onStartReachedThreshold, visibleLength));
} }
// Mark as high priority if we're close to the end of the last item // Mark as high priority if we're close to the end of the last item
// But only if there are items after the last rendered item // But only if there are items after the last rendered item
if (last >= 0 && last < itemCount - 1) { if (!hiPri && last >= 0 && last < itemCount - 1) {
const distBottom = const distBottom =
this.__getFrameMetricsApprox(last, this.props).offset - this.__getFrameMetricsApprox(last, this.props).offset -
(offset + visibleLength); (offset + visibleLength);
hiPri = hiPri =
hiPri ||
distBottom < 0 || distBottom < 0 ||
(velocity > 2 && distBottom < scrollingThreshold); (velocity > 2 &&
distBottom <
getScrollingThreshold(onEndReachedThreshold, visibleLength));
} }
// Only trigger high-priority updates if we've actually rendered cells, // Only trigger high-priority updates if we've actually rendered cells,
// and with that size estimate, accurately compute how many cells we should render. // and with that size estimate, accurately compute how many cells we should render.

Просмотреть файл

@ -170,16 +170,15 @@ type OptionalProps = {|
*/ */
maxToRenderPerBatch?: ?number, maxToRenderPerBatch?: ?number,
/** /**
* Called once when the scroll position gets within `onEndReachedThreshold` of the rendered * Called once when the scroll position gets within within `onEndReachedThreshold`
* content. * from the logical end of the list.
*/ */
onEndReached?: ?(info: {distanceFromEnd: number, ...}) => void, onEndReached?: ?(info: {distanceFromEnd: number, ...}) => void,
/** /**
* How far from the end (in units of visible length of the list) the bottom edge of the * How far from the end (in units of visible length of the list) the trailing edge of the
* list must be from the end of the content to trigger the `onEndReached` callback. * list must be from the end of the content to trigger the `onEndReached` callback.
* Thus a value of 0.5 will trigger `onEndReached` when the end of the content is * Thus, a value of 0.5 will trigger `onEndReached` when the end of the content is
* within half the visible length of the list. A value of 0 will not trigger until scrolling * within half the visible length of the list.
* to the very end of the list.
*/ */
onEndReachedThreshold?: ?number, onEndReachedThreshold?: ?number,
/** /**
@ -198,6 +197,18 @@ type OptionalProps = {|
averageItemLength: number, averageItemLength: number,
... ...
}) => void, }) => void,
/**
* Called once when the scroll position gets within within `onStartReachedThreshold`
* from the logical start of the list.
*/
onStartReached?: ?(info: {distanceFromStart: number, ...}) => void,
/**
* How far from the start (in units of visible length of the list) the leading edge of the
* list must be from the start of the content to trigger the `onStartReached` callback.
* Thus, a value of 0.5 will trigger `onStartReached` when the start of the content is
* within half the visible length of the list.
*/
onStartReachedThreshold?: ?number,
/** /**
* Called when the viewability of rows changes, as defined by the * Called when the viewability of rows changes, as defined by the
* `viewabilityConfig` prop. * `viewabilityConfig` prop.

Просмотреть файл

@ -356,6 +356,168 @@ describe('VirtualizedList', () => {
expect(scrollRef.measureLayout).toBeInstanceOf(jest.fn().constructor); expect(scrollRef.measureLayout).toBeInstanceOf(jest.fn().constructor);
expect(scrollRef.measureInWindow).toBeInstanceOf(jest.fn().constructor); expect(scrollRef.measureInWindow).toBeInstanceOf(jest.fn().constructor);
}); });
it('calls onStartReached when near the start', () => {
const ITEM_HEIGHT = 40;
const layout = {width: 300, height: 600};
let data = Array(40)
.fill()
.map((_, index) => ({key: `key-${index}`}));
const onStartReached = jest.fn();
const props = {
data,
initialNumToRender: 10,
onStartReachedThreshold: 1,
windowSize: 10,
renderItem: ({item}) => <item value={item.key} />,
getItem: (items, index) => items[index],
getItemCount: items => items.length,
getItemLayout: (items, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}),
onStartReached,
initialScrollIndex: data.length - 1,
};
const component = ReactTestRenderer.create(<VirtualizedList {...props} />);
const instance = component.getInstance();
instance._onLayout({nativeEvent: {layout, zoomScale: 1}});
instance._onContentSizeChange(300, data.length * ITEM_HEIGHT);
// Make sure onStartReached is not called initially when initialScrollIndex is set.
performAllBatches();
expect(onStartReached).not.toHaveBeenCalled();
// Scroll for a small amount and make sure onStartReached is not called.
instance._onScroll({
timeStamp: 1000,
nativeEvent: {
contentOffset: {y: (data.length - 2) * ITEM_HEIGHT, x: 0},
layoutMeasurement: layout,
contentSize: {...layout, height: data.length * ITEM_HEIGHT},
zoomScale: 1,
contentInset: {right: 0, top: 0, left: 0, bottom: 0},
},
});
performAllBatches();
expect(onStartReached).not.toHaveBeenCalled();
// Scroll to start and make sure onStartReached is called.
instance._onScroll({
timeStamp: 1000,
nativeEvent: {
contentOffset: {y: 0, x: 0},
layoutMeasurement: layout,
contentSize: {...layout, height: data.length * ITEM_HEIGHT},
zoomScale: 1,
contentInset: {right: 0, top: 0, left: 0, bottom: 0},
},
});
performAllBatches();
expect(onStartReached).toHaveBeenCalled();
});
it('calls onStartReached initially', () => {
const ITEM_HEIGHT = 40;
const layout = {width: 300, height: 600};
let data = Array(40)
.fill()
.map((_, index) => ({key: `key-${index}`}));
const onStartReached = jest.fn();
const props = {
data,
initialNumToRender: 10,
onStartReachedThreshold: 1,
windowSize: 10,
renderItem: ({item}) => <item value={item.key} />,
getItem: (items, index) => items[index],
getItemCount: items => items.length,
getItemLayout: (items, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}),
onStartReached,
};
const component = ReactTestRenderer.create(<VirtualizedList {...props} />);
const instance = component.getInstance();
instance._onLayout({nativeEvent: {layout, zoomScale: 1}});
instance._onContentSizeChange(300, data.length * ITEM_HEIGHT);
performAllBatches();
expect(onStartReached).toHaveBeenCalled();
});
it('calls onEndReached when near the end', () => {
const ITEM_HEIGHT = 40;
const layout = {width: 300, height: 600};
let data = Array(40)
.fill()
.map((_, index) => ({key: `key-${index}`}));
const onEndReached = jest.fn();
const props = {
data,
initialNumToRender: 10,
onEndReachedThreshold: 1,
windowSize: 10,
renderItem: ({item}) => <item value={item.key} />,
getItem: (items, index) => items[index],
getItemCount: items => items.length,
getItemLayout: (items, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}),
onEndReached,
};
const component = ReactTestRenderer.create(<VirtualizedList {...props} />);
const instance = component.getInstance();
instance._onLayout({nativeEvent: {layout, zoomScale: 1}});
instance._onContentSizeChange(300, data.length * ITEM_HEIGHT);
// Make sure onEndReached is not called initially.
performAllBatches();
expect(onEndReached).not.toHaveBeenCalled();
// Scroll for a small amount and make sure onEndReached is not called.
instance._onScroll({
timeStamp: 1000,
nativeEvent: {
contentOffset: {y: ITEM_HEIGHT, x: 0},
layoutMeasurement: layout,
contentSize: {...layout, height: data.length * ITEM_HEIGHT},
zoomScale: 1,
contentInset: {right: 0, top: 0, left: 0, bottom: 0},
},
});
performAllBatches();
expect(onEndReached).not.toHaveBeenCalled();
// Scroll to end and make sure onEndReached is called.
instance._onScroll({
timeStamp: 1000,
nativeEvent: {
contentOffset: {y: data.length * ITEM_HEIGHT, x: 0},
layoutMeasurement: layout,
contentSize: {...layout, height: data.length * ITEM_HEIGHT},
zoomScale: 1,
contentInset: {right: 0, top: 0, left: 0, bottom: 0},
},
});
performAllBatches();
expect(onEndReached).toHaveBeenCalled();
});
it('does not call onEndReached when onContentSizeChange happens after onLayout', () => { it('does not call onEndReached when onContentSizeChange happens after onLayout', () => {
const ITEM_HEIGHT = 40; const ITEM_HEIGHT = 40;
const layout = {width: 300, height: 600}; const layout = {width: 300, height: 600};

Просмотреть файл

@ -110,11 +110,17 @@ export default (BaseFlatListExample: React.AbstractComponent<
FlatList<string>, FlatList<string>,
>); >);
const ITEM_INNER_HEIGHT = 70;
const ITEM_MARGIN = 8;
export const ITEM_HEIGHT: number = ITEM_INNER_HEIGHT + ITEM_MARGIN * 2;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
item: { item: {
backgroundColor: 'pink', backgroundColor: 'pink',
padding: 20, paddingHorizontal: 20,
marginVertical: 8, height: ITEM_INNER_HEIGHT,
marginVertical: ITEM_MARGIN,
justifyContent: 'center',
}, },
header: { header: {
fontSize: 32, fontSize: 32,

Просмотреть файл

@ -0,0 +1,54 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
'use strict';
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
import BaseFlatListExample, {ITEM_HEIGHT} from './BaseFlatListExample';
import * as React from 'react';
import {FlatList} from 'react-native';
export function FlatList_onStartReached(): React.Node {
const [output, setOutput] = React.useState('');
const exampleProps = {
onStartReached: (info: {distanceFromStart: number, ...}) =>
setOutput('onStartReached'),
onStartReachedThreshold: 0,
initialScrollIndex: 5,
getItemLayout: (data: any, index: number) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}),
};
const ref = React.useRef<?FlatList<string>>(null);
const onTest = () => {
ref.current?.scrollToOffset({offset: 0});
};
return (
<BaseFlatListExample
ref={ref}
exampleProps={exampleProps}
testOutput={output}
onTest={onTest}
/>
);
}
export default ({
title: 'onStartReached',
name: 'onStartReached',
description:
'Scroll to start of list or tap Test button to see `onStartReached` triggered.',
render: function (): React.Element<typeof FlatList_onStartReached> {
return <FlatList_onStartReached />;
},
}: RNTesterModuleExample);

Просмотреть файл

@ -10,6 +10,7 @@
import type {RNTesterModule} from '../../types/RNTesterTypes'; import type {RNTesterModule} from '../../types/RNTesterTypes';
import BasicExample from './FlatList-basic'; import BasicExample from './FlatList-basic';
import OnStartReachedExample from './FlatList-onStartReached';
import OnEndReachedExample from './FlatList-onEndReached'; import OnEndReachedExample from './FlatList-onEndReached';
import ContentInsetExample from './FlatList-contentInset'; import ContentInsetExample from './FlatList-contentInset';
import InvertedExample from './FlatList-inverted'; import InvertedExample from './FlatList-inverted';
@ -28,6 +29,7 @@ export default ({
showIndividualExamples: true, showIndividualExamples: true,
examples: [ examples: [
BasicExample, BasicExample,
OnStartReachedExample,
OnEndReachedExample, OnEndReachedExample,
ContentInsetExample, ContentInsetExample,
InvertedExample, InvertedExample,