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

View File

@ -10,38 +10,35 @@
* @flow * @flow
*/ */
/* eslint-disable no-console-disallow */
'use strict'; 'use strict';
const performanceNow = require('fbjs/lib/performanceNow'); const performanceNow = require('fbjs/lib/performanceNow');
const warning = require('fbjs/lib/warning'); const warning = require('fbjs/lib/warning');
export type FillRateExceededInfo = { export type FillRateInfo = Info;
event: {
sample_type: string, class Info {
blankness: number, any_blank_count = 0;
blank_pixels_top: number, any_blank_ms = 0;
blank_pixels_bottom: number, any_blank_speed_sum = 0;
scroll_offset: number, mostly_blank_count = 0;
visible_length: number, mostly_blank_ms = 0;
scroll_speed: number, pixels_blank = 0;
first_frame: Object, pixels_sampled = 0;
last_frame: Object, pixels_scrolled = 0;
}, total_time_spent = 0;
aggregate: { sample_count = 0;
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},
},
};
type FrameMetrics = {inLayout?: boolean, length: number, offset: number}; type FrameMetrics = {inLayout?: boolean, length: number, offset: number};
let _listeners: Array<(FillRateExceededInfo) => void> = []; const DEBUG = false;
let _sampleRate = null;
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. * 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. * `SceneTracker.getActiveScene` to determine the context of the events.
*/ */
class FillRateHelper { class FillRateHelper {
_anyBlankStartTime = (null: ?number);
_enabled = false;
_getFrameMetrics: (index: number) => ?FrameMetrics; _getFrameMetrics: (index: number) => ?FrameMetrics;
_anyBlankCount = 0; _info = new Info();
_anyBlankMinSpeed = Number.MAX_SAFE_INTEGER; _mostlyBlankStartTime = (null: ?number);
_anyBlankSpeedSum = 0; _samplesStartTime = (null: ?number);
_sampleCounts = {};
_fractionBlankSum = 0;
_samplesStartTime = 0;
static addFillRateExceededListener( static addListener(
callback: (FillRateExceededInfo) => void callback: (FillRateInfo) => void
): {remove: () => void} { ): {remove: () => void} {
warning( warning(
_sampleRate !== null, _sampleRate !== null,
'Call `FillRateHelper.setSampleRate` before `addFillRateExceededListener`.' 'Call `FillRateHelper.setSampleRate` before `addListener`.'
); );
_listeners.push(callback); _listeners.push(callback);
return { return {
@ -79,16 +75,62 @@ class FillRateHelper {
_sampleRate = sampleRate; _sampleRate = sampleRate;
} }
static enabled(): boolean { static setMinSampleCount(minSampleCount: number) {
return (_sampleRate || 0) > 0.0; _minSampleCount = minSampleCount;
} }
constructor(getFrameMetrics: (index: number) => ?FrameMetrics) { constructor(getFrameMetrics: (index: number) => ?FrameMetrics) {
this._getFrameMetrics = getFrameMetrics; this._getFrameMetrics = getFrameMetrics;
this._enabled = (_sampleRate || 0) > Math.random();
this._resetData();
} }
computeInfoSampled( activate() {
sampleType: string, 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: { props: {
data: Array<any>, data: Array<any>,
getItemCount: (data: Array<any>) => number, getItemCount: (data: Array<any>) => number,
@ -99,22 +141,35 @@ class FillRateHelper {
last: number, last: number,
}, },
scrollMetrics: { scrollMetrics: {
dOffset: number,
offset: number, offset: number,
velocity: number, velocity: number,
visibleLength: number, visibleLength: number,
}, },
): ?FillRateExceededInfo { ): number {
if (!FillRateHelper.enabled() || (_sampleRate || 0) <= Math.random()) { if (!this._enabled || props.getItemCount(props.data) === 0 || this._samplesStartTime == null) {
return null; return 0;
} }
const start = performanceNow(); const {dOffset, offset, velocity, visibleLength} = scrollMetrics;
if (props.getItemCount(props.data) === 0) {
return null; // 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._anyBlankStartTime = null;
this._samplesStartTime = start; if (this._mostlyBlankStartTime != null) {
this._info.mostly_blank_ms += now - this._mostlyBlankStartTime;
} }
const {offset, velocity, visibleLength} = scrollMetrics; this._mostlyBlankStartTime = null;
let blankTop = 0; let blankTop = 0;
let first = state.first; let first = state.first;
let firstFrame = this._getFrameMetrics(first); let firstFrame = this._getFrameMetrics(first);
@ -122,7 +177,9 @@ class FillRateHelper {
firstFrame = this._getFrameMetrics(first); firstFrame = this._getFrameMetrics(first);
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)); blankTop = Math.min(visibleLength, Math.max(0, firstFrame.offset - offset));
} }
let blankBottom = 0; let blankBottom = 0;
@ -132,47 +189,38 @@ class FillRateHelper {
lastFrame = this._getFrameMetrics(last); lastFrame = this._getFrameMetrics(last);
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; const bottomEdge = lastFrame.offset + lastFrame.length;
blankBottom = Math.min(visibleLength, Math.max(0, offset + visibleLength - bottomEdge)); blankBottom = Math.min(visibleLength, Math.max(0, offset + visibleLength - bottomEdge));
} }
this._sampleCounts.all = (this._sampleCounts.all || 0) + 1; const pixels_blank = Math.round(blankTop + blankBottom);
this._sampleCounts[sampleType] = (this._sampleCounts[sampleType] || 0) + 1; const blankness = pixels_blank / visibleLength;
const blankness = (blankTop + blankBottom) / visibleLength;
if (blankness > 0) { if (blankness > 0) {
const scrollSpeed = Math.abs(velocity); this._anyBlankStartTime = now;
if (scrollSpeed && sampleType === 'onScroll') { this._info.any_blank_speed_sum += scrollSpeed;
this._anyBlankMinSpeed = Math.min(this._anyBlankMinSpeed, 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; } else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) {
this._anyBlankCount++; this.deactivateAndFlush();
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;
} }
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. * to improve perceived performance of scroll-to-top actions.
*/ */
initialNumToRender: number, 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 * 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 * 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: { renderItem?: ?(info: {
item: SectionItemT, item: SectionItemT,
index: number, index: number,
section: SectionBase<SectionItemT>,
separators: { separators: {
highlight: () => void, highlight: () => void,
unhighlight: () => void, unhighlight: () => void,
@ -66,6 +67,7 @@ type OptionalProps<SectionT: SectionBase<any>> = {
renderItem: (info: { renderItem: (info: {
item: Item, item: Item,
index: number, index: number,
section: SectionT,
separators: { separators: {
highlight: () => void, highlight: () => void,
unhighlight: () => void, unhighlight: () => void,
@ -73,10 +75,10 @@ type OptionalProps<SectionT: SectionBase<any>> = {
}, },
}) => ?React.Element<any>, }) => ?React.Element<any>,
/** /**
* Rendered in between each item, but not at the top or bottom. By default, `highlighted` and * Rendered in between each item, but not at the top or bottom. By default, `highlighted`,
* `leadingItem` props are provided. `renderItem` provides `separators.highlight`/`unhighlight` * `section`, and `[leading/trailing][Item/Separator]` props are provided. `renderItem` provides
* which will update the `highlighted` prop, but you can also add custom props with * `separators.highlight`/`unhighlight` which will update the `highlighted` prop, but you can also
* `separators.updateProps`. * add custom props with `separators.updateProps`.
*/ */
ItemSeparatorComponent?: ?ReactClass<any>, ItemSeparatorComponent?: ?ReactClass<any>,
/** /**
@ -88,8 +90,11 @@ type OptionalProps<SectionT: SectionBase<any>> = {
*/ */
ListFooterComponent?: ?(ReactClass<any> | React.Element<any>), ListFooterComponent?: ?(ReactClass<any> | React.Element<any>),
/** /**
* Rendered in between each section. Also receives `highlighted`, `leadingItem`, and any custom * Rendered at the top and bottom of each section (note this is different from
* props from `separators.updateProps`. * `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>, SectionSeparatorComponent?: ?ReactClass<any>,
/** /**
@ -151,6 +156,10 @@ type OptionalProps<SectionT: SectionBase<any>> = {
* iOS. See `stickySectionHeadersEnabled`. * iOS. See `stickySectionHeadersEnabled`.
*/ */
renderSectionHeader?: ?(info: {section: SectionT}) => ?React.Element<any>, 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 * 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. * 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 visibleBegin = Math.max(0, offset);
const visibleEnd = visibleBegin + visibleLength; const visibleEnd = visibleBegin + visibleLength;
const overscanLength = (windowSize - 1) * 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 overscanBegin = Math.max(0, visibleBegin - (1 - leadFactor) * overscanLength);
const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength); const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);
@ -129,13 +134,15 @@ function computeWindowedRenderLimits(
// possible. // possible.
break; break;
} }
if (firstShouldIncrement) { if (firstShouldIncrement &&
!(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore)) {
if (firstWillAddMore) { if (firstWillAddMore) {
newCellCount++; newCellCount++;
} }
first--; first--;
} }
if (lastShouldIncrement) { if (lastShouldIncrement &&
!(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore)) {
if (lastWillAddMore) { if (lastWillAddMore) {
newCellCount++; newCellCount++;
} }

View File

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

View File

@ -33,6 +33,7 @@ type SectionBase = {
renderItem?: ?({ renderItem?: ?({
item: SectionItem, item: SectionItem,
index: number, index: number,
section: SectionBase,
separators: { separators: {
highlight: () => void, highlight: () => void,
unhighlight: () => void, unhighlight: () => void,
@ -67,6 +68,7 @@ type OptionalProps<SectionT: SectionBase> = {
renderItem: (info: { renderItem: (info: {
item: Item, item: Item,
index: number, index: number,
section: SectionT,
separators: { separators: {
highlight: () => void, highlight: () => void,
unhighlight: () => void, unhighlight: () => void,
@ -77,6 +79,10 @@ type OptionalProps<SectionT: SectionBase> = {
* Rendered at the top of each section. * Rendered at the top of each section.
*/ */
renderSectionHeader?: ?({section: SectionT}) => ?React.Element<*>, 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 * Rendered at the bottom of every Section, except the very last one, in place of the normal
* ItemSeparatorComponent. * ItemSeparatorComponent.
@ -164,6 +170,10 @@ class VirtualizedSectionList<SectionT: SectionBase>
section: SectionT, section: SectionT,
key: string, // Key of the section or combined key for section + item key: string, // Key of the section or combined key for section + item
index: ?number, // Relative index within the section index: ?number, // Relative index within the section
leadingItem?: ?Item,
leadingSection?: ?SectionT,
trailingItem?: ?Item,
trailingSection?: ?SectionT,
} { } {
let itemIndex = index; let itemIndex = index;
const defaultKeyExtractor = this.props.keyExtractor; const defaultKeyExtractor = this.props.keyExtractor;
@ -178,13 +188,17 @@ class VirtualizedSectionList<SectionT: SectionBase>
if (itemIndex >= section.data.length) { if (itemIndex >= section.data.length) {
itemIndex -= section.data.length; itemIndex -= section.data.length;
} else if (itemIndex === -1) { } else if (itemIndex === -1) {
return {section, key, index: null}; return {section, key, index: null, trailingSection: this.props.sections[ii + 1]};
} else { } else {
const keyExtractor = section.keyExtractor || defaultKeyExtractor; const keyExtractor = section.keyExtractor || defaultKeyExtractor;
return { return {
section, section,
key: key + ':' + keyExtractor(section.data[itemIndex], itemIndex), key: key + ':' + keyExtractor(section.data[itemIndex], itemIndex),
index: 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} cellKey={info.key}
index={infoIndex} index={infoIndex}
item={item} item={item}
leadingItem={info.leadingItem}
leadingSection={info.leadingSection}
onUpdateSeparator={this._onUpdateSeparator} onUpdateSeparator={this._onUpdateSeparator}
prevCellKey={(this._subExtractor(index - 1) || {}).key} prevCellKey={(this._subExtractor(index - 1) || {}).key}
ref={(ref) => {this._cellRefs[info.key] = ref;}} ref={(ref) => {this._cellRefs[info.key] = ref;}}
renderItem={renderItem} renderItem={renderItem}
renderSectionFooter={infoIndex === info.section.data.length - 1
? this.props.renderSectionFooter
: undefined
}
section={info.section} section={info.section}
trailingItem={info.trailingItem}
trailingSection={info.trailingSection}
/> />
); );
} }
@ -259,7 +281,8 @@ class VirtualizedSectionList<SectionT: SectionBase>
if (!info) { if (!info) {
return null; return null;
} }
const ItemSeparatorComponent = info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent; const ItemSeparatorComponent =
info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent;
const {SectionSeparatorComponent} = this.props; const {SectionSeparatorComponent} = this.props;
const isLastItemInList = index === this.state.childProps.getItemCount() - 1; const isLastItemInList = index === this.state.childProps.getItemCount() - 1;
const isLastItemInSection = info.index === info.section.data.length - 1; const isLastItemInSection = info.index === info.section.data.length - 1;
@ -282,6 +305,7 @@ class VirtualizedSectionList<SectionT: SectionBase>
}, },
0 0
); );
return { return {
childProps: { childProps: {
...props, ...props,
@ -326,17 +350,30 @@ class ItemWithSeparator extends React.Component {
onUpdateSeparator: (cellKey: string, newProps: Object) => void, onUpdateSeparator: (cellKey: string, newProps: Object) => void,
prevCellKey?: ?string, prevCellKey?: ?string,
renderItem: Function, renderItem: Function,
renderSectionFooter: ?Function,
section: Object, section: Object,
leadingItem: ?Item,
leadingSection: ?Object,
trailingItem: ?Item,
trailingSection: ?Object,
}; };
state = { state = {
separatorProps: { separatorProps: {
highlighted: false, highlighted: false,
leadingItem: this.props.item, 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: { leadingSeparatorProps: {
highlighted: false, 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() { render() {
const {LeadingSeparatorComponent, SeparatorComponent, renderItem, item, index} = this.props; const {LeadingSeparatorComponent, SeparatorComponent, item, index, section} = this.props;
const element = renderItem({ const element = this.props.renderItem({
item, item,
index, index,
section,
separators: this._separators, separators: this._separators,
}); });
const leadingSeparator = LeadingSeparatorComponent && const leadingSeparator = LeadingSeparatorComponent &&
<LeadingSeparatorComponent {...this.state.leadingSeparatorProps} />; <LeadingSeparatorComponent {...this.state.leadingSeparatorProps} />;
const separator = SeparatorComponent && <SeparatorComponent {...this.state.separatorProps} />; 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'); const FillRateHelper = require('FillRateHelper');
let rowFramesGlobal; 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) { function getFrameMetrics(index: number) {
const frame = rowFramesGlobal[dataGlobal[index].key]; const frame = rowFramesGlobal[dataGlobal[index].key];
return {length: frame.height, offset: frame.y, inLayout: frame.inLayout}; return {length: frame.height, offset: frame.y, inLayout: frame.inLayout};
} }
function computeResult({helper, props, state, scroll}) { function computeResult({helper, props, state, scroll}): number {
return helper.computeInfoSampled( helper.activate();
'test', return helper.computeBlankness(
{ {
data: dataGlobal, data: dataGlobal,
fillRateTrackingSampleRate: 1,
getItemCount: (data2) => data2.length, getItemCount: (data2) => data2.length,
initialNumToRender: 10, initialNumToRender: 10,
...(props || {}), ...(props || {}),
}, },
{first: 0, last: 1, ...(state || {})}, {first: 1, last: 2, ...(state || {})},
{offset: 0, visibleLength: 100, ...(scroll || {})}, {offset: 0, visibleLength: 100, ...(scroll || {})},
); );
} }
describe('computeInfoSampled', function() { describe('computeBlankness', function() {
beforeEach(() => { beforeEach(() => {
FillRateHelper.setSampleRate(1); FillRateHelper.setSampleRate(1);
FillRateHelper.setMinSampleCount(0);
}); });
it('computes correct blankness of viewport', function() { it('computes correct blankness of viewport', function() {
const helper = new FillRateHelper(getFrameMetrics); const helper = new FillRateHelper(getFrameMetrics);
rowFramesGlobal = { rowFramesGlobal = {
header: {y: 0, height: 0, inLayout: true},
a: {y: 0, height: 50, inLayout: true}, a: {y: 0, height: 50, inLayout: true},
b: {y: 50, height: 50, inLayout: true}, b: {y: 50, height: 50, inLayout: true},
}; };
let result = computeResult({helper}); let blankness = computeResult({helper});
expect(result).toBeNull(); expect(blankness).toBe(0);
result = computeResult({helper, state: {last: 0}}); blankness = computeResult({helper, state: {last: 1}});
expect(result.event.blankness).toBe(0.5); expect(blankness).toBe(0.5);
result = computeResult({helper, scroll: {offset: 25}}); blankness = computeResult({helper, scroll: {offset: 25}});
expect(result.event.blankness).toBe(0.25); expect(blankness).toBe(0.25);
result = computeResult({helper, scroll: {visibleLength: 400}}); blankness = computeResult({helper, scroll: {visibleLength: 400}});
expect(result.event.blankness).toBe(0.75); expect(blankness).toBe(0.75);
result = computeResult({helper, scroll: {offset: 100}}); blankness = computeResult({helper, scroll: {offset: 100}});
expect(result.event.blankness).toBe(1); expect(blankness).toBe(1);
expect(result.aggregate.avg_blankness).toBe(0.5);
}); });
it('skips frames that are not in layout', function() { it('skips frames that are not in layout', function() {
const helper = new FillRateHelper(getFrameMetrics); const helper = new FillRateHelper(getFrameMetrics);
rowFramesGlobal = { rowFramesGlobal = {
header: {y: 0, height: 0, inLayout: false},
a: {y: 0, height: 10, inLayout: false}, a: {y: 0, height: 10, inLayout: false},
b: {y: 10, height: 30, inLayout: true}, b: {y: 10, height: 30, inLayout: true},
c: {y: 40, height: 40, inLayout: true}, c: {y: 40, height: 40, inLayout: true},
d: {y: 80, height: 20, inLayout: false}, d: {y: 80, height: 20, inLayout: false},
footer: {y: 100, height: 0, inLayout: false},
}; };
const result = computeResult({helper, state: {last: 3}}); const blankness = computeResult({helper, state: {last: 4}});
expect(result.event.blankness).toBe(0.3); expect(blankness).toBe(0.3);
}); });
it('sampling rate can disable', function() { it('sampling rate can disable', function() {
const helper = new FillRateHelper(getFrameMetrics); let helper = new FillRateHelper(getFrameMetrics);
rowFramesGlobal = { rowFramesGlobal = {
header: {y: 0, height: 0, inLayout: true},
a: {y: 0, height: 40, inLayout: true}, a: {y: 0, height: 40, inLayout: true},
b: {y: 40, height: 40, inLayout: true}, b: {y: 40, height: 40, inLayout: true},
}; };
let result = computeResult({helper}); let blankness = computeResult({helper});
expect(result.event.blankness).toBe(0.2); expect(blankness).toBe(0.2);
FillRateHelper.setSampleRate(0); FillRateHelper.setSampleRate(0);
result = computeResult({helper}); helper = new FillRateHelper(getFrameMetrics);
expect(result).toBeNull(); blankness = computeResult({helper});
expect(blankness).toBe(0);
}); });
it('can handle multiple listeners and unsubscribe', function() { it('can handle multiple listeners and unsubscribe', function() {
const listeners = [jest.fn(), jest.fn(), jest.fn()]; const listeners = [jest.fn(), jest.fn(), jest.fn()];
const subscriptions = listeners.map( const subscriptions = listeners.map(
(listener) => FillRateHelper.addFillRateExceededListener(listener) (listener) => FillRateHelper.addListener(listener)
); );
subscriptions[1].remove(); subscriptions[1].remove();
const helper = new FillRateHelper(getFrameMetrics); const helper = new FillRateHelper(getFrameMetrics);
rowFramesGlobal = { rowFramesGlobal = {
header: {y: 0, height: 0, inLayout: true},
a: {y: 0, height: 40, inLayout: true}, a: {y: 0, height: 40, inLayout: true},
b: {y: 40, height: 40, inLayout: true}, b: {y: 40, height: 40, inLayout: true},
}; };
const result = computeResult({helper}); const blankness = computeResult({helper});
expect(result.event.blankness).toBe(0.2); expect(blankness).toBe(0.2);
expect(listeners[0]).toBeCalledWith(result); 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[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( const component = ReactTestRenderer.create(
<SectionList <SectionList
sections={[]} sections={[]}
renderItem={({item}) => <item value={item.key} />} renderItem={({item}) => <item v={item.key} />}
/> />
); );
expect(component).toMatchSnapshot(); expect(component).toMatchSnapshot();
@ -30,7 +30,7 @@ describe('SectionList', () => {
const component = ReactTestRenderer.create( const component = ReactTestRenderer.create(
<SectionList <SectionList
sections={[{key: 's1', data: [{key: 'i1'}, {key: 'i2'}]}]} sections={[{key: 's1', data: [{key: 'i1'}, {key: 'i2'}]}]}
renderItem={({item}) => <item value={item.key} />} renderItem={({item}) => <item v={item.key} />}
renderSectionHeader={() => null} renderSectionHeader={() => null}
/> />
); );
@ -39,29 +39,41 @@ describe('SectionList', () => {
it('renders all the bells and whistles', () => { it('renders all the bells and whistles', () => {
const component = ReactTestRenderer.create( const component = ReactTestRenderer.create(
<SectionList <SectionList
ItemSeparatorComponent={() => <defaultItemSeparator />} ItemSeparatorComponent={(props) => <defaultItemSeparator v={propStr(props)} />}
ListFooterComponent={() => <footer />} ListFooterComponent={(props) => <footer v={propStr(props)} />}
ListHeaderComponent={() => <header />} ListHeaderComponent={(props) => <header v={propStr(props)} />}
SectionSeparatorComponent={() => <sectionSeparator />} SectionSeparatorComponent={(props) => <sectionSeparator v={propStr(props)} />}
sections={[ sections={[
{ {
renderItem: ({item}) => <itemForSection1 value={item.id} />, renderItem: (props) => <itemForSection1 v={propStr(props)} />,
key: '1st Section', key: 's1',
keyExtractor: (item, index) => item.id, keyExtractor: (item, index) => item.id,
ItemSeparatorComponent: () => <itemSeparatorForSection1 />, ItemSeparatorComponent: (props) => <itemSeparatorForSection1 v={propStr(props)} />,
data: [{id: 'i1s1'}, {id: 'i2s1'}], data: [{id: 'i1s1'}, {id: 'i2s1'}],
}, },
{ {
key: '2nd Section', key: 's2',
data: [{key: 'i1s2'}, {key: 'i2s2'}], data: [{key: 'i1s2'}, {key: 'i2s2'}],
}, },
{
key: 's3',
data: [{key: 'i1s3'}, {key: 'i2s3'}],
},
]} ]}
refreshing={false} refreshing={false}
onRefresh={jest.fn()} onRefresh={jest.fn()}
renderItem={({item}) => <defaultItem value={item.key} />} renderItem={(props) => <defaultItem v={propStr(props)} />}
renderSectionHeader={({section}) => <sectionHeader value={section.key} />} renderSectionHeader={(props) => <sectionHeader v={propStr(props)} />}
renderSectionFooter={(props) => <sectionFooter v={propStr(props)} />}
/> />
); );
expect(component).toMatchSnapshot(); 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]} onContentSizeChange={[Function]}
onEndReachedThreshold={2} onEndReachedThreshold={2}
onLayout={[Function]} onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onRefresh={[Function]} onRefresh={[Function]}
onScroll={[Function]} onScroll={[Function]}
onScrollBeginDrag={[Function]} onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onViewableItemsChanged={undefined} onViewableItemsChanged={undefined}
refreshControl={ refreshControl={
<RefreshControlMock <RefreshControlMock
@ -146,8 +148,10 @@ exports[`FlatList renders empty list 1`] = `
onContentSizeChange={[Function]} onContentSizeChange={[Function]}
onEndReachedThreshold={2} onEndReachedThreshold={2}
onLayout={[Function]} onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]} onScroll={[Function]}
onScrollBeginDrag={[Function]} onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onViewableItemsChanged={undefined} onViewableItemsChanged={undefined}
renderItem={[Function]} renderItem={[Function]}
renderScrollComponent={[Function]} renderScrollComponent={[Function]}
@ -174,8 +178,10 @@ exports[`FlatList renders null list 1`] = `
onContentSizeChange={[Function]} onContentSizeChange={[Function]}
onEndReachedThreshold={2} onEndReachedThreshold={2}
onLayout={[Function]} onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]} onScroll={[Function]}
onScrollBeginDrag={[Function]} onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onViewableItemsChanged={undefined} onViewableItemsChanged={undefined}
renderItem={[Function]} renderItem={[Function]}
renderScrollComponent={[Function]} renderScrollComponent={[Function]}
@ -214,8 +220,10 @@ exports[`FlatList renders simple list 1`] = `
onContentSizeChange={[Function]} onContentSizeChange={[Function]}
onEndReachedThreshold={2} onEndReachedThreshold={2}
onLayout={[Function]} onLayout={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]} onScroll={[Function]}
onScrollBeginDrag={[Function]} onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onViewableItemsChanged={undefined} onViewableItemsChanged={undefined}
renderItem={[Function]} renderItem={[Function]}
renderScrollComponent={[Function]} renderScrollComponent={[Function]}

View File

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

View File

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