mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-06-18 22:05:06 +08:00
configurable Viewability
Reviewed By: yungsters Differential Revision: D4577395 fbshipit-source-id: 9b9099f5bd5f8fe20b5c24eab7e43f298ba665d9
This commit is contained in:
committed by
Facebook Github Bot
parent
fa34035def
commit
f2687bf4b6
@@ -40,7 +40,7 @@ const VirtualizedList = require('VirtualizedList');
|
||||
const invariant = require('invariant');
|
||||
|
||||
import type {StyleObj} from 'StyleSheetTypes';
|
||||
import type {Viewable} from 'ViewabilityHelper';
|
||||
import type {ViewabilityConfig, Viewable} from 'ViewabilityHelper';
|
||||
import type {Props as VirtualizedListProps} from 'VirtualizedList';
|
||||
|
||||
type Item = any;
|
||||
@@ -128,6 +128,10 @@ type OptionalProps<ItemT> = {
|
||||
prevProps: {item: ItemT, index: number},
|
||||
nextProps: {item: ItemT, index: number}
|
||||
) => boolean,
|
||||
/**
|
||||
* See ViewabilityHelper for flow type and comments.
|
||||
*/
|
||||
viewabilityConfig?: ViewabilityConfig,
|
||||
};
|
||||
type Props<ItemT> = RequiredProps<ItemT> & OptionalProps<ItemT> & VirtualizedListProps;
|
||||
|
||||
|
||||
@@ -15,21 +15,69 @@ const invariant = require('invariant');
|
||||
|
||||
export type Viewable = {item: any, key: string, index: ?number, isViewable: boolean, section?: any};
|
||||
|
||||
export type ViewabilityConfig = {
|
||||
/**
|
||||
* Minimum amount of time (in milliseconds) that an item must be physically viewable before the
|
||||
* viewability callback will be fired. A high number means that scrolling through content without
|
||||
* stopping will not mark the content as viewable.
|
||||
*/
|
||||
minViewTime?: number,
|
||||
|
||||
/**
|
||||
* Percent of viewport that must be covered for a partially occluded item to count as
|
||||
* "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means
|
||||
* that a single pixel in the viewport makes the item viewable, and a value of 100 means that
|
||||
* an item must be either entirely visible or cover the entire viewport to count as viewable.
|
||||
*/
|
||||
viewAreaCoveragePercentThreshold?: number,
|
||||
|
||||
/**
|
||||
* Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible,
|
||||
* rather than the fraction of the viewable area it covers.
|
||||
*/
|
||||
itemVisiblePercentThreashold?: number,
|
||||
|
||||
/**
|
||||
* Nothing is considered viewable until the user scrolls (tbd: or taps) the screen after render.
|
||||
*/
|
||||
waitForInteraction?: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* A row is said to be in a "viewable" state when either of the following
|
||||
* is true:
|
||||
* - Occupying >= viewablePercentThreshold of the viewport
|
||||
* - Entirely visible on screen
|
||||
*/
|
||||
const ViewabilityHelper = {
|
||||
class ViewabilityHelper {
|
||||
_config: ViewabilityConfig;
|
||||
_viewableItems: Map<string, Viewable> = new Map();
|
||||
|
||||
constructor(config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0}) {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
remove() {
|
||||
// clear all timeouts...
|
||||
}
|
||||
|
||||
computeViewableItems(
|
||||
viewablePercentThreshold: number,
|
||||
itemCount: number,
|
||||
scrollOffset: number,
|
||||
viewportHeight: number,
|
||||
getFrameMetrics: (index: number) => ?{length: number, offset: number},
|
||||
renderRange?: {first: number, last: number}, // Optional optimization to reduce the scan size
|
||||
): Array<number> {
|
||||
const {itemVisiblePercentThreashold, viewAreaCoveragePercentThreshold} = this._config;
|
||||
const viewAreaMode = viewAreaCoveragePercentThreshold != null;
|
||||
const viewablePercentThreshold = viewAreaMode ?
|
||||
viewAreaCoveragePercentThreshold :
|
||||
itemVisiblePercentThreashold;
|
||||
invariant(
|
||||
viewablePercentThreshold != null &&
|
||||
(itemVisiblePercentThreashold != null) !== (viewAreaCoveragePercentThreshold != null),
|
||||
'Must set exactly one of itemVisiblePercentThreashold or viewAreaCoveragePercentThreshold',
|
||||
);
|
||||
const viewableIndices = [];
|
||||
if (itemCount === 0) {
|
||||
return viewableIndices;
|
||||
@@ -50,10 +98,12 @@ const ViewabilityHelper = {
|
||||
if ((top < viewportHeight) && (bottom > 0)) {
|
||||
firstVisible = idx;
|
||||
if (_isViewable(
|
||||
viewAreaMode,
|
||||
viewablePercentThreshold,
|
||||
top,
|
||||
bottom,
|
||||
viewportHeight
|
||||
viewportHeight,
|
||||
metrics.length,
|
||||
)) {
|
||||
viewableIndices.push(idx);
|
||||
}
|
||||
@@ -62,29 +112,78 @@ const ViewabilityHelper = {
|
||||
}
|
||||
}
|
||||
return viewableIndices;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onUpdate(
|
||||
itemCount: number,
|
||||
scrollOffset: number,
|
||||
viewportHeight: number,
|
||||
getFrameMetrics: (index: number) => ?{length: number, offset: number},
|
||||
createViewable: (index: number, isViewable: boolean) => Viewable,
|
||||
onViewableItemsChanged: ({viewableItems: Array<Viewable>, changed: Array<Viewable>}) => void,
|
||||
renderRange?: {first: number, last: number}, // Optional optimization to reduce the scan size
|
||||
): void {
|
||||
let viewableIndices = [];
|
||||
if (itemCount) {
|
||||
viewableIndices = this.computeViewableItems(
|
||||
itemCount,
|
||||
scrollOffset,
|
||||
viewportHeight,
|
||||
getFrameMetrics,
|
||||
renderRange,
|
||||
);
|
||||
}
|
||||
const prevItems = this._viewableItems;
|
||||
const nextItems = new Map(
|
||||
viewableIndices.map(ii => {
|
||||
const viewable = createViewable(ii, true);
|
||||
return [viewable.key, viewable];
|
||||
})
|
||||
);
|
||||
|
||||
const changed = [];
|
||||
for (const [key, viewable] of nextItems) {
|
||||
if (!prevItems.has(key)) {
|
||||
changed.push(viewable);
|
||||
}
|
||||
}
|
||||
for (const [key, viewable] of prevItems) {
|
||||
if (!nextItems.has(key)) {
|
||||
changed.push({...viewable, isViewable: false});
|
||||
}
|
||||
}
|
||||
if (changed.length > 0) {
|
||||
onViewableItemsChanged({viewableItems: Array.from(nextItems.values()), changed});
|
||||
this._viewableItems = nextItems;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function _isViewable(
|
||||
viewAreaMode: boolean,
|
||||
viewablePercentThreshold: number,
|
||||
top: number,
|
||||
bottom: number,
|
||||
viewportHeight: number
|
||||
viewportHeight: number,
|
||||
itemLength: number,
|
||||
): bool {
|
||||
return _isEntirelyVisible(top, bottom, viewportHeight) ||
|
||||
_getPercentOccupied(top, bottom, viewportHeight) >=
|
||||
viewablePercentThreshold;
|
||||
if (_isEntirelyVisible(top, bottom, viewportHeight)) {
|
||||
return true;
|
||||
} else {
|
||||
const pixels = _getPixelsVisible(top, bottom, viewportHeight);
|
||||
const percent = 100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength);
|
||||
return percent >= viewablePercentThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
function _getPercentOccupied(
|
||||
function _getPixelsVisible(
|
||||
top: number,
|
||||
bottom: number,
|
||||
viewportHeight: number
|
||||
): number {
|
||||
let visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0);
|
||||
visibleHeight = Math.max(0, visibleHeight);
|
||||
return Math.max(0, visibleHeight * 100 / viewportHeight);
|
||||
const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0);
|
||||
return Math.max(0, visibleHeight);
|
||||
}
|
||||
|
||||
function _isEntirelyVisible(
|
||||
|
||||
@@ -44,7 +44,7 @@ const invariant = require('fbjs/lib/invariant');
|
||||
|
||||
const {computeWindowedRenderLimits} = require('VirtualizeUtils');
|
||||
|
||||
import type {Viewable} from 'ViewabilityHelper';
|
||||
import type {ViewabilityConfig, Viewable} from 'ViewabilityHelper';
|
||||
|
||||
type Item = any;
|
||||
type ItemComponentType = ReactClass<{item: Item, index: number}>;
|
||||
@@ -117,13 +117,7 @@ type OptionalProps = {
|
||||
nextProps: {item: Item, index: number}
|
||||
) => boolean,
|
||||
updateCellsBatchingPeriod: number,
|
||||
/**
|
||||
* Percent of viewport that must be covered for a partially occluded item to count as
|
||||
* "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means
|
||||
* that a single pixel in the viewport makes the item viewable, and a value of 100 means that
|
||||
* an item must be either entirely visible or cover the entire viewport to count as viewable.
|
||||
*/
|
||||
viewablePercentThreshold: number,
|
||||
viewabilityConfig?: ViewabilityConfig,
|
||||
windowSize: number, // units of visible length
|
||||
};
|
||||
export type Props = RequiredProps & OptionalProps;
|
||||
@@ -245,6 +239,7 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, *> {
|
||||
this._updateCellsToRender,
|
||||
this.props.updateCellsBatchingPeriod,
|
||||
);
|
||||
this._viewabilityHelper = new ViewabilityHelper(this.props.viewabilityConfig);
|
||||
this.state = {
|
||||
first: 0,
|
||||
last: Math.min(this.props.getItemCount(this.props.data), this.props.initialNumToRender) - 1,
|
||||
@@ -381,8 +376,7 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, *> {
|
||||
_totalCellLength = 0;
|
||||
_totalCellsMeasured = 0;
|
||||
_updateCellsToRenderBatcher: Batchinator;
|
||||
_viewableKeys: {[key: string]: boolean} = {};
|
||||
_viewableItems: Array<Viewable> = [];
|
||||
_viewabilityHelper: ViewabilityHelper;
|
||||
|
||||
_captureScrollRef = (ref) => {
|
||||
this._scrollRef = ref;
|
||||
@@ -571,12 +565,12 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, *> {
|
||||
});
|
||||
};
|
||||
|
||||
_createViewable(index: number, isViewable: boolean): Viewable {
|
||||
_createViewable = (index: number, isViewable: boolean): Viewable => {
|
||||
const {data, getItem, keyExtractor} = this.props;
|
||||
const item = getItem(data, index);
|
||||
invariant(item, 'Missing item for index ' + index);
|
||||
return {index, item, key: keyExtractor(item, index), isViewable};
|
||||
}
|
||||
};
|
||||
|
||||
_getFrameMetricsApprox = (index: number): {length: number, offset: number} => {
|
||||
const frame = this._getFrameMetrics(index);
|
||||
@@ -609,37 +603,19 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, *> {
|
||||
};
|
||||
|
||||
_updateViewableItems(data: any) {
|
||||
const {getItemCount, onViewableItemsChanged, viewablePercentThreshold} = this.props;
|
||||
const {getItemCount, onViewableItemsChanged} = this.props;
|
||||
if (!onViewableItemsChanged) {
|
||||
return;
|
||||
}
|
||||
let viewableIndices = [];
|
||||
if (data) {
|
||||
viewableIndices = ViewabilityHelper.computeViewableItems(
|
||||
viewablePercentThreshold,
|
||||
getItemCount(data),
|
||||
this._scrollMetrics.offset,
|
||||
this._scrollMetrics.visibleLength,
|
||||
this._getFrameMetrics,
|
||||
this.state,
|
||||
);
|
||||
}
|
||||
const viewableKeys = {};
|
||||
const viewableItems = viewableIndices.map((ii) => {
|
||||
const viewable = this._createViewable(ii, true);
|
||||
viewableKeys[viewable.key] = true;
|
||||
return viewable;
|
||||
});
|
||||
const changed = viewableItems.filter(v => !this._viewableKeys[v.key])
|
||||
.concat(
|
||||
this._viewableItems.filter(v => !viewableKeys[v.key])
|
||||
.map(v => ({...v, isViewable: false}))
|
||||
);
|
||||
if (changed.length > 0) {
|
||||
onViewableItemsChanged({viewableItems, changed});
|
||||
this._viewableItems = viewableItems;
|
||||
this._viewableKeys = viewableKeys;
|
||||
}
|
||||
this._viewabilityHelper.onUpdate(
|
||||
getItemCount(data),
|
||||
this._scrollMetrics.offset,
|
||||
this._scrollMetrics.visibleLength,
|
||||
this._getFrameMetrics,
|
||||
this._createViewable,
|
||||
onViewableItemsChanged,
|
||||
this.state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@ class WindowedListView extends React.Component {
|
||||
_viewableRows: Array<number> = [];
|
||||
_cellsInProgress: Set<string> = new Set();
|
||||
_scrollRef: ?ScrollView;
|
||||
_viewabilityHelper: ViewabilityHelper;
|
||||
|
||||
static defaultProps = {
|
||||
initialNumToRender: 10,
|
||||
@@ -207,6 +208,9 @@ class WindowedListView extends React.Component {
|
||||
() => this._computeRowsToRender(this.props),
|
||||
this.props.recomputeRowsBatchingPeriod,
|
||||
);
|
||||
this._viewabilityHelper = new ViewabilityHelper({
|
||||
viewAreaCoveragePercentThreshold: this.props.viewablePercentThreshold,
|
||||
});
|
||||
this.state = {
|
||||
firstRow: 0,
|
||||
lastRow: Math.min(this.props.data.length, this.props.initialNumToRender) - 1,
|
||||
@@ -272,8 +276,7 @@ class WindowedListView extends React.Component {
|
||||
this._computeRowsToRenderBatcher.schedule();
|
||||
}
|
||||
if (this.props.onViewableRowsChanged && Object.keys(this._rowFrames).length) {
|
||||
const viewableRows = ViewabilityHelper.computeViewableItems(
|
||||
this.props.viewablePercentThreshold,
|
||||
const viewableRows = this._viewabilityHelper.computeViewableItems(
|
||||
this.props.data.length,
|
||||
e.nativeEvent.contentOffset.y,
|
||||
e.nativeEvent.layoutMeasurement.height,
|
||||
|
||||
@@ -18,9 +18,13 @@ function getFrameMetrics(index: number) {
|
||||
const frame = rowFrames[data[index].key];
|
||||
return {length: frame.height, offset: frame.y};
|
||||
}
|
||||
function createViewable(index: number, isViewable: boolean) {
|
||||
return {key: data[index].key, isViewable};
|
||||
}
|
||||
|
||||
describe('computeViewableItems', function() {
|
||||
it('returns all 4 entirely visible rows as viewable', function() {
|
||||
const helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 50});
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
b: {y: 50, height: 50},
|
||||
@@ -28,13 +32,14 @@ describe('computeViewableItems', function() {
|
||||
d: {y: 150, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}];
|
||||
expect(ViewabilityHelper.computeViewableItems(50, data.length, 0, 200, getFrameMetrics))
|
||||
expect(helper.computeViewableItems(data.length, 0, 200, getFrameMetrics))
|
||||
.toEqual([0, 1, 2, 3]);
|
||||
});
|
||||
|
||||
it(
|
||||
'returns top 2 rows as viewable (1. entirely visible and 2. majority)',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 50});
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
b: {y: 50, height: 150},
|
||||
@@ -42,13 +47,14 @@ describe('computeViewableItems', function() {
|
||||
d: {y: 250, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}];
|
||||
expect(ViewabilityHelper.computeViewableItems(50, data.length, 0, 200, getFrameMetrics))
|
||||
expect(helper.computeViewableItems(data.length, 0, 200, getFrameMetrics))
|
||||
.toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it(
|
||||
'returns only 2nd row as viewable (majority)',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 50});
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
b: {y: 50, height: 150},
|
||||
@@ -56,16 +62,173 @@ describe('computeViewableItems', function() {
|
||||
d: {y: 250, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}];
|
||||
expect(ViewabilityHelper.computeViewableItems(50, data.length, 25, 200, getFrameMetrics))
|
||||
expect(helper.computeViewableItems(data.length, 25, 200, getFrameMetrics))
|
||||
.toEqual([1]);
|
||||
});
|
||||
|
||||
it(
|
||||
'handles empty input',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 50});
|
||||
rowFrames = {};
|
||||
data = [];
|
||||
expect(ViewabilityHelper.computeViewableItems(50, data.length, 0, 200, getFrameMetrics))
|
||||
expect(helper.computeViewableItems(data.length, 0, 200, getFrameMetrics))
|
||||
.toEqual([]);
|
||||
});
|
||||
|
||||
it(
|
||||
'handles different view area coverage percent thresholds',
|
||||
function() {
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
b: {y: 50, height: 150},
|
||||
c: {y: 200, height: 500},
|
||||
d: {y: 700, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}];
|
||||
|
||||
let helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 0});
|
||||
expect(helper.computeViewableItems(data.length, 0, 50, getFrameMetrics))
|
||||
.toEqual([0]);
|
||||
expect(helper.computeViewableItems(data.length, 1, 50, getFrameMetrics))
|
||||
.toEqual([0, 1]);
|
||||
expect(helper.computeViewableItems(data.length, 199, 50, getFrameMetrics))
|
||||
.toEqual([1, 2]);
|
||||
expect(helper.computeViewableItems(data.length, 250, 50, getFrameMetrics))
|
||||
.toEqual([2]);
|
||||
|
||||
helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 100});
|
||||
expect(helper.computeViewableItems(data.length, 0, 200, getFrameMetrics))
|
||||
.toEqual([0, 1]);
|
||||
expect(helper.computeViewableItems(data.length, 1, 200, getFrameMetrics))
|
||||
.toEqual([1]);
|
||||
expect(helper.computeViewableItems(data.length, 400, 200, getFrameMetrics))
|
||||
.toEqual([2]);
|
||||
expect(helper.computeViewableItems(data.length, 600, 200, getFrameMetrics))
|
||||
.toEqual([3]);
|
||||
});
|
||||
|
||||
it(
|
||||
'handles different item visible percent thresholds',
|
||||
function() {
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
b: {y: 50, height: 150},
|
||||
c: {y: 200, height: 50},
|
||||
d: {y: 250, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}];
|
||||
let helper = new ViewabilityHelper({itemVisiblePercentThreashold: 0});
|
||||
expect(helper.computeViewableItems(data.length, 0, 50, getFrameMetrics))
|
||||
.toEqual([0]);
|
||||
expect(helper.computeViewableItems(data.length, 1, 50, getFrameMetrics))
|
||||
.toEqual([0, 1]);
|
||||
helper = new ViewabilityHelper({itemVisiblePercentThreashold: 100});
|
||||
expect(helper.computeViewableItems(data.length, 0, 250, getFrameMetrics))
|
||||
.toEqual([0, 1, 2]);
|
||||
expect(helper.computeViewableItems(data.length, 1, 250, getFrameMetrics))
|
||||
.toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onUpdate', function() {
|
||||
it(
|
||||
'returns 1 visible row as viewable then scrolls away',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper();
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}];
|
||||
const onViewableItemsChanged = jest.fn();
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
0,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewable,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'a'}],
|
||||
viewableItems: [{isViewable: true, key: 'a'}],
|
||||
});
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
0,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewable,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(1); // nothing changed!
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
100,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewable,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(2);
|
||||
expect(onViewableItemsChanged.mock.calls[1][0]).toEqual({
|
||||
changed: [{isViewable: false, key: 'a'}],
|
||||
viewableItems: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'returns 1st visible row then 1st and 2nd then just 2nd',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper();
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 200},
|
||||
b: {y: 200, height: 200},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}];
|
||||
const onViewableItemsChanged = jest.fn();
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
0,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewable,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'a'}],
|
||||
viewableItems: [{isViewable: true, key: 'a'}],
|
||||
});
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
100,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewable,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(2);
|
||||
// Both visible with 100px overlap each
|
||||
expect(onViewableItemsChanged.mock.calls[1][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'b'}],
|
||||
viewableItems: [{isViewable: true, key: 'a'}, {isViewable: true, key: 'b'}],
|
||||
});
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
200,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewable,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(3);
|
||||
expect(onViewableItemsChanged.mock.calls[2][0]).toEqual({
|
||||
changed: [{isViewable: false, key: 'a'}],
|
||||
viewableItems: [{isViewable: true, key: 'b'}],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user