Skip to content

Commit ad733ad

Browse files
Vince0613facebook-github-bot
authored andcommitted
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
1 parent 64be883 commit ad733ad

File tree

5 files changed

+166
-46
lines changed

5 files changed

+166
-46
lines changed

Libraries/Lists/FlatList.js

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ const VirtualizedList = require('VirtualizedList');
2020
const invariant = require('fbjs/lib/invariant');
2121

2222
import type {StyleObj} from 'StyleSheetTypes';
23-
import type {ViewabilityConfig, ViewToken} from 'ViewabilityHelper';
23+
import type {
24+
ViewabilityConfig,
25+
ViewToken,
26+
ViewabilityConfigCallbackPair,
27+
} from 'ViewabilityHelper';
2428
import type {Props as VirtualizedListProps} from 'VirtualizedList';
2529

2630
type RequiredProps<ItemT> = {
@@ -191,6 +195,11 @@ type OptionalProps<ItemT> = {
191195
* See `ViewabilityHelper` for flow type and further documentation.
192196
*/
193197
viewabilityConfig?: ViewabilityConfig,
198+
/**
199+
* List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged
200+
* will be called when its corresponding ViewabilityConfig's conditions are met.
201+
*/
202+
viewabilityConfigCallbackPairs?: Array<ViewabilityConfigCallbackPair>,
194203
};
195204
type Props<ItemT> = RequiredProps<ItemT> &
196205
OptionalProps<ItemT> &
@@ -405,11 +414,47 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
405414
'Changing numColumns on the fly is not supported. Change the key prop on FlatList when ' +
406415
'changing the number of columns to force a fresh render of the component.',
407416
);
417+
invariant(
418+
nextProps.onViewableItemsChanged === this.props.onViewableItemsChanged,
419+
'Changing onViewableItemsChanged on the fly is not supported',
420+
);
421+
invariant(
422+
nextProps.viewabilityConfig === this.props.viewabilityConfig,
423+
'Changing viewabilityConfig on the fly is not supported',
424+
);
425+
invariant(
426+
nextProps.viewabilityConfigCallbackPairs ===
427+
this.props.viewabilityConfigCallbackPairs,
428+
'Changing viewabilityConfigCallbackPairs on the fly is not supported',
429+
);
430+
408431
this._checkProps(nextProps);
409432
}
410433

434+
constructor(props: Props<*>) {
435+
super(props);
436+
if (this.props.viewabilityConfigCallbackPairs) {
437+
this._virtualizedListPairs = this.props.viewabilityConfigCallbackPairs.map(
438+
pair => ({
439+
viewabilityConfig: pair.viewabilityConfig,
440+
onViewableItemsChanged: this._createOnViewableItemsChanged(
441+
pair.onViewableItemsChanged,
442+
),
443+
}),
444+
);
445+
} else if (this.props.onViewableItemsChanged) {
446+
this._virtualizedListPairs.push({
447+
viewabilityConfig: this.props.viewabilityConfig,
448+
onViewableItemsChanged: this._createOnViewableItemsChanged(
449+
this.props.onViewableItemsChanged,
450+
),
451+
});
452+
}
453+
}
454+
411455
_hasWarnedLegacy = false;
412456
_listRef: VirtualizedList;
457+
_virtualizedListPairs: Array<ViewabilityConfigCallbackPair> = [];
413458

