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:
Родитель
79e603c5ab
Коммит
7683713264
|
@ -104,19 +104,6 @@ export interface FlatListProps<ItemT> extends VirtualizedListProps<ItemT> {
|
|||
*/
|
||||
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.
|
||||
* Make sure to also set the refreshing prop correctly.
|
||||
|
|
|
@ -262,8 +262,18 @@ export interface VirtualizedListWithoutRenderItemProps<ItemT>
|
|||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
onLayout?: ((event: LayoutChangeEvent) => void) | undefined;
|
||||
|
@ -287,6 +297,23 @@ export interface VirtualizedListWithoutRenderItemProps<ItemT>
|
|||
}) => void)
|
||||
| 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
|
||||
* `viewabilityConfig` prop.
|
||||
|
|
|
@ -50,7 +50,7 @@ import * as React from 'react';
|
|||
|
||||
export type {RenderItemProps, RenderItemType, Separators};
|
||||
|
||||
const ON_END_REACHED_EPSILON = 0.001;
|
||||
const ON_EDGE_REACHED_EPSILON = 0.001;
|
||||
|
||||
let _usedIndexForKey = false;
|
||||
let _keylessItemComponentName: string = '';
|
||||
|
@ -90,11 +90,21 @@ function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) {
|
|||
return maxToRenderPerBatch ?? 10;
|
||||
}
|
||||
|
||||
// onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold)
|
||||
function onStartReachedThresholdOrDefault(onStartReachedThreshold: ?number) {
|
||||
return onStartReachedThreshold ?? 2;
|
||||
}
|
||||
|
||||
// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold)
|
||||
function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) {
|
||||
return onEndReachedThreshold ?? 2;
|
||||
}
|
||||
|
||||
// getScrollingThreshold(visibleLength, onEndReachedThreshold)
|
||||
function getScrollingThreshold(threshold: number, visibleLength: number) {
|
||||
return (threshold * visibleLength) / 2;
|
||||
}
|
||||
|
||||
// scrollEventThrottleOrDefault(this.props.scrollEventThrottle)
|
||||
function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) {
|
||||
return scrollEventThrottle ?? 50;
|
||||
|
@ -1114,6 +1124,7 @@ export default class VirtualizedList extends StateSafePureComponent<
|
|||
zoomScale: 1,
|
||||
};
|
||||
_scrollRef: ?React.ElementRef<any> = null;
|
||||
_sentStartForContentLength = 0;
|
||||
_sentEndForContentLength = 0;
|
||||
_totalCellLength = 0;
|
||||
_totalCellsMeasured = 0;
|
||||
|
@ -1301,7 +1312,7 @@ export default class VirtualizedList extends StateSafePureComponent<
|
|||
}
|
||||
this.props.onLayout && this.props.onLayout(e);
|
||||
this._scheduleCellsToRenderUpdate();
|
||||
this._maybeCallOnEndReached();
|
||||
this._maybeCallOnEdgeReached();
|
||||
};
|
||||
|
||||
_onLayoutEmpty = (e: LayoutEvent) => {
|
||||
|
@ -1410,35 +1421,86 @@ export default class VirtualizedList extends StateSafePureComponent<
|
|||
return !horizontalOrDefault(this.props.horizontal) ? metrics.y : metrics.x;
|
||||
}
|
||||
|
||||
_maybeCallOnEndReached() {
|
||||
const {data, getItemCount, onEndReached, onEndReachedThreshold} =
|
||||
this.props;
|
||||
_maybeCallOnEdgeReached() {
|
||||
const {
|
||||
data,
|
||||
getItemCount,
|
||||
onStartReached,
|
||||
onStartReachedThreshold,
|
||||
onEndReached,
|
||||
onEndReachedThreshold,
|
||||
initialScrollIndex,
|
||||
} = this.props;
|
||||
const {contentLength, visibleLength, offset} = this._scrollMetrics;
|
||||
let distanceFromStart = 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
|
||||
// be at the "end" of the list with a distanceFromEnd approximating 0 but not quite there.
|
||||
if (distanceFromEnd < ON_END_REACHED_EPSILON) {
|
||||
// be at the edge of the list with a distance approximating 0 but not quite there.
|
||||
if (distanceFromStart < ON_EDGE_REACHED_EPSILON) {
|
||||
distanceFromStart = 0;
|
||||
}
|
||||
if (distanceFromEnd < ON_EDGE_REACHED_EPSILON) {
|
||||
distanceFromEnd = 0;
|
||||
}
|
||||
|
||||
// TODO: T121172172 Look into why we're "defaulting" to a threshold of 2 when oERT is not present
|
||||
const threshold =
|
||||
onEndReachedThreshold != null ? onEndReachedThreshold * visibleLength : 2;
|
||||
// TODO: T121172172 Look into why we're "defaulting" to a threshold of 2px
|
||||
// when oERT is not present (different from 2 viewports used elsewhere)
|
||||
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 (
|
||||
onEndReached &&
|
||||
this.state.cellsAroundViewport.last === getItemCount(data) - 1 &&
|
||||
distanceFromEnd <= threshold &&
|
||||
isWithinEndThreshold &&
|
||||
this._scrollMetrics.contentLength !== this._sentEndForContentLength
|
||||
) {
|
||||
// Only call onEndReached once for a given content length
|
||||
this._sentEndForContentLength = this._scrollMetrics.contentLength;
|
||||
onEndReached({distanceFromEnd});
|
||||
} else if (distanceFromEnd > threshold) {
|
||||
// If the user scrolls away from the end and back again cause
|
||||
// an onEndReached to be triggered again
|
||||
this._sentEndForContentLength = 0;
|
||||
}
|
||||
|
||||
// Next check if the user just scrolled within the start threshold
|
||||
// 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._scheduleCellsToRenderUpdate();
|
||||
this._maybeCallOnEndReached();
|
||||
this._maybeCallOnEdgeReached();
|
||||
};
|
||||
|
||||
/* Translates metrics from a scroll event in a parent VirtualizedList into
|
||||
|
@ -1551,7 +1613,7 @@ export default class VirtualizedList extends StateSafePureComponent<
|
|||
if (!this.props) {
|
||||
return;
|
||||
}
|
||||
this._maybeCallOnEndReached();
|
||||
this._maybeCallOnEdgeReached();
|
||||
if (velocity !== 0) {
|
||||
this._fillRateHelper.activate();
|
||||
}
|
||||
|
@ -1564,28 +1626,34 @@ export default class VirtualizedList extends StateSafePureComponent<
|
|||
const {offset, visibleLength, velocity} = this._scrollMetrics;
|
||||
const itemCount = this.props.getItemCount(this.props.data);
|
||||
let hiPri = false;
|
||||
const onStartReachedThreshold = onStartReachedThresholdOrDefault(
|
||||
this.props.onStartReachedThreshold,
|
||||
);
|
||||
const onEndReachedThreshold = onEndReachedThresholdOrDefault(
|
||||
this.props.onEndReachedThreshold,
|
||||
);
|
||||
const scrollingThreshold = (onEndReachedThreshold * visibleLength) / 2;
|
||||
// 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
|
||||
if (first > 0) {
|
||||
const distTop =
|
||||
offset - this.__getFrameMetricsApprox(first, this.props).offset;
|
||||
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
|
||||
// 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 =
|
||||
this.__getFrameMetricsApprox(last, this.props).offset -
|
||||
(offset + visibleLength);
|
||||
hiPri =
|
||||
hiPri ||
|
||||
distBottom < 0 ||
|
||||
(velocity > 2 && distBottom < scrollingThreshold);
|
||||
(velocity > 2 &&
|
||||
distBottom <
|
||||
getScrollingThreshold(onEndReachedThreshold, visibleLength));
|
||||
}
|
||||
// Only trigger high-priority updates if we've actually rendered cells,
|
||||
// and with that size estimate, accurately compute how many cells we should render.
|
||||
|
|
|
@ -170,16 +170,15 @@ type OptionalProps = {|
|
|||
*/
|
||||
maxToRenderPerBatch?: ?number,
|
||||
/**
|
||||
* Called once when the scroll position gets within `onEndReachedThreshold` of the rendered
|
||||
* content.
|
||||
* Called once when the scroll position gets within within `onEndReachedThreshold`
|
||||
* from the logical end of the list.
|
||||
*/
|
||||
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.
|
||||
* 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
|
||||
* to the very end of the list.
|
||||
* 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,
|
||||
/**
|
||||
|
@ -198,6 +197,18 @@ type OptionalProps = {|
|
|||
averageItemLength: number,
|
||||
...
|
||||
}) => 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
|
||||
* `viewabilityConfig` prop.
|
||||
|
|
|
@ -356,6 +356,168 @@ describe('VirtualizedList', () => {
|
|||
expect(scrollRef.measureLayout).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', () => {
|
||||
const ITEM_HEIGHT = 40;
|
||||
const layout = {width: 300, height: 600};
|
||||
|
|
|
@ -110,11 +110,17 @@ export default (BaseFlatListExample: React.AbstractComponent<
|
|||
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({
|
||||
item: {
|
||||
backgroundColor: 'pink',
|
||||
padding: 20,
|
||||
marginVertical: 8,
|
||||
paddingHorizontal: 20,
|
||||
height: ITEM_INNER_HEIGHT,
|
||||
marginVertical: ITEM_MARGIN,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
header: {
|
||||
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 BasicExample from './FlatList-basic';
|
||||
import OnStartReachedExample from './FlatList-onStartReached';
|
||||
import OnEndReachedExample from './FlatList-onEndReached';
|
||||
import ContentInsetExample from './FlatList-contentInset';
|
||||
import InvertedExample from './FlatList-inverted';
|
||||
|
@ -28,6 +29,7 @@ export default ({
|
|||
showIndividualExamples: true,
|
||||
examples: [
|
||||
BasicExample,
|
||||
OnStartReachedExample,
|
||||
OnEndReachedExample,
|
||||
ContentInsetExample,
|
||||
InvertedExample,
|
||||
|
|
Загрузка…
Ссылка в новой задаче