Skip to content

Commit

Permalink
Extend FlatList to support multiple viewability configs
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Vince0613 authored and facebook-github-bot committed Sep 6, 2017
1 parent 64be883 commit ad733ad
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 46 deletions.
101 changes: 80 additions & 21 deletions Libraries/Lists/FlatList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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> = {
Expand Down Expand Up @@ -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> &
Expand Down Expand Up @@ -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
Expand All @@ -426,6 +471,8 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
legacyImplementation,
numColumns,
columnWrapperStyle,
onViewableItemsChanged,
viewabilityConfigCallbackPairs,
} = props;
invariant(
!getItem && !getItemCount,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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}
/>
);
}
Expand Down
9 changes: 9 additions & 0 deletions Libraries/Lists/ViewabilityHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -256,6 +264,7 @@ class ViewabilityHelper {
onViewableItemsChanged({
viewableItems: Array.from(nextItems.values()),
changed,
viewabilityConfig: this._config,
});
}
}
Expand Down
77 changes: 56 additions & 21 deletions Libraries/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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:
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
};

Expand Down Expand Up @@ -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,
);
});
}
}

Expand Down
17 changes: 17 additions & 0 deletions Libraries/Lists/__tests__/ViewabilityHelper-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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: [],
});
});
Expand All @@ -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(
Expand All @@ -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'},
Expand All @@ -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'}],
});
});
Expand Down Expand Up @@ -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'}],
});
});
Expand Down Expand Up @@ -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'}],
});
});
Expand Down Expand Up @@ -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'}],
});
});
Expand Down
Loading

0 comments on commit ad733ad

Please sign in to comment.