Many improvements

Summary:
These got smashed together with some weird rebase snafu. They are pretty intertwined anyway so the value of
separate commits is minimal (e.g. separate commits would not revert cleanly anyway).

== [lists] better fill rate logging (previously D4907958)

After looking through some production data, I think this will address all the issues we're seeing. Now:

- Header/Footer getting no longer counted as blank.
- Avoid floating point for Scuba.
- Compare actual time of blankness, not just samples.
- Include both "any" vs. "mostly" blank (similar to 1 and 4 frame drops).
- Include events where there is no blankness so we have a baseline.
- Remove events with too few samples

**Test Plan: **

A bunch of scrolling in FlatListExample

T17384966

== [Lists] Update SectionSeparatorItem docs (previously D4909526)

Forgot to update the language here when we modified the behavior with the introduction of separator
highlighting support.

** Test Plan: **
nope.

== [Lists] Add renderSectionFooter prop to SectionList (previously D4923353)

Handy for things like "see more" links and such.

The logic here is to render the footer last, *after* the bottom section separator. This is to preserve
the highlighting behavior of the section separator by keeping it adjacent to the items.

**Test Plan: **
Added to snapshot test and example:

{F66635525}

{F66635526}

== [SectionList] Add a bunch more info for rendering items and separators (previously D4923663)

This extra info can be helpful for rending more complex patterns.

**Test Plan: **
Made snapshot test more comprehensive and inspected the output.

== [Lists] reduce render churn (previously D4924639)

I don't think the velocity based leadFactor is helping and might actually be hurting because
it causes a lot of churn in the items we render.

Instead, this diff introduces fillPreference which biases the window expansion in the direction of scroll,
but doesn't actually affect the final bounds of the window at all, so items that are already rendered are
more likely to stay rendered.

**Test Plan: **

Played around in debug mode and watched the overlay - seems better. Also tests all pass.

T16621861

== [Lists] Add initialScrollIndex prop

Makes it easy to load a VirtualizedList at a location in the middle of the content without
wasting time rendering initial rows that aren't relevant, for example when opening an infinite calendar
view to "today".

**Test Plan: **
With debug overlay, set `initialScrollIndex={52}` prop in `FlatListExample` and
and see it immediately render a full screen of items with item 52 aligned at the top of the screen. Note
no initial items are mounted per debug overlay. Scroll around a bunch and everything else seems to work
as normal.

No SectionList impl since `getItemLayout` isn't easy to use there.

T17091314

Reviewed By: bvaughn

Differential Revision: D4907958

fbshipit-source-id: 8b9f1f542f9b240f1e317f3fd7e31c9376e8670e
This commit is contained in:
Spencer Ahrens 2017-04-25 14:44:00 -07:00 committed by Facebook Github Bot
parent 1f8d1002ef
commit 28aaa88808
12 changed files with 493 additions and 202 deletions

View File

