Extend FlatList to support multiple viewability configs
Summary: FlatList only supports one viewability configuration and callback. This change extends FlatList and VirtualizedList to support multiple viewability configurations and corresponding callbacks. Reviewed By: sahrens Differential Revision: D5720860 fbshipit-source-id: 9d24946362fa9001d44d4980c85f7d2627e45a33
This commit is contained in:
parent
64be88398d
commit
ad733ad430
|
@ -20,7 +20,11 @@ const VirtualizedList = require('VirtualizedList');
|
|||
const invariant = require('fbjs/lib/invariant');
|
||||
|
||||
import type {StyleObj} from 'StyleSheetTypes';
|
||||
import type {ViewabilityConfig, ViewToken} from 'ViewabilityHelper';
|
||||
import type {
|
||||
ViewabilityConfig,
|
||||
ViewToken,
|
||||
ViewabilityConfigCallbackPair,
|
||||
} from 'ViewabilityHelper';
|
||||
import type {Props as VirtualizedListProps} from 'VirtualizedList';
|
||||
|
||||
type RequiredProps<ItemT> = {
|
||||
|
@ -191,6 +195,11 @@ type OptionalProps<ItemT> = {
|
|||
* See `ViewabilityHelper` for flow type and further documentation.
|
||||
*/
|
||||
viewabilityConfig?: ViewabilityConfig,
|
||||
/**
|
||||
* List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged
|
||||
* will be called when its corresponding ViewabilityConfig's conditions are met.
|
||||
*/
|
||||
viewabilityConfigCallbackPairs?: Array<ViewabilityConfigCallbackPair>,
|
||||
};
|
||||
type Props<ItemT> = RequiredProps<ItemT> &
|
||||
OptionalProps<ItemT> &
|
||||
|
@ -405,11 +414,47 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
|
|||
'Changing numColumns on the fly is not supported. Change the key prop on FlatList when ' +
|
||||
'changing the number of columns to force a fresh render of the component.',
|
||||
);
|
||||
invariant(
|
||||
nextProps.onViewableItemsChanged === this.props.onViewableItemsChanged,
|
||||
'Changing onViewableItemsChanged on the fly is not supported',
|
||||
);
|
||||
invariant(
|
||||
nextProps.viewabilityConfig === this.props.viewabilityConfig,
|
||||
'Changing viewabilityConfig on the fly is not supported',
|
||||
);
|
||||
invariant(
|
||||
nextProps.viewabilityConfigCallbackPairs ===
|
||||
this.props.viewabilityConfigCallbackPairs,
|
||||
'Changing viewabilityConfigCallbackPairs on the fly is not supported',
|
||||
);
|
||||
|
||||
this._checkProps(nextProps);
|
||||
}
|
||||
|
||||
constructor(props: Props<*>) {
|
||||
super(props);
|
||||
if (this.props.viewabilityConfigCallbackPairs) {
|
||||
this._virtualizedListPairs = this.props.viewabilityConfigCallbackPairs.map(
|
||||
pair => ({
|
||||
viewabilityConfig: pair.viewabilityConfig,
|
||||
onViewableItemsChanged: this._createOnViewableItemsChanged(
|
||||
pair.onViewableItemsChanged,
|
||||
),
|
||||
}),
|
||||
);
|
||||
} else if (this.props.onViewableItemsChanged) {
|
||||
this._virtualizedListPairs.push({
|
||||
viewabilityConfig: this.props.viewabilityConfig,
|
||||
onViewableItemsChanged: this._createOnViewableItemsChanged(
|
||||
this.props.onViewableItemsChanged,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_hasWarnedLegacy = false;
|
||||
_listRef: VirtualizedList;
|
||||
_virtualizedListPairs: Array<ViewabilityConfigCallbackPair> = [];
|
||||
|
||||
_captureRef = ref => {
|
||||
/* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment
|
||||
|
@ -426,6 +471,8 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
|
|||
legacyImplementation,
|
||||
numColumns,
|
||||
columnWrapperStyle,
|
||||
onViewableItemsChanged,
|
||||
viewabilityConfigCallbackPairs,
|
||||
} = props;
|
||||
invariant(
|
||||
!getItem && !getItemCount,
|
||||
|
@ -454,6 +501,11 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
|
|||
this._hasWarnedLegacy = true;
|
||||
}
|
||||
}
|
||||
invariant(
|
||||
!(onViewableItemsChanged && viewabilityConfigCallbackPairs),
|
||||
'FlatList does not support setting both onViewableItemsChanged and ' +
|
||||
'viewabilityConfigCallbackPairs.',
|
||||
);
|
||||
}
|
||||
|
||||
_getItem = (data: Array<ItemT>, index: number) => {
|
||||
|
@ -500,23 +552,32 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
|
|||
});
|
||||
}
|
||||
|
||||
_onViewableItemsChanged = info => {
|
||||
const {numColumns, onViewableItemsChanged} = this.props;
|
||||
if (!onViewableItemsChanged) {
|
||||
return;
|
||||
}
|
||||
if (numColumns > 1) {
|
||||
const changed = [];
|
||||
const viewableItems = [];
|
||||
info.viewableItems.forEach(v =>
|
||||
this._pushMultiColumnViewable(viewableItems, v),
|
||||
);
|
||||
info.changed.forEach(v => this._pushMultiColumnViewable(changed, v));
|
||||
onViewableItemsChanged({viewableItems, changed});
|
||||
} else {
|
||||
onViewableItemsChanged(info);
|
||||
}
|
||||
};
|
||||
_createOnViewableItemsChanged(
|
||||
onViewableItemsChanged: ?(info: {
|
||||
viewableItems: Array<ViewToken>,
|
||||
changed: Array<ViewToken>,
|
||||
}) => void,
|
||||
) {
|
||||
return (info: {
|
||||
viewableItems: Array<ViewToken>,
|
||||
changed: Array<ViewToken>,
|
||||
}) => {
|
||||
const {numColumns} = this.props;
|
||||
if (onViewableItemsChanged) {
|
||||
if (numColumns > 1) {
|
||||
const changed = [];
|
||||
const viewableItems = [];
|
||||
info.viewableItems.forEach(v =>
|
||||
this._pushMultiColumnViewable(viewableItems, v),
|
||||
);
|
||||
info.changed.forEach(v => this._pushMultiColumnViewable(changed, v));
|
||||
onViewableItemsChanged({viewableItems, changed});
|
||||
} else {
|
||||
onViewableItemsChanged(info);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_renderItem = (info: Object) => {
|
||||
const {renderItem, numColumns, columnWrapperStyle} = this.props;
|
||||
|
@ -561,9 +622,7 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
|
|||
getItemCount={this._getItemCount}
|
||||
keyExtractor={this._keyExtractor}
|
||||
ref={this._captureRef}
|
||||
onViewableItemsChanged={
|
||||
this.props.onViewableItemsChanged && this._onViewableItemsChanged
|
||||
}
|
||||
viewabilityConfigCallbackPairs={this._virtualizedListPairs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,14 @@ export type ViewToken = {
|
|||
section?: any,
|
||||
};
|
||||
|
||||
export type ViewabilityConfigCallbackPair = {
|
||||
viewabilityConfig: ViewabilityConfig,
|
||||
onViewableItemsChanged: (info: {
|
||||
viewableItems: Array<ViewToken>,
|
||||
changed: Array<ViewToken>,
|
||||
}) => void,
|
||||
};
|
||||
|
||||
export type ViewabilityConfig = {|
|
||||
/**
|
||||
* Minimum amount of time (in milliseconds) that an item must be physically viewable before the
|
||||
|
@ -256,6 +264,7 @@ class ViewabilityHelper {
|
|||
onViewableItemsChanged({
|
||||
viewableItems: Array.from(nextItems.values()),
|
||||
changed,
|
||||
viewabilityConfig: this._config,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,12 +31,24 @@ const warning = require('fbjs/lib/warning');
|
|||
const {computeWindowedRenderLimits} = require('VirtualizeUtils');
|
||||
|
||||
import type {StyleObj} from 'StyleSheetTypes';
|
||||
import type {ViewabilityConfig, ViewToken} from 'ViewabilityHelper';
|
||||
import type {
|
||||
ViewabilityConfig,
|
||||
ViewToken,
|
||||
ViewabilityConfigCallbackPair,
|
||||
} from 'ViewabilityHelper';
|
||||
|
||||
type Item = any;
|
||||
|
||||
type renderItemType = (info: any) => ?React.Element<any>;
|
||||
|
||||
type ViewabilityHelperCallbackTuple = {
|
||||
viewabilityHelper: ViewabilityHelper,
|
||||
onViewableItemsChanged: (info: {
|
||||
viewableItems: Array<ViewToken>,
|
||||
changed: Array<ViewToken>,
|
||||
}) => void,
|
||||
};
|
||||
|
||||
type RequiredProps = {
|
||||
renderItem: renderItemType,
|
||||
/**
|
||||
|
@ -161,6 +173,11 @@ type OptionalProps = {
|
|||
*/
|
||||
updateCellsBatchingPeriod: number,
|
||||
viewabilityConfig?: ViewabilityConfig,
|
||||
/**
|
||||
* List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged
|
||||
* will be called when its corresponding ViewabilityConfig's conditions are met.
|
||||
*/
|
||||
viewabilityConfigCallbackPairs?: Array<ViewabilityConfigCallbackPair>,
|
||||
/**
|
||||
* Determines the maximum number of items rendered outside of the visible area, in units of
|
||||
* visible lengths. So if your list fills the screen, then `windowSize={21}` (the default) will
|
||||
|
@ -311,7 +328,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
recordInteraction() {
|
||||
this._viewabilityHelper.recordInteraction();
|
||||
this._viewabilityTuples.forEach(t => {
|
||||
t.viewabilityHelper.recordInteraction();
|
||||
});
|
||||
this._updateViewableItems(this.props.data);
|
||||
}
|
||||
|
||||
|
@ -415,9 +434,21 @@ class VirtualizedList extends React.PureComponent<Props, State> {
|
|||
this._updateCellsToRender,
|
||||
this.props.updateCellsBatchingPeriod,
|
||||
);
|
||||
this._viewabilityHelper = new ViewabilityHelper(
|
||||
this.props.viewabilityConfig,
|
||||
);
|
||||
|
||||
if (this.props.viewabilityConfigCallbackPairs) {
|
||||
this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map(
|
||||
pair => ({
|
||||
viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig),
|
||||
onViewableItemsChanged: pair.onViewableItemsChanged,
|
||||
}),
|
||||
);
|
||||
} else if (this.props.onViewableItemsChanged) {
|
||||
this._viewabilityTuples.push({
|
||||
viewabilityHelper: new ViewabilityHelper(this.props.viewabilityConfig),
|
||||
onViewableItemsChanged: this.props.onViewableItemsChanged,
|
||||
});
|
||||
}
|
||||
|
||||
this.state = {
|
||||
first: this.props.initialScrollIndex || 0,
|
||||
last:
|
||||
|
@ -444,7 +475,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
|
|||
componentWillUnmount() {
|
||||
this._updateViewableItems(null);
|
||||
this._updateCellsToRenderBatcher.dispose();
|
||||
this._viewabilityHelper.dispose();
|
||||
this._viewabilityTuples.forEach(tuple => {
|
||||
tuple.viewabilityHelper.dispose();
|
||||
});
|
||||
this._fillRateHelper.deactivateAndFlush();
|
||||
clearTimeout(this._initialScrollIndexTimeout);
|
||||
}
|
||||
|
@ -770,7 +803,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
|
|||
_totalCellLength = 0;
|
||||
_totalCellsMeasured = 0;
|
||||
_updateCellsToRenderBatcher: Batchinator;
|
||||
_viewabilityHelper: ViewabilityHelper;
|
||||
_viewabilityTuples: Array<ViewabilityHelperCallbackTuple> = [];
|
||||
|
||||
_captureScrollRef = ref => {
|
||||
this._scrollRef = ref;
|
||||
|
@ -1062,7 +1095,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
_onScrollBeginDrag = (e): void => {
|
||||
this._viewabilityHelper.recordInteraction();
|
||||
this._viewabilityTuples.forEach(tuple => {
|
||||
tuple.viewabilityHelper.recordInteraction();
|
||||
});
|
||||
this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
|
||||
};
|
||||
|
||||
|
@ -1195,19 +1230,19 @@ class VirtualizedList extends React.PureComponent<Props, State> {
|
|||
};
|
||||
|
||||
_updateViewableItems(data: any) {
|
||||
const {getItemCount, onViewableItemsChanged} = this.props;
|
||||
if (!onViewableItemsChanged) {
|
||||
return;
|
||||
}
|
||||
this._viewabilityHelper.onUpdate(
|
||||
getItemCount(data),
|
||||
this._scrollMetrics.offset,
|
||||
this._scrollMetrics.visibleLength,
|
||||
this._getFrameMetrics,
|
||||
this._createViewToken,
|
||||
onViewableItemsChanged,
|
||||
this.state,
|
||||
);
|
||||
const {getItemCount} = this.props;
|
||||
|
||||
this._viewabilityTuples.forEach(tuple => {
|
||||
tuple.viewabilityHelper.onUpdate(
|
||||
getItemCount(data),
|
||||
this._scrollMetrics.offset,
|
||||
this._scrollMetrics.visibleLength,
|
||||
this._getFrameMetrics,
|
||||
this._createViewToken,
|
||||
tuple.onViewableItemsChanged,
|
||||
this.state,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -185,6 +185,7 @@ describe('onUpdate', function() {
|
|||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'a'}],
|
||||
viewabilityConfig: {viewAreaCoveragePercentThreshold: 0},
|
||||
viewableItems: [{isViewable: true, key: 'a'}],
|
||||
});
|
||||
helper.onUpdate(
|
||||
|
@ -207,6 +208,7 @@ describe('onUpdate', function() {
|
|||
expect(onViewableItemsChanged.mock.calls.length).toBe(2);
|
||||
expect(onViewableItemsChanged.mock.calls[1][0]).toEqual({
|
||||
changed: [{isViewable: false, key: 'a'}],
|
||||
viewabilityConfig: {viewAreaCoveragePercentThreshold: 0},
|
||||
viewableItems: [],
|
||||
});
|
||||
});
|
||||
|
@ -230,6 +232,7 @@ describe('onUpdate', function() {
|
|||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'a'}],
|
||||
viewabilityConfig: {viewAreaCoveragePercentThreshold: 0},
|
||||
viewableItems: [{isViewable: true, key: 'a'}],
|
||||
});
|
||||
helper.onUpdate(
|
||||
|
@ -244,6 +247,7 @@ describe('onUpdate', function() {
|
|||
// Both visible with 100px overlap each
|
||||
expect(onViewableItemsChanged.mock.calls[1][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'b'}],
|
||||
viewabilityConfig: {viewAreaCoveragePercentThreshold: 0},
|
||||
viewableItems: [
|
||||
{isViewable: true, key: 'a'},
|
||||
{isViewable: true, key: 'b'},
|
||||
|
@ -260,6 +264,7 @@ describe('onUpdate', function() {
|
|||
expect(onViewableItemsChanged.mock.calls.length).toBe(3);
|
||||
expect(onViewableItemsChanged.mock.calls[2][0]).toEqual({
|
||||
changed: [{isViewable: false, key: 'a'}],
|
||||
viewabilityConfig: {viewAreaCoveragePercentThreshold: 0},
|
||||
viewableItems: [{isViewable: true, key: 'b'}],
|
||||
});
|
||||
});
|
||||
|
@ -290,6 +295,10 @@ describe('onUpdate', function() {
|
|||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'a'}],
|
||||
viewabilityConfig: {
|
||||
minimumViewTime: 350,
|
||||
viewAreaCoveragePercentThreshold: 0,
|
||||
},
|
||||
viewableItems: [{isViewable: true, key: 'a'}],
|
||||
});
|
||||
});
|
||||
|
@ -327,6 +336,10 @@ describe('onUpdate', function() {
|
|||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'b'}],
|
||||
viewabilityConfig: {
|
||||
minimumViewTime: 350,
|
||||
viewAreaCoveragePercentThreshold: 0,
|
||||
},
|
||||
viewableItems: [{isViewable: true, key: 'b'}],
|
||||
});
|
||||
});
|
||||
|
@ -365,6 +378,10 @@ describe('onUpdate', function() {
|
|||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'a'}],
|
||||
viewabilityConfig: {
|
||||
waitForInteraction: true,
|
||||
viewAreaCoveragePercentThreshold: 0,
|
||||
},
|
||||
viewableItems: [{isViewable: true, key: 'a'}],
|
||||
});
|
||||
});
|
||||
|
|
|
@ -42,7 +42,6 @@ exports[`FlatList renders all the bells and whistles 1`] = `
|
|||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
onViewableItemsChanged={undefined}
|
||||
refreshControl={
|
||||
<RefreshControlMock
|
||||
onRefresh={[Function]}
|
||||
|
@ -55,6 +54,7 @@ exports[`FlatList renders all the bells and whistles 1`] = `
|
|||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
viewabilityConfigCallbackPairs={Array []}
|
||||
windowSize={21}
|
||||
>
|
||||
<RCTRefreshControl />
|
||||
|
@ -158,11 +158,11 @@ exports[`FlatList renders empty list 1`] = `
|
|||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
onViewableItemsChanged={undefined}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
viewabilityConfigCallbackPairs={Array []}
|
||||
windowSize={21}
|
||||
>
|
||||
<View />
|
||||
|
@ -187,11 +187,11 @@ exports[`FlatList renders null list 1`] = `
|
|||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
onViewableItemsChanged={undefined}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
viewabilityConfigCallbackPairs={Array []}
|
||||
windowSize={21}
|
||||
>
|
||||
<View />
|
||||
|
@ -228,11 +228,11 @@ exports[`FlatList renders simple list 1`] = `
|
|||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
onViewableItemsChanged={undefined}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
viewabilityConfigCallbackPairs={Array []}
|
||||
windowSize={21}
|
||||
>
|
||||
<View>
|
||||
|
|
Loading…
Reference in New Issue