Support virtualization and onViewableItemsChanged for nested, same-orientation VirtualizedLists

Reviewed By: sahrens

Differential Revision: D6330846

fbshipit-source-id: c555f4d449b75753befbd376dbf4e6fb4812fa75
This commit is contained in:
Logan Daniels 2017-12-18 13:16:04 -08:00 committed by Facebook Github Bot
parent d2dc451407
commit 2668dc8e1b
2 changed files with 277 additions and 25 deletions

View File

@ -114,6 +114,15 @@ function computeWindowedRenderLimits(
);
const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);
const lastItemOffset = getFrameMetricsApprox(itemCount - 1).offset;
if (lastItemOffset < overscanBegin) {
// Entire list is before our overscan window
return {
first: Math.max(0, itemCount - 1 - maxToRenderPerBatch),
last: itemCount - 1,
};
}
// Find the indices that correspond to the items at the render boundaries we're targetting.
let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets(
[overscanBegin, visibleBegin, visibleEnd, overscanEnd],

View File

@ -20,6 +20,7 @@ const ReactNative = require('ReactNative');
const RefreshControl = require('RefreshControl');
const ScrollView = require('ScrollView');
const StyleSheet = require('StyleSheet');
const UIManager = require('UIManager');
const View = require('View');
const ViewabilityHelper = require('ViewabilityHelper');
@ -129,6 +130,12 @@ type OptionalProps = {
* a rendered element.
*/
ListHeaderComponent?: ?(React.ComponentType<any> | React.Element<any>),
/**
* A unique identifier for this list. If there are multiple VirtualizedLists at the same level of
* nesting within another VirtualizedList, this key is necessary for virtualization to
* work properly.
*/
listKey?: string,
/**
* The maximum number of items to render in each incremental render batch. The more rendered at
* once, the better the fill rate, but responsiveness my suffer because rendering content may
@ -206,6 +213,19 @@ export type Props = RequiredProps & OptionalProps;
let _usedIndexForKey = false;
type Frame = {
offset: number,
length: number,
index: number,
inLayout: boolean,
};
type ChildListState = {
first: number,
last: number,
frames: {[key: number]: Frame},
};
type State = {first: number, last: number};
/**
@ -412,26 +432,92 @@ class VirtualizedList extends React.PureComponent<Props, State> {
};
static contextTypes = {
virtualizedListCellRenderer: PropTypes.shape({
cellKey: PropTypes.string,
}),
virtualizedList: PropTypes.shape({
getScrollMetrics: PropTypes.func,
horizontal: PropTypes.bool,
getOutermostParentListRef: PropTypes.func,
registerAsNestedChild: PropTypes.func,
unregisterAsNestedChild: PropTypes.func,
}),
};
static childContextTypes = {
virtualizedList: PropTypes.shape({
getScrollMetrics: PropTypes.func,
horizontal: PropTypes.bool,
getOutermostParentListRef: PropTypes.func,
registerAsNestedChild: PropTypes.func,
unregisterAsNestedChild: PropTypes.func,
}),
};
getChildContext() {
return {
virtualizedList: {
getScrollMetrics: this._getScrollMetrics,
horizontal: this.props.horizontal,
// TODO: support nested virtualization and onViewableItemsChanged
getOutermostParentListRef: this._getOutermostParentListRef,
registerAsNestedChild: this._registerAsNestedChild,
unregisterAsNestedChild: this._unregisterAsNestedChild,
},
};
}
_getScrollMetrics = () => {
return this._scrollMetrics;
};
hasMore(): boolean {
return this._hasMore;
}
_getOutermostParentListRef = () => {
if (this._isNestedWithSameOrientation()) {
return this.context.virtualizedList.getOutermostParentListRef();
} else {
return this;
}
};
_registerAsNestedChild = (childList: {
cellKey: string,
key: string,
ref: VirtualizedList,
}): ?ChildListState => {
// Register the mapping between this child key and the cellKey for its cell
const childListsInCell =
this._cellKeysToChildListKeys.get(childList.cellKey) || new Set();
childListsInCell.add(childList.key);
this._cellKeysToChildListKeys.set(childList.cellKey, childListsInCell);
const existingChildData = this._nestedChildLists.get(childList.key);
invariant(
!(existingChildData && existingChildData.ref !== null),
'A VirtualizedList contains a cell which itself contains ' +
'more than one VirtualizedList of the same orientation as the parent ' +
'list. You must pass a unique listKey prop to each sibling list.',
);
this._nestedChildLists.set(childList.key, {
ref: childList.ref,
state: null,
});
return existingChildData && existingChildData.state;
};
_unregisterAsNestedChild = (childList: {
key: string,
state: ChildListState,
}): void => {
this._nestedChildLists.set(childList.key, {
ref: null,
state: childList.state,
});
};
state: State;
constructor(props: Props, context: Object) {
@ -441,11 +527,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' +
'to support native onScroll events with useNativeDriver',
);
invariant(
!(this._isNestedWithSameOrientation() && props.onViewableItemsChanged),
'Nesting lists that scroll in the same direction does not support onViewableItemsChanged' +
'on the inner list.',
);
this._fillRateHelper = new FillRateHelper(this._getFrameMetrics);
this._updateCellsToRenderBatcher = new Batchinator(
@ -467,7 +548,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
});
}
this.state = {
let initialState = {
first: this.props.initialScrollIndex || 0,
last:
Math.min(
@ -475,8 +556,27 @@ class VirtualizedList extends React.PureComponent<Props, State> {
(this.props.initialScrollIndex || 0) + this.props.initialNumToRender,
) - 1,
};
if (this._isNestedWithSameOrientation()) {
const storedState = this.context.virtualizedList.registerAsNestedChild({
cellKey: this.context.virtualizedListCellRenderer.cellKey,
key:
this.props.listKey ||
this.context.virtualizedListCellRenderer.cellKey,
ref: this,
});
if (storedState) {
initialState = storedState;
this.state = storedState;
this._frames = storedState.frames;
}
}
this.state = initialState;
}
componentWillMount() {}
componentDidMount() {
if (this.props.initialScrollIndex) {
this._initialScrollIndexTimeout = setTimeout(
@ -491,8 +591,20 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
componentWillUnmount() {
if (this._isNestedWithSameOrientation()) {
this.context.virtualizedList.unregisterAsNestedChild({
key:
this.props.listKey ||
this.context.virtualizedListCellRenderer.cellKey,
state: {
first: this.state.first,
last: this.state.last,
frames: this._frames,
},
});
}
this._updateViewableItems(null);
this._updateCellsToRenderBatcher.dispose();
this._updateCellsToRenderBatcher.dispose({abort: true});
this._viewabilityTuples.forEach(tuple => {
tuple.viewabilityHelper.dispose();
});
@ -549,6 +661,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
for (let ii = first; ii <= last; ii++) {
const item = getItem(data, ii);
const key = keyExtractor(item, ii);
this._indicesToKeys.set(ii, key);
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
stickyHeaderIndices.push(cells.length);
}
@ -585,9 +698,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
};
_isVirtualizationDisabled(): boolean {
return (
this.props.disableVirtualization || this._isNestedWithSameOrientation()
);
return this.props.disableVirtualization;
}
_isNestedWithSameOrientation(): boolean {
@ -606,7 +717,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
'Consider using `numColumns` with `FlatList` instead.',
);
}
const {
ListEmptyComponent,
ListFooterComponent,
@ -779,6 +889,10 @@ class VirtualizedList extends React.PureComponent<Props, State> {
if (inversionStyle) {
scrollProps.style = [inversionStyle, this.props.style];
}
this._hasMore =
this.state.last < this.props.getItemCount(this.props.data) - 1;
const ret = React.cloneElement(
(this.props.renderScrollComponent || this._defaultRenderScrollComponent)(
scrollProps,
@ -805,15 +919,25 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
_averageCellLength = 0;
// Maps a cell key to the set of keys for all outermost child lists within that cell
_cellKeysToChildListKeys: Map<string, Set<string>> = new Map();
_cellRefs = {};
_hasDataChangedSinceEndReached = true;
_hasWarned = {};
_highestMeasuredFrameIndex = 0;
_headerLength = 0;
_initialScrollIndexTimeout = 0;
_fillRateHelper: FillRateHelper;
_frames = {};
_footerLength = 0;
_hasDataChangedSinceEndReached = true;
_hasMore = false;
_hasWarned = {};
_highestMeasuredFrameIndex = 0;
_headerLength = 0;
_indicesToKeys: Map<number, string> = new Map();
_initialScrollIndexTimeout = 0;
_nestedChildLists: Map<
string,
{ref: ?VirtualizedList, state: ?ChildListState},
> = new Map();
_offsetFromParentVirtualizedList: number = 0;
_prevParentOffset: number = 0;
_scrollMetrics = {
contentLength: 0,
dOffset: 0,
@ -910,10 +1034,41 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
};
_onLayout = (e: Object) => {
this._scrollMetrics.visibleLength = this._selectLength(
e.nativeEvent.layout,
_measureLayoutRelativeToContainingList(): void {
UIManager.measureLayout(
ReactNative.findNodeHandle(this),
ReactNative.findNodeHandle(
this.context.virtualizedList.getOutermostParentListRef(),
),
error => {
console.warn(
"VirtualizedList: Encountered an error while measuring a list's" +
' offset from its containing VirtualizedList.',
);
},
(x, y, width, height) => {
this._offsetFromParentVirtualizedList = this._selectOffset({x, y});
this._scrollMetrics.contentLength = this._selectLength({width, height});
const scrollMetrics = this._convertParentScrollMetrics(
this.context.virtualizedList.getScrollMetrics(),
);
this._scrollMetrics.visibleLength = scrollMetrics.visibleLength;
this._scrollMetrics.offset = scrollMetrics.offset;
},
);
}
_onLayout = (e: Object) => {
if (this._isNestedWithSameOrientation()) {
// Need to adjust our scroll metrics to be relative to our containing
// VirtualizedList before we can make claims about list item viewability
this._measureLayoutRelativeToContainingList();
} else {
this._scrollMetrics.visibleLength = this._selectLength(
e.nativeEvent.layout,
);
}
this.props.onLayout && this.props.onLayout(e);
this._scheduleCellsToRenderUpdate();
this._maybeCallOnEndReached();
@ -1033,17 +1188,63 @@ class VirtualizedList extends React.PureComponent<Props, State> {
this._maybeCallOnEndReached();
};
/* Translates metrics from a scroll event in a parent VirtualizedList into
* coordinates relative to the child list.
*/
_convertParentScrollMetrics = (metrics: {
visibleLength: number,
offset: number,
}) => {
// Offset of the top of the nested list relative to the top of its parent's viewport
const offset = metrics.offset - this._offsetFromParentVirtualizedList;
// Child's visible length is the same as its parent's
const visibleLength = metrics.visibleLength;
const dOffset = offset - this._scrollMetrics.offset;
const contentLength = this._scrollMetrics.contentLength;
return {
visibleLength,
contentLength,
offset,
dOffset,
};
};
_onScroll = (e: Object) => {
this._nestedChildLists.forEach(childList => {
childList.ref && childList.ref._onScroll(e);
});
if (this.props.onScroll) {
this.props.onScroll(e);
}
const timestamp = e.timeStamp;
const visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement);
const contentLength = this._selectLength(e.nativeEvent.contentSize);
const offset = this._selectOffset(e.nativeEvent.contentOffset);
let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement);
let contentLength = this._selectLength(e.nativeEvent.contentSize);
let offset = this._selectOffset(e.nativeEvent.contentOffset);
let dOffset = offset - this._scrollMetrics.offset;
if (this._isNestedWithSameOrientation()) {
if (this._scrollMetrics.contentLength === 0) {
// Ignore scroll events until onLayout has been called and we
// know our offset from our offset from our parent
return;
}
({
visibleLength,
contentLength,
offset,
dOffset,
} = this._convertParentScrollMetrics({
visibleLength,
offset,
}));
}
const dt = this._scrollMetrics.timestamp
? Math.max(1, timestamp - this._scrollMetrics.timestamp)
: 1;
const velocity = dOffset / dt;
if (
dt > 500 &&
this._scrollMetrics.dt > 500 &&
@ -1058,8 +1259,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
);
this._hasWarned.perf = true;
}
const dOffset = offset - this._scrollMetrics.offset;
const velocity = dOffset / dt;
this._scrollMetrics = {
contentLength,
dt,
@ -1175,6 +1374,36 @@ class VirtualizedList extends React.PureComponent<Props, State> {
last: Math.min(state.last + renderAhead, getItemCount(data) - 1),
};
}
if (newState && this._nestedChildLists.size > 0) {
const newFirst = newState.first;
const newLast = newState.last;
// If some cell in the new state has a child list in it, we should only render
// up through that item, so that we give that list a chance to render.
// Otherwise there's churn from multiple child lists mounting and un-mounting
// their items.
for (let ii = newFirst; ii <= newLast; ii++) {
const cellKeyForIndex = this._indicesToKeys.get(ii);
const childListKeys =
cellKeyForIndex &&
this._cellKeysToChildListKeys.get(cellKeyForIndex);
if (!childListKeys) {
continue;
}
let someChildHasMore = false;
// For each cell, need to check whether any child list in it has more elements to render
for (let childKey of childListKeys) {
const childList = this._nestedChildLists.get(childKey);
if (childList && childList.ref && childList.ref.hasMore()) {
someChildHasMore = true;
break;
}
}
if (someChildHasMore) {
newState.last = ii;
break;
}
}
}
return newState;
});
};
@ -1292,6 +1521,20 @@ class CellRenderer extends React.Component<
},
};
static childContextTypes = {
virtualizedListCellRenderer: PropTypes.shape({
cellKey: PropTypes.string,
}),
};
getChildContext() {
return {
virtualizedListCellRenderer: {
cellKey: this.props.cellKey,
},
};
}
// TODO: consider factoring separator stuff out of VirtualizedList into FlatList since it's not
// reused by SectionList and we can keep VirtualizedList simpler.
_separators = {