@ -65,7 +65,14 @@ const renderSectionHeader = ({section}) => (
</View>
);
const CustomSeparatorComponent = ({text, highlighted}) => (
const renderSectionFooter = ({section}) => (
<View style={styles.header}>
<Text style={styles.headerText}>SECTION FOOTER: {section.key}</Text>
<SeparatorComponent />
</View>
);
const CustomSeparatorComponent = ({highlighted, text}) => (
<View style={[styles.customSeparator, highlighted && {backgroundColor: 'rgb(217, 217, 217)'}]}>
<Text style={styles.separatorText}>{text}</Text>
</View>
@ -128,11 +135,11 @@ class SectionListExample extends React.PureComponent {
<AnimatedSectionList
ListHeaderComponent={HeaderComponent}
ListFooterComponent={FooterComponent}
SectionSeparatorComponent={({highlighted}) =>
<CustomSeparatorComponent highlighted={highlighted} text="SECTION SEPARATOR" />
SectionSeparatorComponent={(info) =>
<CustomSeparatorComponent {...info} text="SECTION SEPARATOR" />
}
ItemSeparatorComponent={({highlighted}) =>
<CustomSeparatorComponent highlighted={highlighted} text="ITEM SEPARATOR" />
ItemSeparatorComponent={(info) =>
<CustomSeparatorComponent {...info} text="ITEM SEPARATOR" />
}
debug={this.state.debug}
enableVirtualization={this.state.virtualized}
@ -142,15 +149,23 @@ class SectionListExample extends React.PureComponent {
refreshing={false}
renderItem={this._renderItemComponent}
renderSectionHeader={renderSectionHeader}
renderSectionFooter={renderSectionFooter}
stickySectionHeadersEnabled
sections={[
{renderItem: renderStackedItem, key: 's1', data: [
{title: 'Item In Header Section', text: 'Section s1', key: 'header item'},
]},
{key: 's2', data: [
{noImage: true, title: '1st item', text: 'Section s2', key: 'noimage0'},
{noImage: true, title: '2nd item', text: 'Section s2', key: 'noimage1'},
]},
{
renderItem: renderStackedItem,
key: 's1',
data: [
{title: 'Item In Header Section', text: 'Section s1', key: 'header item'},
],
},
{
key: 's2',
data: [
{noImage: true, title: '1st item', text: 'Section s2', key: 'noimage0'},
{noImage: true, title: '2nd item', text: 'Section s2', key: 'noimage1'},
],
},
...filteredSectionData,
]}
style={styles.list}

View File

@ -10,38 +10,35 @@
* @flow
*/
/* eslint-disable no-console-disallow */
'use strict';
const performanceNow = require('fbjs/lib/performanceNow');
const warning = require('fbjs/lib/warning');
export type FillRateExceededInfo = {
event: {
sample_type: string,
blankness: number,
blank_pixels_top: number,
blank_pixels_bottom: number,
scroll_offset: number,
visible_length: number,
scroll_speed: number,
first_frame: Object,
last_frame: Object,
},
aggregate: {
avg_blankness: number,
min_speed_when_blank: number,
avg_speed_when_blank: number,
avg_blankness_when_any_blank: number,
fraction_any_blank: number,
all_samples_timespan_sec: number,
fill_rate_sample_counts: {[key: string]: number},
},
};
export type FillRateInfo = Info;
class Info {
any_blank_count = 0;
any_blank_ms = 0;
any_blank_speed_sum = 0;
mostly_blank_count = 0;
mostly_blank_ms = 0;
pixels_blank = 0;
pixels_sampled = 0;
pixels_scrolled = 0;
total_time_spent = 0;
sample_count = 0;
}
type FrameMetrics = {inLayout?: boolean, length: number, offset: number};
let _listeners: Array<(FillRateExceededInfo) => void> = [];
let _sampleRate = null;
const DEBUG = false;
let _listeners: Array<(Info) => void> = [];
let _minSampleCount = 10;
let _sampleRate = DEBUG ? 1 : null;
/**
* A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded.
@ -52,20 +49,19 @@ let _sampleRate = null;
* `SceneTracker.getActiveScene` to determine the context of the events.
*/
class FillRateHelper {
_anyBlankStartTime = (null: ?number);
_enabled = false;
_getFrameMetrics: (index: number) => ?FrameMetrics;
_anyBlankCount = 0;
_anyBlankMinSpeed = Number.MAX_SAFE_INTEGER;
_anyBlankSpeedSum = 0;
_sampleCounts = {};
_fractionBlankSum = 0;
_samplesStartTime = 0;
_info = new Info();
_mostlyBlankStartTime = (null: ?number);
_samplesStartTime = (null: ?number);
static addFillRateExceededListener(
callback: (FillRateExceededInfo) => void
static addListener(
callback: (FillRateInfo) => void
): {remove: () => void} {
warning(
_sampleRate !== null,
'Call `FillRateHelper.setSampleRate` before `addFillRateExceededListener`.'
'Call `FillRateHelper.setSampleRate` before `addListener`.'
);
_listeners.push(callback);
return {
@ -79,16 +75,62 @@ class FillRateHelper {
_sampleRate = sampleRate;
}
static enabled(): boolean {
return (_sampleRate || 0) > 0.0;
static setMinSampleCount(minSampleCount: number) {
_minSampleCount = minSampleCount;
}
constructor(getFrameMetrics: (index: number) => ?FrameMetrics) {
this._getFrameMetrics = getFrameMetrics;
this._enabled = (_sampleRate || 0) > Math.random();
this._resetData();
}
computeInfoSampled(
sampleType: string,
activate() {
if (this._enabled && this._samplesStartTime == null) {
DEBUG && console.debug('FillRateHelper: activate');
this._samplesStartTime = performanceNow();
}
}
deactivateAndFlush() {
if (!this._enabled) {
return;
}
const start = this._samplesStartTime; // const for flow
if (start == null) {
DEBUG && console.debug('FillRateHelper: bail on deactivate with no start time');
return;
}
if (this._info.sample_count < _minSampleCount) {
// Don't bother with under-sampled events.
this._resetData();
return;
}
const total_time_spent = performanceNow() - start;
const info: any = {
...this._info,
total_time_spent,
};
if (DEBUG) {
const derived = {
avg_blankness: this._info.pixels_blank / this._info.pixels_sampled,
avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000),
avg_speed_when_any_blank: this._info.any_blank_speed_sum / this._info.any_blank_count,
any_blank_per_min: this._info.any_blank_count / (total_time_spent / 1000 / 60),
any_blank_time_frac: this._info.any_blank_ms / total_time_spent,
mostly_blank_per_min: this._info.mostly_blank_count / (total_time_spent / 1000 / 60),
mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent,
};
for (const key in derived) {
derived[key] = Math.round(1000 * derived[key]) / 1000;
}
console.debug('FillRateHelper deactivateAndFlush: ', {derived, info});
}
_listeners.forEach((listener) => listener(info));
this._resetData();
}
computeBlankness(
props: {
data: Array<any>,
getItemCount: (data: Array<any>) => number,
@ -99,22 +141,35 @@ class FillRateHelper {
last: number,
},
scrollMetrics: {
dOffset: number,
offset: number,
velocity: number,
visibleLength: number,
},
): ?FillRateExceededInfo {
if (!FillRateHelper.enabled() || (_sampleRate || 0) <= Math.random()) {
return null;
): number {
if (!this._enabled || props.getItemCount(props.data) === 0 || this._samplesStartTime == null) {
return 0;
}
const start = performanceNow();
if (props.getItemCount(props.data) === 0) {
return null;
const {dOffset, offset, velocity, visibleLength} = scrollMetrics;
// Denominator metrics that we track for all events - most of the time there is no blankness and
// we want to capture that.
this._info.sample_count++;
this._info.pixels_sampled += Math.round(visibleLength);
this._info.pixels_scrolled += Math.round(Math.abs(dOffset));
const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec
// Whether blank now or not, record the elapsed time blank if we were blank last time.
const now = performanceNow();
if (this._anyBlankStartTime != null) {
this._info.any_blank_ms += now - this._anyBlankStartTime;
}
if (!this._samplesStartTime) {
this._samplesStartTime = start;
this._anyBlankStartTime = null;
if (this._mostlyBlankStartTime != null) {
this._info.mostly_blank_ms += now - this._mostlyBlankStartTime;
}
const {offset, velocity, visibleLength} = scrollMetrics;
this._mostlyBlankStartTime = null;
let blankTop = 0;
let first = state.first;
let firstFrame = this._getFrameMetrics(first);
@ -122,7 +177,9 @@ class FillRateHelper {
firstFrame = this._getFrameMetrics(first);
first++;
}
if (firstFrame) {
// Only count blankTop if we aren't rendering the first item, otherwise we will count the header
// as blank.
if (firstFrame && first > 0) {
blankTop = Math.min(visibleLength, Math.max(0, firstFrame.offset - offset));
}
let blankBottom = 0;
@ -132,47 +189,38 @@ class FillRateHelper {
lastFrame = this._getFrameMetrics(last);
last--;
}
if (lastFrame) {
// Only count blankBottom if we aren't rendering the last item, otherwise we will count the
// footer as blank.
if (lastFrame && last < props.getItemCount(props.data) - 1) {
const bottomEdge = lastFrame.offset + lastFrame.length;
blankBottom = Math.min(visibleLength, Math.max(0, offset + visibleLength - bottomEdge));
}
this._sampleCounts.all = (this._sampleCounts.all || 0) + 1;
this._sampleCounts[sampleType] = (this._sampleCounts[sampleType] || 0) + 1;
const blankness = (blankTop + blankBottom) / visibleLength;
const pixels_blank = Math.round(blankTop + blankBottom);
const blankness = pixels_blank / visibleLength;
if (blankness > 0) {
const scrollSpeed = Math.abs(velocity);
if (scrollSpeed && sampleType === 'onScroll') {
this._anyBlankMinSpeed = Math.min(this._anyBlankMinSpeed, scrollSpeed);
this._anyBlankStartTime = now;
this._info.any_blank_speed_sum += scrollSpeed;
this._info.any_blank_count++;
this._info.pixels_blank += pixels_blank;
if (blankness > 0.5) {
this._mostlyBlankStartTime = now;
this._info.mostly_blank_count++;
}
this._anyBlankSpeedSum += scrollSpeed;
this._anyBlankCount++;
this._fractionBlankSum += blankness;
const event = {
sample_type: sampleType,
blankness: blankness,
blank_pixels_top: blankTop,
blank_pixels_bottom: blankBottom,
scroll_offset: offset,
visible_length: visibleLength,
scroll_speed: scrollSpeed,
first_frame: {...firstFrame},
last_frame: {...lastFrame},
};
const aggregate = {
avg_blankness: this._fractionBlankSum / this._sampleCounts.all,
min_speed_when_blank: this._anyBlankMinSpeed,
avg_speed_when_blank: this._anyBlankSpeedSum / this._anyBlankCount,
avg_blankness_when_any_blank: this._fractionBlankSum / this._anyBlankCount,
fraction_any_blank: this._anyBlankCount / this._sampleCounts.all,
all_samples_timespan_sec: (performanceNow() - this._samplesStartTime) / 1000.0,
fill_rate_sample_counts: {...this._sampleCounts},
compute_time: performanceNow() - start,
};
const info = {event, aggregate};
_listeners.forEach((listener) => listener(info));
return info;
} else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) {
this.deactivateAndFlush();
}
return null;
return blankness;
}
enabled(): boolean {
return this._enabled;
}
_resetData() {
this._anyBlankStartTime = null;
this._info = new Info();
this._mostlyBlankStartTime = null;
this._samplesStartTime = null;
}
}

View File

@ -116,6 +116,13 @@ type OptionalProps<ItemT> = {
* to improve perceived performance of scroll-to-top actions.
*/
initialNumToRender: number,
/**
* Instead of starting at the top with the first item, start at `initialScrollIndex`. This
* disables the "scroll to top" optimization that keeps the first `initialNumToRender` items
* always rendered and immediately renders the items starting at this initial index. Requires
* `getItemLayout` to be implemented.
*/
initialScrollIndex?: ?number,
/**
* Used to extract a unique key for a given item at the specified index. Key is used for caching
* and as the react key to track item re-ordering. The default extractor checks `item.key`, then

View File

@ -30,6 +30,7 @@ type SectionBase<SectionItemT> = {
renderItem?: ?(info: {
item: SectionItemT,
index: number,
section: SectionBase<SectionItemT>,
separators: {
highlight: () => void,
unhighlight: () => void,
@ -66,6 +67,7 @@ type OptionalProps<SectionT: SectionBase<any>> = {
renderItem: (info: {
item: Item,
index: number,
section: SectionT,
separators: {
highlight: () => void,
unhighlight: () => void,
@ -73,10 +75,10 @@ type OptionalProps<SectionT: SectionBase<any>> = {
},
}) => ?React.Element<any>,
/**
* Rendered in between each item, but not at the top or bottom. By default, `highlighted` and
* `leadingItem` props are provided. `renderItem` provides `separators.highlight`/`unhighlight`
* which will update the `highlighted` prop, but you can also add custom props with
* `separators.updateProps`.
* Rendered in between each item, but not at the top or bottom. By default, `highlighted`,
* `section`, and `[leading/trailing][Item/Separator]` props are provided. `renderItem` provides
* `separators.highlight`/`unhighlight` which will update the `highlighted` prop, but you can also
* add custom props with `separators.updateProps`.
*/
ItemSeparatorComponent?: ?ReactClass<any>,
/**
@ -88,8 +90,11 @@ type OptionalProps<SectionT: SectionBase<any>> = {
*/
ListFooterComponent?: ?(ReactClass<any> | React.Element<any>),
/**
* Rendered in between each section. Also receives `highlighted`, `leadingItem`, and any custom
* props from `separators.updateProps`.
* Rendered at the top and bottom of each section (note this is different from
* `ItemSeparatorComponent` which is only rendered between items). These are intended to separate
* sections from the headers above and below and typically have the same highlight response as
* `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`,
* and any custom props from `separators.updateProps`.
*/
SectionSeparatorComponent?: ?ReactClass<any>,
/**
@ -151,6 +156,10 @@ type OptionalProps<SectionT: SectionBase<any>> = {
* iOS. See `stickySectionHeadersEnabled`.
*/
renderSectionHeader?: ?(info: {section: SectionT}) => ?React.Element<any>,
/**
* Rendered at the bottom of each section.
*/
renderSectionFooter?: ?(info: {section: SectionT}) => ?React.Element<any>,
/**
* Makes section headers stick to the top of the screen until the next one pushes it off. Only
* enabled by default on iOS because that is the platform standard there.

View File

@ -90,7 +90,12 @@ function computeWindowedRenderLimits(
const visibleBegin = Math.max(0, offset);
const visibleEnd = visibleBegin + visibleLength;
const overscanLength = (windowSize - 1) * visibleLength;
const leadFactor = Math.max(0, Math.min(1, velocity / 5 + 0.5));
// Considering velocity seems to introduce more churn than it's worth.
const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5));
const fillPreference = velocity > 1 ? 'after' : (velocity < -1 ? 'before' : 'none');
const overscanBegin = Math.max(0, visibleBegin - (1 - leadFactor) * overscanLength);
const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);
@ -129,13 +134,15 @@ function computeWindowedRenderLimits(
// possible.
break;
}
if (firstShouldIncrement) {
if (firstShouldIncrement &&
!(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore)) {
if (firstWillAddMore) {
newCellCount++;
}
first--;
}
if (lastShouldIncrement) {
if (lastShouldIncrement &&
!(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore)) {
if (lastWillAddMore) {
newCellCount++;
}

View File

@ -29,15 +29,7 @@ import type {ViewabilityConfig, ViewToken} from 'ViewabilityHelper';
type Item = any;
type renderItemType = (info: {
item: Item,
index: number,
separators: {
highlight: () => void,
unhighlight: () => void,
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
},
}) => ?React.Element<any>;
type renderItemType = (info: any) => ?React.Element<any>;
type RequiredProps = {
renderItem: renderItemType,
@ -82,6 +74,13 @@ type OptionalProps = {
* to improve perceived performance of scroll-to-top actions.
*/
initialNumToRender: number,
/**
* Instead of starting at the top with the first item, start at `initialScrollIndex`. This
* disables the "scroll to top" optimization that keeps the first `initialNumToRender` items
* always rendered and immediately renders the items starting at this initial index. Requires
* `getItemLayout` to be implemented.
*/
initialScrollIndex?: ?number,
keyExtractor: (item: Item, index: number) => string,
/**
* The maximum number of items to render in each incremental render batch. The more rendered at
@ -312,15 +311,29 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
);
this._viewabilityHelper = new ViewabilityHelper(this.props.viewabilityConfig);
this.state = {
first: 0,
last: Math.min(this.props.getItemCount(this.props.data), this.props.initialNumToRender) - 1,
first: this.props.initialScrollIndex || 0,
last: Math.min(
this.props.getItemCount(this.props.data),
(this.props.initialScrollIndex || 0) + this.props.initialNumToRender,
) - 1,
};
}
componentDidMount() {
if (this.props.initialScrollIndex) {
this._initialScrollIndexTimeout = setTimeout(
() => this.scrollToIndex({animated: false, index: this.props.initialScrollIndex}),
0,
);
}
}
componentWillUnmount() {
this._updateViewableItems(null);
this._updateCellsToRenderBatcher.dispose();
this._viewabilityHelper.dispose();
this._fillRateHelper.deactivateAndFlush();
clearTimeout(this._initialScrollIndexTimeout);
}
componentWillReceiveProps(newProps: Props) {
@ -358,8 +371,9 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
}
cells.push(
<CellRenderer
cellKey={key}
ItemSeparatorComponent={ii < end ? ItemSeparatorComponent : undefined}
cellKey={key}
fillRateHelper={this._fillRateHelper}
index={ii}
item={item}
key={key}
@ -402,7 +416,9 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
if (itemCount > 0) {
_usedIndexForKey = false;
const spacerKey = !horizontal ? 'height' : 'width';
const lastInitialIndex = this.props.initialNumToRender - 1;
const lastInitialIndex = this.props.initialScrollIndex
? -1
: this.props.initialNumToRender - 1;
const {first, last} = this.state;
this._pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, 0, lastInitialIndex);
const firstAfterInitial = Math.max(lastInitialIndex + 1, first);
@ -481,6 +497,8 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
onLayout: this._onLayout,
onScroll: this._onScroll,
onScrollBeginDrag: this._onScrollBeginDrag,
onScrollEndDrag: this._onScrollEndDrag,
onMomentumScrollEnd: this._onMomentumScrollEnd,
ref: this._captureScrollRef,
scrollEventThrottle: this.props.scrollEventThrottle, // TODO: Android support
stickyHeaderIndices,
@ -504,11 +522,12 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
_hasWarned = {};
_highestMeasuredFrameIndex = 0;
_headerLength = 0;
_initialScrollIndexTimeout = 0;
_fillRateHelper: FillRateHelper;
_frames = {};
_footerLength = 0;
_scrollMetrics = {
visibleLength: 0, contentLength: 0, offset: 0, dt: 10, velocity: 0, timestamp: 0,
contentLength: 0, dOffset: 0, dt: 10, offset: 0, timestamp: 0, velocity: 0, visibleLength: 0,
};
_scrollRef = (null: any);
_sentEndForContentLength = 0;
@ -521,6 +540,14 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
this._scrollRef = ref;
};
_computeBlankness() {
this._fillRateHelper.computeBlankness(
this.props,
this.state,
this._scrollMetrics,
);
}
_onCellLayout(e, cellKey, index) {
const layout = e.nativeEvent.layout;
const next = {
@ -544,7 +571,7 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
} else {
this._frames[cellKey].inLayout = true;
}
this._sampleFillRate('onCellLayout');
this._computeBlankness();
}
_onCellUnmount = (cellKey: string) => {
@ -648,15 +675,6 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
this._maybeCallOnEndReached();
};
_sampleFillRate(sampleType: string) {
this._fillRateHelper.computeInfoSampled(
sampleType,
this.props,
this.state,
this._scrollMetrics,
);
}
_onScroll = (e: Object) => {
if (this.props.onScroll) {
this.props.onScroll(e);
@ -678,11 +696,9 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
}
const dOffset = offset - this._scrollMetrics.offset;
const velocity = dOffset / dt;
this._scrollMetrics = {contentLength, dt, offset, timestamp, velocity, visibleLength};
this._scrollMetrics = {contentLength, dt, dOffset, offset, timestamp, velocity, visibleLength};
const {data, getItemCount, windowSize} = this.props;
this._sampleFillRate('onScroll');
this._updateViewableItems(data);
if (!data) {
return;
@ -690,6 +706,10 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
this._maybeCallOnEndReached();
const {first, last} = this.state;
if (velocity !== 0) {
this._fillRateHelper.activate();
}
this._computeBlankness();
const itemCount = getItemCount(data);
if ((first > 0 && velocity < 0) || (last < itemCount - 1 && velocity > 0)) {
const distanceToContentEdge = Math.min(
@ -713,6 +733,21 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
};
_onScrollEndDrag = (e): void => {
const {velocity} = e.nativeEvent;
if (velocity) {
this._scrollMetrics.velocity = this._selectOffset(velocity);
}
this._computeBlankness();
this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
};
_onMomentumScrollEnd = (e): void => {
this._scrollMetrics.velocity = 0;
this._computeBlankness();
this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
};
_updateCellsToRender = () => {
const {data, disableVirtualization, getItemCount, onEndReachedThreshold} = this.props;
this._updateViewableItems(data);
@ -799,6 +834,7 @@ class CellRenderer extends React.Component {
props: {
ItemSeparatorComponent: ?ReactClass<*>,
cellKey: string,
fillRateHelper: FillRateHelper,
index: number,
item: Item,
onLayout: (event: Object) => void, // This is extracted by ScrollViewStickyHeader
@ -844,7 +880,7 @@ class CellRenderer extends React.Component {
}
render() {
const {ItemSeparatorComponent, item, index, parentProps} = this.props;
const {ItemSeparatorComponent, fillRateHelper, item, index, parentProps} = this.props;
const {renderItem, getItemLayout} = parentProps;
invariant(renderItem, 'no renderItem!');
const element = renderItem({
@ -852,7 +888,7 @@ class CellRenderer extends React.Component {
index,
separators: this._separators,
});
const onLayout = (getItemLayout && !parentProps.debug && !FillRateHelper.enabled())
const onLayout = (getItemLayout && !parentProps.debug && !fillRateHelper.enabled())
? undefined
: this.props.onLayout;
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and

View File

@ -33,6 +33,7 @@ type SectionBase = {
renderItem?: ?({
item: SectionItem,
index: number,
section: SectionBase,
separators: {
highlight: () => void,
unhighlight: () => void,
@ -67,6 +68,7 @@ type OptionalProps<SectionT: SectionBase> = {
renderItem: (info: {
item: Item,
index: number,
section: SectionT,
separators: {
highlight: () => void,
unhighlight: () => void,
@ -77,6 +79,10 @@ type OptionalProps<SectionT: SectionBase> = {
* Rendered at the top of each section.
*/
renderSectionHeader?: ?({section: SectionT}) => ?React.Element<*>,
/**
* Rendered at the bottom of each section.
*/
renderSectionFooter?: ?({section: SectionT}) => ?React.Element<*>,
/**
* Rendered at the bottom of every Section, except the very last one, in place of the normal
* ItemSeparatorComponent.
@ -164,6 +170,10 @@ class VirtualizedSectionList<SectionT: SectionBase>
section: SectionT,
key: string, // Key of the section or combined key for section + item
index: ?number, // Relative index within the section
leadingItem?: ?Item,
leadingSection?: ?SectionT,
trailingItem?: ?Item,
trailingSection?: ?SectionT,
} {
let itemIndex = index;
const defaultKeyExtractor = this.props.keyExtractor;
@ -178,13 +188,17 @@ class VirtualizedSectionList<SectionT: SectionBase>
if (itemIndex >= section.data.length) {
itemIndex -= section.data.length;
} else if (itemIndex === -1) {
return {section, key, index: null};
return {section, key, index: null, trailingSection: this.props.sections[ii + 1]};
} else {
const keyExtractor = section.keyExtractor || defaultKeyExtractor;
return {
section,
key: key + ':' + keyExtractor(section.data[itemIndex], itemIndex),
index: itemIndex,
leadingItem: section.data[itemIndex - 1],
leadingSection: this.props.sections[ii - 1],
trailingItem: section.data[itemIndex + 1],
trailingSection: this.props.sections[ii + 1],
};
}
}
@ -239,11 +253,19 @@ class VirtualizedSectionList<SectionT: SectionBase>
cellKey={info.key}
index={infoIndex}
item={item}
leadingItem={info.leadingItem}
leadingSection={info.leadingSection}
onUpdateSeparator={this._onUpdateSeparator}
prevCellKey={(this._subExtractor(index - 1) || {}).key}
ref={(ref) => {this._cellRefs[info.key] = ref;}}
renderItem={renderItem}
renderSectionFooter={infoIndex === info.section.data.length - 1
? this.props.renderSectionFooter
: undefined
}
section={info.section}
trailingItem={info.trailingItem}
trailingSection={info.trailingSection}
/>
);
}
@ -259,7 +281,8 @@ class VirtualizedSectionList<SectionT: SectionBase>
if (!info) {
return null;
}
const ItemSeparatorComponent = info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent;
const ItemSeparatorComponent =
info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent;
const {SectionSeparatorComponent} = this.props;
const isLastItemInList = index === this.state.childProps.getItemCount() - 1;
const isLastItemInSection = info.index === info.section.data.length - 1;
@ -282,6 +305,7 @@ class VirtualizedSectionList<SectionT: SectionBase>
},
0
);
return {
childProps: {
...props,
@ -326,17 +350,30 @@ class ItemWithSeparator extends React.Component {
onUpdateSeparator: (cellKey: string, newProps: Object) => void,
prevCellKey?: ?string,
renderItem: Function,
renderSectionFooter: ?Function,
section: Object,
leadingItem: ?Item,
leadingSection: ?Object,
trailingItem: ?Item,
trailingSection: ?Object,
};
state = {
separatorProps: {
highlighted: false,
leadingItem: this.props.item,
leadingSection: this.props.section,
leadingSection: this.props.leadingSection,
section: this.props.section,
trailingItem: this.props.trailingItem,
trailingSection: this.props.trailingSection,
},
leadingSeparatorProps: {
highlighted: false,
leadingItem: this.props.leadingItem,
leadingSection: this.props.leadingSection,
section: this.props.section,
trailingItem: this.props.item,
trailingSection: this.props.trailingSection,
},
};
@ -364,16 +401,20 @@ class ItemWithSeparator extends React.Component {
}
render() {
const {LeadingSeparatorComponent, SeparatorComponent, renderItem, item, index} = this.props;
const element = renderItem({
const {LeadingSeparatorComponent, SeparatorComponent, item, index, section} = this.props;
const element = this.props.renderItem({
item,
index,
section,
separators: this._separators,
});
const leadingSeparator = LeadingSeparatorComponent &&
<LeadingSeparatorComponent {...this.state.leadingSeparatorProps} />;
const separator = SeparatorComponent && <SeparatorComponent {...this.state.separatorProps} />;
return separator ? <View>{leadingSeparator}{element}{separator}</View> : element;
const footer = this.props.renderSectionFooter && this.props.renderSectionFooter({section});
return (leadingSeparator || separator || footer)
? <View>{leadingSeparator}{element}{separator}{footer}</View>
: element;
}
}

View File

@ -14,93 +14,102 @@ jest.unmock('FillRateHelper');
const FillRateHelper = require('FillRateHelper');
let rowFramesGlobal;
const dataGlobal = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}];
const dataGlobal =
[{key: 'header'}, {key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}, {key: 'footer'}];
function getFrameMetrics(index: number) {
const frame = rowFramesGlobal[dataGlobal[index].key];
return {length: frame.height, offset: frame.y, inLayout: frame.inLayout};
}
function computeResult({helper, props, state, scroll}) {
return helper.computeInfoSampled(
'test',
function computeResult({helper, props, state, scroll}): number {
helper.activate();
return helper.computeBlankness(
{
data: dataGlobal,
fillRateTrackingSampleRate: 1,
getItemCount: (data2) => data2.length,
initialNumToRender: 10,
...(props || {}),
},
{first: 0, last: 1, ...(state || {})},
{first: 1, last: 2, ...(state || {})},
{offset: 0, visibleLength: 100, ...(scroll || {})},
);
}
describe('computeInfoSampled', function() {
describe('computeBlankness', function() {
beforeEach(() => {
FillRateHelper.setSampleRate(1);
FillRateHelper.setMinSampleCount(0);
});
it('computes correct blankness of viewport', function() {
const helper = new FillRateHelper(getFrameMetrics);
rowFramesGlobal = {
header: {y: 0, height: 0, inLayout: true},
a: {y: 0, height: 50, inLayout: true},
b: {y: 50, height: 50, inLayout: true},
};
let result = computeResult({helper});
expect(result).toBeNull();
result = computeResult({helper, state: {last: 0}});
expect(result.event.blankness).toBe(0.5);
result = computeResult({helper, scroll: {offset: 25}});
expect(result.event.blankness).toBe(0.25);
result = computeResult({helper, scroll: {visibleLength: 400}});
expect(result.event.blankness).toBe(0.75);
result = computeResult({helper, scroll: {offset: 100}});
expect(result.event.blankness).toBe(1);
expect(result.aggregate.avg_blankness).toBe(0.5);
let blankness = computeResult({helper});
expect(blankness).toBe(0);
blankness = computeResult({helper, state: {last: 1}});
expect(blankness).toBe(0.5);
blankness = computeResult({helper, scroll: {offset: 25}});
expect(blankness).toBe(0.25);
blankness = computeResult({helper, scroll: {visibleLength: 400}});
expect(blankness).toBe(0.75);
blankness = computeResult({helper, scroll: {offset: 100}});
expect(blankness).toBe(1);
});
it('skips frames that are not in layout', function() {
const helper = new FillRateHelper(getFrameMetrics);
rowFramesGlobal = {
header: {y: 0, height: 0, inLayout: false},
a: {y: 0, height: 10, inLayout: false},
b: {y: 10, height: 30, inLayout: true},
c: {y: 40, height: 40, inLayout: true},
d: {y: 80, height: 20, inLayout: false},
footer: {y: 100, height: 0, inLayout: false},
};
const result = computeResult({helper, state: {last: 3}});
expect(result.event.blankness).toBe(0.3);
const blankness = computeResult({helper, state: {last: 4}});
expect(blankness).toBe(0.3);
});
it('sampling rate can disable', function() {
const helper = new FillRateHelper(getFrameMetrics);
let helper = new FillRateHelper(getFrameMetrics);
rowFramesGlobal = {
header: {y: 0, height: 0, inLayout: true},
a: {y: 0, height: 40, inLayout: true},
b: {y: 40, height: 40, inLayout: true},
};
let result = computeResult({helper});
expect(result.event.blankness).toBe(0.2);
let blankness = computeResult({helper});
expect(blankness).toBe(0.2);
FillRateHelper.setSampleRate(0);
result = computeResult({helper});
expect(result).toBeNull();
helper = new FillRateHelper(getFrameMetrics);
blankness = computeResult({helper});
expect(blankness).toBe(0);
});
it('can handle multiple listeners and unsubscribe', function() {
const listeners = [jest.fn(), jest.fn(), jest.fn()];
const subscriptions = listeners.map(
(listener) => FillRateHelper.addFillRateExceededListener(listener)
(listener) => FillRateHelper.addListener(listener)
);
subscriptions[1].remove();
const helper = new FillRateHelper(getFrameMetrics);
rowFramesGlobal = {
header: {y: 0, height: 0, inLayout: true},
a: {y: 0, height: 40, inLayout: true},
b: {y: 40, height: 40, inLayout: true},
};
const result = computeResult({helper});
expect(result.event.blankness).toBe(0.2);
expect(listeners[0]).toBeCalledWith(result);
const blankness = computeResult({helper});
expect(blankness).toBe(0.2);
helper.deactivateAndFlush();
const info0 = listeners[0].mock.calls[0][0];
expect(info0.pixels_blank / info0.pixels_sampled).toBe(blankness);
expect(listeners[1]).not.toBeCalled();
expect(listeners[2]).toBeCalledWith(result);
const info1 = listeners[2].mock.calls[0][0];
expect(info1.pixels_blank / info1.pixels_sampled).toBe(blankness);
});
});

View File

@ -21,7 +21,7 @@ describe('SectionList', () => {
const component = ReactTestRenderer.create(
<SectionList
sections={[]}
renderItem={({item}) => <item value={item.key} />}
renderItem={({item}) => <item v={item.key} />}
/>
);
expect(component).toMatchSnapshot();
@ -30,7 +30,7 @@ describe('SectionList', () => {
const component = ReactTestRenderer.create(
<SectionList
sections={[{key: 's1', data: [{key: 'i1'}, {key: 'i2'}]}]}
renderItem={({item}) => <item value={item.key} />}
renderItem={({item}) => <item v={item.key} />}
renderSectionHeader={() => null}
/>
);
@ -39,29 +39,41 @@ describe('SectionList', () => {
it('renders all the bells and whistles', () => {
const component = ReactTestRenderer.create(
<SectionList
ItemSeparatorComponent={() => <defaultItemSeparator />}
ListFooterComponent={() => <footer />}
ListHeaderComponent={() => <header />}
SectionSeparatorComponent={() => <sectionSeparator />}
ItemSeparatorComponent={(props) => <defaultItemSeparator v={propStr(props)} />}
ListFooterComponent={(props) => <footer v={propStr(props)} />}
ListHeaderComponent={(props) => <header v={propStr(props)} />}
SectionSeparatorComponent={(props) => <sectionSeparator v={propStr(props)} />}
sections={[
{
renderItem: ({item}) => <itemForSection1 value={item.id} />,
key: '1st Section',
renderItem: (props) => <itemForSection1 v={propStr(props)} />,
key: 's1',
keyExtractor: (item, index) => item.id,
ItemSeparatorComponent: () => <itemSeparatorForSection1 />,
ItemSeparatorComponent: (props) => <itemSeparatorForSection1 v={propStr(props)} />,
data: [{id: 'i1s1'}, {id: 'i2s1'}],
},
{
key: '2nd Section',
key: 's2',
data: [{key: 'i1s2'}, {key: 'i2s2'}],
},
{
key: 's3',
data: [{key: 'i1s3'}, {key: 'i2s3'}],
},
]}
refreshing={false}
onRefresh={jest.fn()}
renderItem={({item}) => <defaultItem value={item.key} />}
renderSectionHeader={({section}) => <sectionHeader value={section.key} />}
renderItem={(props) => <defaultItem v={propStr(props)} />}
renderSectionHeader={(props) => <sectionHeader v={propStr(props)} />}
renderSectionFooter={(props) => <sectionFooter v={propStr(props)} />}
/>
);
expect(component).toMatchSnapshot();
});
});
function propStr(props) {
return Object.keys(props).map(k => {
const propObj = props[k] || {};
return `${k}:${propObj.key || propObj.id || props[k]}`;
}).join(',');
}

View File

@ -36,9 +36,11 @@ exports[`FlatList renders all the bells and whistles 1`] = `
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onRefresh={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onViewableItemsChanged={undefined}
refreshControl={
<RefreshControlMock
@ -146,8 +148,10 @@ exports[`FlatList renders empty list 1`] = `
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onViewableItemsChanged={undefined}
renderItem={[Function]}
renderScrollComponent={[Function]}
@ -174,8 +178,10 @@ exports[`FlatList renders null list 1`] = `
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onViewableItemsChanged={undefined}
renderItem={[Function]}
renderScrollComponent={[Function]}
@ -214,8 +220,10 @@ exports[`FlatList renders simple list 1`] = `
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onViewableItemsChanged={undefined}
renderItem={[Function]}
renderScrollComponent={[Function]}

View File

@ -28,8 +28,10 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onViewableItemsChanged={undefined}
renderItem={[Function]}
renderScrollComponent={[Function]}
@ -67,14 +69,14 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
onLayout={[Function]}
>
<item
value="i1"
v="i1"
/>
</View>
<View
onLayout={[Function]}
>
<item
value="i2"
v="i2"
/>
</View>
</View>
@ -99,7 +101,7 @@ exports[`SectionList renders all the bells and whistles 1`] = `
"id": "i2s1",
},
],
"key": "1st Section",
"key": "s1",
"keyExtractor": [Function],
"renderItem": [Function],
},
@ -112,7 +114,18 @@ exports[`SectionList renders all the bells and whistles 1`] = `
"key": "i2s2",
},
],
"key": "2nd Section",
"key": "s2",
},
Object {
"data": Array [
Object {
"key": "i1s3",
},
Object {
"key": "i2s3",
},
],
"key": "s3",
},
]
}
@ -126,9 +139,11 @@ exports[`SectionList renders all the bells and whistles 1`] = `
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onRefresh={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onViewableItemsChanged={undefined}
refreshControl={
<RefreshControlMock
@ -139,6 +154,7 @@ exports[`SectionList renders all the bells and whistles 1`] = `
refreshing={false}
renderItem={[Function]}
renderScrollComponent={[Function]}
renderSectionFooter={[Function]}
renderSectionHeader={[Function]}
scrollEventThrottle={50}
sections={
@ -153,7 +169,7 @@ exports[`SectionList renders all the bells and whistles 1`] = `
"id": "i2s1",
},
],
"key": "1st Section",
"key": "s1",
"keyExtractor": [Function],
"renderItem": [Function],
},
@ -166,7 +182,18 @@ exports[`SectionList renders all the bells and whistles 1`] = `
"key": "i2s2",
},
],
"key": "2nd Section",
"key": "s2",
},
Object {
"data": Array [
Object {
"key": "i1s3",
},
Object {
"key": "i2s3",
},
],
"key": "s3",
},
]
}
@ -174,6 +201,7 @@ exports[`SectionList renders all the bells and whistles 1`] = `
Array [
1,
4,
7,
]
}
stickySectionHeadersEnabled={true}
@ -185,24 +213,30 @@ exports[`SectionList renders all the bells and whistles 1`] = `
<View
onLayout={[Function]}
>
<header />
<header
v=""
/>
</View>
<View
onLayout={[Function]}
>
<sectionHeader
value="1st Section"
v="section:s1"
/>
</View>
<View
onLayout={[Function]}
>
<View>
<sectionSeparator />
<itemForSection1
value="i1s1"
<sectionSeparator
v="highlighted:false,leadingItem:undefined,leadingSection:undefined,section:s1,trailingItem:i1s1,trailingSection:s2"
/>
<itemForSection1
v="item:i1s1,index:0,section:s1,separators:[object Object]"
/>
<itemSeparatorForSection1
v="highlighted:false,leadingItem:i1s1,leadingSection:undefined,section:s1,trailingItem:i2s1,trailingSection:s2"
/>
<itemSeparatorForSection1 />
</View>
</View>
<View
@ -210,27 +244,36 @@ exports[`SectionList renders all the bells and whistles 1`] = `
>
<View>
<itemForSection1
value="i2s1"
v="item:i2s1,index:1,section:s1,separators:[object Object]"
/>
<sectionSeparator
v="highlighted:false,leadingItem:i2s1,leadingSection:undefined,section:s1,trailingItem:undefined,trailingSection:s2"
/>
<sectionFooter
v="section:s1"
/>
<sectionSeparator />
</View>
</View>
<View
onLayout={[Function]}
>
<sectionHeader
value="2nd Section"
v="section:s2"
/>
</View>
<View
onLayout={[Function]}
>
<View>
<sectionSeparator />
<defaultItem
value="i1s2"
<sectionSeparator
v="highlighted:false,leadingItem:undefined,leadingSection:s1,section:s2,trailingItem:i1s2,trailingSection:s3"
/>
<defaultItem
v="item:i1s2,index:0,section:s2,separators:[object Object]"
/>
<defaultItemSeparator
v="highlighted:false,leadingItem:i1s2,leadingSection:s1,section:s2,trailingItem:i2s2,trailingSection:s3"
/>
<defaultItemSeparator />
</View>
</View>
<View
@ -238,15 +281,59 @@ exports[`SectionList renders all the bells and whistles 1`] = `
>
<View>
<defaultItem
value="i2s2"
v="item:i2s2,index:1,section:s2,separators:[object Object]"
/>
<sectionSeparator
v="highlighted:false,leadingItem:i2s2,leadingSection:s1,section:s2,trailingItem:undefined,trailingSection:s3"
/>
<sectionFooter
v="section:s2"
/>
<sectionSeparator />
</View>
</View>
<View
onLayout={[Function]}
>
<footer />
<sectionHeader
v="section:s3"
/>
</View>
<View
onLayout={[Function]}
>
<View>
<sectionSeparator
v="highlighted:false,leadingItem:undefined,leadingSection:s2,section:s3,trailingItem:i1s3,trailingSection:undefined"
/>
<defaultItem
v="item:i1s3,index:0,section:s3,separators:[object Object]"
/>
<defaultItemSeparator
v="highlighted:false,leadingItem:i1s3,leadingSection:s2,section:s3,trailingItem:i2s3,trailingSection:undefined"
/>
</View>
</View>
<View
onLayout={[Function]}
>
<View>
<defaultItem
v="item:i2s3,index:1,section:s3,separators:[object Object]"
/>
<sectionSeparator
v="highlighted:false,leadingItem:i2s3,leadingSection:s2,section:s3,trailingItem:undefined,trailingSection:undefined"
/>
<sectionFooter
v="section:s3"
/>
</View>
</View>
<View
onLayout={[Function]}
>
<footer
v=""
/>
</View>
</View>
</RCTScrollView>
@ -266,8 +353,10 @@ exports[`SectionList renders empty list 1`] = `
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onViewableItemsChanged={undefined}
renderItem={[Function]}
renderScrollComponent={[Function]}

View File

@ -36,9 +36,11 @@ exports[`VirtualizedList renders all the bells and whistles 1`] = `
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onRefresh={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
refreshControl={
<RefreshControlMock
onRefresh={[Function]}
@ -121,8 +123,10 @@ exports[`VirtualizedList renders empty list 1`] = `
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
renderItem={[Function]}
renderScrollComponent={[Function]}
scrollEventThrottle={50}
@ -147,8 +151,10 @@ exports[`VirtualizedList renders null list 1`] = `
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
renderItem={[Function]}
renderScrollComponent={[Function]}
scrollEventThrottle={50}
@ -185,8 +191,10 @@ exports[`VirtualizedList renders simple list 1`] = `
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
renderItem={[Function]}
renderScrollComponent={[Function]}
scrollEventThrottle={50}
@ -239,8 +247,10 @@ exports[`VirtualizedList test getItem functionality where data is not an Array 1
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
renderItem={[Function]}
renderScrollComponent={[Function]}
scrollEventThrottle={50}