414459
_captureRef = ref => {
415460
/* $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> {
426471
legacyImplementation,
427472
numColumns,
428473
columnWrapperStyle,
474+
onViewableItemsChanged,
475+
viewabilityConfigCallbackPairs,
429476
} = props;
430477
invariant(
431478
!getItem && !getItemCount,
@@ -454,6 +501,11 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
454501
this._hasWarnedLegacy = true;
455502
}
456503
}
504+
invariant(
505+
!(onViewableItemsChanged && viewabilityConfigCallbackPairs),
506+
'FlatList does not support setting both onViewableItemsChanged and ' +
507+
'viewabilityConfigCallbackPairs.',
508+
);
457509
}
458510

459511
_getItem = (data: Array<ItemT>, index: number) => {
@@ -500,23 +552,32 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
500552
});
501553
}
502554

503-
_onViewableItemsChanged = info => {
504-
const {numColumns, onViewableItemsChanged} = this.props;
505-
if (!onViewableItemsChanged) {
506-
return;
507-
}
508-
if (numColumns > 1) {
509-
const changed = [];
510-
const viewableItems = [];
511-
info.viewableItems.forEach(v =>
512-
this._pushMultiColumnViewable(viewableItems, v),
513-
);
514-
info.changed.forEach(v => this._pushMultiColumnViewable(changed, v));
515-
onViewableItemsChanged({viewableItems, changed});
516-
} else {
517-
onViewableItemsChanged(info);
518-
}
519-
};
555+
_createOnViewableItemsChanged(
556+
onViewableItemsChanged: ?(info: {
557+
viewableItems: Array<ViewToken>,
558+
changed: Array<ViewToken>,
559+
}) => void,
560+
) {
561+
return (info: {
562+
viewableItems: Array<ViewToken>,
563+
changed: Array<ViewToken>,
564+
}) => {
565+
const {numColumns} = this.props;
566+
if (onViewableItemsChanged) {
567+
if (numColumns > 1) {
568+
const changed = [];
569+
const viewableItems = [];
570+
info.viewableItems.forEach(v =>
571+
this._pushMultiColumnViewable(viewableItems, v),
572+
);
573+
info.changed.forEach(v => this._pushMultiColumnViewable(changed, v));
574+
onViewableItemsChanged({viewableItems, changed});
575+
} else {
576+
onViewableItemsChanged(info);
577+
}
578+
}
579+
};
580+
}
520581

521582
_renderItem = (info: Object) => {
522583
const {renderItem, numColumns, columnWrapperStyle} = this.props;
@@ -561,9 +622,7 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
561622
getItemCount={this._getItemCount}
562623
keyExtractor={this._keyExtractor}
563624
ref={this._captureRef}
564-
onViewableItemsChanged={
565-
this.props.onViewableItemsChanged && this._onViewableItemsChanged
566-
}
625+
viewabilityConfigCallbackPairs={this._virtualizedListPairs}
567626
/>
568627
);
569628
}

Libraries/Lists/ViewabilityHelper.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ export type ViewToken = {
2222
section?: any,
2323
};
2424

25+
export type ViewabilityConfigCallbackPair = {
26+
viewabilityConfig: ViewabilityConfig,
27+
onViewableItemsChanged: (info: {
28+
viewableItems: Array<ViewToken>,
29+
changed: Array<ViewToken>,
30+
}) => void,
31+
};
32+
2533
export type ViewabilityConfig = {|
2634
/**
2735
* Minimum amount of time (in milliseconds) that an item must be physically viewable before the
@@ -256,6 +264,7 @@ class ViewabilityHelper {
256264
onViewableItemsChanged({
257265
viewableItems: Array.from(nextItems.values()),
258266
changed,
267+
viewabilityConfig: this._config,
259268
});
260269
}
261270
}

Libraries/Lists/VirtualizedList.js

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,24 @@ const warning = require('fbjs/lib/warning');
3131
const {computeWindowedRenderLimits} = require('VirtualizeUtils');
3232

3333
import type {StyleObj} from 'StyleSheetTypes';
34-
import type {ViewabilityConfig, ViewToken} from 'ViewabilityHelper';
34+
import type {
35+
ViewabilityConfig,
36+
ViewToken,
37+
ViewabilityConfigCallbackPair,
38+
} from 'ViewabilityHelper';
3539

3640
type Item = any;
3741

3842
type renderItemType = (info: any) => ?React.Element<any>;
3943

44+
type ViewabilityHelperCallbackTuple = {
45+
viewabilityHelper: ViewabilityHelper,
46+
onViewableItemsChanged: (info: {
47+
viewableItems: Array<ViewToken>,
48+
changed: Array<ViewToken>,
49+
}) => void,
50+
};
51+
4052
type RequiredProps = {
4153
renderItem: renderItemType,
4254
/**
@@ -161,6 +173,11 @@ type OptionalProps = {
161173
*/
162174
updateCellsBatchingPeriod: number,
163175
viewabilityConfig?: ViewabilityConfig,
176+
/**
177+
* List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged
178+
* will be called when its corresponding ViewabilityConfig's conditions are met.
179+
*/
180+
viewabilityConfigCallbackPairs?: Array<ViewabilityConfigCallbackPair>,
164181
/**
165182
* Determines the maximum number of items rendered outside of the visible area, in units of
166183
* 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> {
311328
}
312329

313330
recordInteraction() {
314-
this._viewabilityHelper.recordInteraction();
331+
this._viewabilityTuples.forEach(t => {
332+
t.viewabilityHelper.recordInteraction();
333+
});
315334
this._updateViewableItems(this.props.data);
316335
}
317336

@@ -415,9 +434,21 @@ class VirtualizedList extends React.PureComponent<Props, State> {
415434
this._updateCellsToRender,
416435
this.props.updateCellsBatchingPeriod,
417436
);
418-
this._viewabilityHelper = new ViewabilityHelper(
419-
this.props.viewabilityConfig,
420-
);
437+
438+
if (this.props.viewabilityConfigCallbackPairs) {
439+
this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map(
440+
pair => ({
441+
viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig),
442+
onViewableItemsChanged: pair.onViewableItemsChanged,
443+
}),
444+
);
445+
} else if (this.props.onViewableItemsChanged) {
446+
this._viewabilityTuples.push({
447+
viewabilityHelper: new ViewabilityHelper(this.props.viewabilityConfig),
448+
onViewableItemsChanged: this.props.onViewableItemsChanged,
449+
});
450+
}
451+
421452
this.state = {
422453
first: this.props.initialScrollIndex || 0,
423454
last:
@@ -444,7 +475,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
444475
componentWillUnmount() {
445476
this._updateViewableItems(null);
446477
this._updateCellsToRenderBatcher.dispose();
447-
this._viewabilityHelper.dispose();
478+
this._viewabilityTuples.forEach(tuple => {
479+
tuple.viewabilityHelper.dispose();
480+
});
448481
this._fillRateHelper.deactivateAndFlush();
449482
clearTimeout(this._initialScrollIndexTimeout);
450483
}
@@ -770,7 +803,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
770803
_totalCellLength = 0;
771804
_totalCellsMeasured = 0;
772805
_updateCellsToRenderBatcher: Batchinator;
773-
_viewabilityHelper: ViewabilityHelper;
806+
_viewabilityTuples: Array<ViewabilityHelperCallbackTuple> = [];
774807

775808
_captureScrollRef = ref => {
776809
this._scrollRef = ref;
@@ -1062,7 +1095,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
10621095
}
10631096

10641097
_onScrollBeginDrag = (e): void => {
1065-
this._viewabilityHelper.recordInteraction();
1098+
this._viewabilityTuples.forEach(tuple => {
1099+
tuple.viewabilityHelper.recordInteraction();
1100+
});
10661101
this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
10671102
};
10681103

@@ -1195,19 +1230,19 @@ class VirtualizedList extends React.PureComponent<Props, State> {
11951230
};
11961231

11971232
_updateViewableItems(data: any) {
1198-
const {getItemCount, onViewableItemsChanged} = this.props;
1199-
if (!onViewableItemsChanged) {
1200-
return;
1201-
}
1202-
this._viewabilityHelper.onUpdate(
1203-
getItemCount(data),
1204-
this._scrollMetrics.offset,
1205-
this._scrollMetrics.visibleLength,
1206-
this._getFrameMetrics,
1207-
this._createViewToken,
1208-
onViewableItemsChanged,
1209-
this.state,
1210-
);
1233+
const {getItemCount} = this.props;
1234+
1235+
this._viewabilityTuples.forEach(tuple => {
1236+
tuple.viewabilityHelper.onUpdate(
1237+
getItemCount(data),
1238+
this._scrollMetrics.offset,
1239+
this._scrollMetrics.visibleLength,
1240+
this._getFrameMetrics,
1241+
this._createViewToken,
1242+
tuple.onViewableItemsChanged,
1243+
this.state,
1244+
);
1245+
});
12111246
}
12121247
}
12131248

Libraries/Lists/__tests__/ViewabilityHelper-test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ describe('onUpdate', function() {
185185
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
186186
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
187187
changed: [{isViewable: true, key: 'a'}],
188+
viewabilityConfig: {viewAreaCoveragePercentThreshold: 0},
188189
viewableItems: [{isViewable: true, key: 'a'}],
189190
});
190191
helper.onUpdate(
@@ -207,6 +208,7 @@ describe('onUpdate', function() {
207208
expect(onViewableItemsChanged.mock.calls.length).toBe(2);
208209
expect(onViewableItemsChanged.mock.calls[1][0]).toEqual({
209210
changed: [{isViewable: false, key: 'a'}],
211+
viewabilityConfig: {viewAreaCoveragePercentThreshold: 0},
210212
viewableItems: [],
211213
});
212214
});
@@ -230,6 +232,7 @@ describe('onUpdate', function() {
230232
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
231233
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
232234
changed: [{isViewable: true, key: 'a'}],
235+
viewabilityConfig: {viewAreaCoveragePercentThreshold: 0},
233236
viewableItems: [{isViewable: true, key: 'a'}],
234237
});
235238
helper.onUpdate(
@@ -244,6 +247,7 @@ describe('onUpdate', function() {
244247
// Both visible with 100px overlap each
245248
expect(onViewableItemsChanged.mock.calls[1][0]).toEqual({
246249
changed: [{isViewable: true, key: 'b'}],
250+
viewabilityConfig: {viewAreaCoveragePercentThreshold: 0},
247251
viewableItems: [
248252
{isViewable: true, key: 'a'},
249253
{isViewable: true, key: 'b'},
@@ -260,6 +264,7 @@ describe('onUpdate', function() {
260264
expect(onViewableItemsChanged.mock.calls.length).toBe(3);
261265
expect(onViewableItemsChanged.mock.calls[2][0]).toEqual({
262266
changed: [{isViewable: false, key: 'a'}],
267+
viewabilityConfig: {viewAreaCoveragePercentThreshold: 0},
263268
viewableItems: [{isViewable: true, key: 'b'}],
264269
});
265270
});
@@ -290,6 +295,10 @@ describe('onUpdate', function() {
290295
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
291296
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
292297
changed: [{isViewable: true, key: 'a'}],
298+
viewabilityConfig: {
299+
minimumViewTime: 350,
300+
viewAreaCoveragePercentThreshold: 0,
301+
},
293302
viewableItems: [{isViewable: true, key: 'a'}],
294303
});
295304
});
@@ -327,6 +336,10 @@ describe('onUpdate', function() {
327336
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
328337
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
329338
changed: [{isViewable: true, key: 'b'}],
339+
viewabilityConfig: {
340+
minimumViewTime: 350,
341+
viewAreaCoveragePercentThreshold: 0,
342+
},
330343
viewableItems: [{isViewable: true, key: 'b'}],
331344
});
332345
});
@@ -365,6 +378,10 @@ describe('onUpdate', function() {
365378
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
366379
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
367380
changed: [{isViewable: true, key: 'a'}],
381+
viewabilityConfig: {
382+
waitForInteraction: true,
383+
viewAreaCoveragePercentThreshold: 0,
384+
},
368385
viewableItems: [{isViewable: true, key: 'a'}],
369386
});
370387
});

0 commit comments

Comments
 (0)