[change] Update VirtualizedList implementation

Mirror contents of React Native 0.57.5

Ref #1172
This commit is contained in:
Nicolas Gallagher
2018-11-17 14:37:41 -08:00
parent 04b71784a4
commit 1d1ebdb984

View File

@@ -7,6 +7,8 @@
* @noflow
* @format
*/
'use strict';
import Batchinator from '../Batchinator';
import FillRateHelper from '../FillRateHelper';
import PropTypes from 'prop-types';
@@ -34,6 +36,8 @@ type DangerouslyImpreciseStyleProp = any;
type Item = any;
type ViewStyleProp = any;
export type renderItemType = (info: any) => ?React.Element<any>;
type ViewabilityHelperCallbackTuple = {
@@ -118,11 +122,19 @@ type OptionalProps = {
* a rendered element.
*/
ListFooterComponent?: ?(React.ComponentType<any> | React.Element<any>),
/**
* Styling for internal View for ListFooterComponent
*/
ListFooterComponentStyle?: ViewStyleProp,
/**
* Rendered at the top of all the items. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListHeaderComponent?: ?(React.ComponentType<any> | React.Element<any>),
/**
* Styling for internal View for ListHeaderComponent
*/
ListHeaderComponentStyle?: ViewStyleProp,
/**
* 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
@@ -166,6 +178,12 @@ type OptionalProps = {
* @platform android
*/
progressViewOffset?: number,
/**
* A custom refresh control element. When set, it overrides the default
* <RefreshControl> component built internally. The onRefresh and refreshing
* props are also ignored. Only works for vertical VirtualizedList.
*/
refreshControl?: ?React.Element<any>,
/**
* Set this true while waiting for new data from a refresh.
*/
@@ -205,6 +223,7 @@ type OptionalProps = {
export type Props = RequiredProps & OptionalProps;
let _usedIndexForKey = false;
let _keylessItemComponentName: string = '';
type Frame = {
offset: number,
@@ -399,7 +418,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
if (this._scrollRef && this._scrollRef.getScrollableNode) {
return this._scrollRef.getScrollableNode();
} else {
return findNodeHandle(this._scrollRef);
return ReactNative.findNodeHandle(this._scrollRef);
}
}
@@ -410,7 +429,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
static defaultProps = {
disableVirtualization: process.env.NODE_ENV === 'test',
disableVirtualization: false,
horizontal: false,
initialNumToRender: 10,
keyExtractor: (item: Item, index: number) => {
@@ -418,6 +437,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
return item.key;
}
_usedIndexForKey = true;
if (item.type && item.type.displayName) {
_keylessItemComponentName = item.type.displayName;
}
return String(index);
},
maxToRenderPerBatch: 10,
@@ -740,12 +762,16 @@ class VirtualizedList extends React.PureComponent<Props, State> {
<VirtualizedCellWrapper
cellKey={this._getCellKey() + '-header'}
key="$header">
<View onLayout={this._onLayoutHeader} style={inversionStyle}>
{/*
Flow doesn't know this is a React.Element and not a React.Component
$FlowFixMe https://fburl.com/b9xmtm09
*/}
{element}
<View
onLayout={this._onLayoutHeader}
style={StyleSheet.compose(
inversionStyle,
this.props.ListHeaderComponentStyle,
)}>
{
// $FlowFixMe - Typing ReactNativeComponent revealed errors
element
}
</View>
</VirtualizedCellWrapper>,
);
@@ -753,6 +779,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
const itemCount = this.props.getItemCount(data);
if (itemCount > 0) {
_usedIndexForKey = false;
_keylessItemComponentName = '';
const spacerKey = !horizontal ? 'height' : 'width';
const lastInitialIndex = this.props.initialScrollIndex
? -1
@@ -776,8 +803,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
const stickyBlock = this._getFrameMetricsApprox(ii);
const leadSpace =
stickyBlock.offset - (initBlock.offset + initBlock.length);
const leadSpace = stickyBlock.offset - initBlock.offset;
cells.push(
<View key="$sticky_lead" style={{[spacerKey]: leadSpace}} />,
);
@@ -822,6 +848,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
console.warn(
'VirtualizedList: missing keys for items, make sure to specify a key property on each ' +
'item or provide a custom keyExtractor.',
_keylessItemComponentName,
);
this._hasWarned.keys = true;
}
@@ -843,23 +870,25 @@ class VirtualizedList extends React.PureComponent<Props, State> {
);
}
} else if (ListEmptyComponent) {
const element = React.isValidElement(ListEmptyComponent) ? (
const element: React.Element<any> = ((React.isValidElement(
ListEmptyComponent,
) ? (
ListEmptyComponent
) : (
// $FlowFixMe
<ListEmptyComponent />
);
)): any);
cells.push(
<View
key="$empty"
onLayout={this._onLayoutEmpty}
style={inversionStyle}>
{/*
Flow doesn't know this is a React.Element and not a React.Component
$FlowFixMe https://fburl.com/b9xmtm09
*/}
{element}
</View>,
React.cloneElement(element, {
key: '$empty',
onLayout: event => {
this._onLayoutEmpty(event);
if (element.props.onLayout) {
element.props.onLayout(event);
}
},
style: [element.props.style, inversionStyle],
}),
);
}
if (ListFooterComponent) {
@@ -873,12 +902,16 @@ class VirtualizedList extends React.PureComponent<Props, State> {
<VirtualizedCellWrapper
cellKey={this._getCellKey() + '-footer'}
key="$footer">
<View onLayout={this._onLayoutFooter} style={inversionStyle}>
{/*
Flow doesn't know this is a React.Element and not a React.Component
$FlowFixMe https://fburl.com/b9xmtm09
*/}
{element}
<View
onLayout={this._onLayoutFooter}
style={StyleSheet.compose(
inversionStyle,
this.props.ListFooterComponentStyle,
)}>
{
// $FlowFixMe - Typing ReactNativeComponent revealed errors
element
}
</View>
</VirtualizedCellWrapper>,
);
@@ -892,12 +925,16 @@ class VirtualizedList extends React.PureComponent<Props, State> {
onScrollEndDrag: this._onScrollEndDrag,
onMomentumScrollEnd: this._onMomentumScrollEnd,
scrollEventThrottle: this.props.scrollEventThrottle, // TODO: Android support
invertStickyHeaders: this.props.invertStickyHeaders !== undefined
? this.props.invertStickyHeaders
: this.props.inverted,
invertStickyHeaders:
this.props.invertStickyHeaders !== undefined
? this.props.invertStickyHeaders
: this.props.inverted,
stickyHeaderIndices,
};
if (inversionStyle) {
/* $FlowFixMe(>=0.70.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.70 was deployed. To see the error delete
* this comment and run Flow. */
scrollProps.style = [inversionStyle, this.props.style];
}
@@ -908,6 +945,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
(this.props.renderScrollComponent || this._defaultRenderScrollComponent)(
scrollProps,
),
// $FlowFixMe Invalid prop usage
{
ref: this._captureScrollRef,
},
@@ -989,9 +1027,11 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
_defaultRenderScrollComponent = props => {
const onRefresh = props.onRefresh;
if (this._isNestedWithSameOrientation()) {
// $FlowFixMe - Typing ReactNativeComponent revealed errors
return <View {...props} />;
} else if (props.onRefresh) {
} else if (onRefresh) {
invariant(
typeof props.refreshing === 'boolean',
'`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' +
@@ -999,21 +1039,24 @@ class VirtualizedList extends React.PureComponent<Props, State> {
'`',
);
return (
// $FlowFixMe Invalid prop usage
<ScrollView
{...props}
refreshControl={
/* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This
* comment suppresses an error when upgrading Flow's support for
* React. To see the error delete this comment and run Flow. */
<RefreshControl
refreshing={props.refreshing}
onRefresh={props.onRefresh}
progressViewOffset={props.progressViewOffset}
/>
props.refreshControl == null ? (
<RefreshControl
refreshing={props.refreshing}
onRefresh={onRefresh}
progressViewOffset={props.progressViewOffset}
/>
) : (
props.refreshControl
)
}
/>
);
} else {
// $FlowFixMe Invalid prop usage
return <ScrollView {...props} />;
}
};
@@ -1046,6 +1089,17 @@ class VirtualizedList extends React.PureComponent<Props, State> {
} else {
this._frames[cellKey].inLayout = true;
}
const childListKeys = this._cellKeysToChildListKeys.get(cellKey);
if (childListKeys) {
for (let childKey of childListKeys) {
const childList = this._nestedChildLists.get(childKey);
childList &&
childList.ref &&
childList.ref.measureLayoutRelativeToContainingList();
}
}
this._computeBlankness();
}
@@ -1056,36 +1110,48 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
};
_measureLayoutRelativeToContainingList(): void {
UIManager.measureLayout(
findNodeHandle(this),
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});
measureLayoutRelativeToContainingList(): void {
// TODO (T35574538): findNodeHandle sometimes crashes with "Unable to find
// node on an unmounted component" during scrolling
try {
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;
},
);
const scrollMetrics = this._convertParentScrollMetrics(
this.context.virtualizedList.getScrollMetrics(),
);
this._scrollMetrics.visibleLength = scrollMetrics.visibleLength;
this._scrollMetrics.offset = scrollMetrics.offset;
},
);
} catch (error) {
console.warn(
'measureLayoutRelativeToContainingList threw an error',
error.stack,
);
}
}
_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();
this.measureLayoutRelativeToContainingList();
} else {
this._scrollMetrics.visibleLength = this._selectLength(
e.nativeEvent.layout,
@@ -1115,6 +1181,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
const itemCount = this.props.getItemCount(this.props.data);
for (let ii = 0; ii < itemCount; ii++) {
const frame = this._getFrameMetricsApprox(ii);
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.68 was deployed. To see the error delete this
* comment and run Flow. */
if (frame.inLayout) {
framesInLayout.push(frame);
}
@@ -1323,18 +1392,26 @@ class VirtualizedList extends React.PureComponent<Props, State> {
const {offset, visibleLength, velocity} = this._scrollMetrics;
const itemCount = this.props.getItemCount(this.props.data);
let hiPri = false;
if (first > 0 || last < itemCount - 1) {
const scrollingThreshold =
/* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.63 was deployed. To see the error delete
* this comment and run Flow. */
(this.props.onEndReachedThreshold * visibleLength) / 2;
// Mark as high priority if we're close to the start of the first item
// But only if there are items before the first rendered item
if (first > 0) {
const distTop = offset - this._getFrameMetricsApprox(first).offset;
hiPri =
hiPri || distTop < 0 || (velocity < -2 && distTop < scrollingThreshold);
}
// Mark as high priority if we're close to the end of the last item
// But only if there are items after the last rendered item
if (last < itemCount - 1) {
const distBottom =
this._getFrameMetricsApprox(last).offset - (offset + visibleLength);
const scrollingThreshold =
/* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.63 was deployed. To see the error delete
* this comment and run Flow. */
this.props.onEndReachedThreshold * visibleLength / 2;
hiPri =
Math.min(distTop, distBottom) < 0 ||
(velocity < -2 && distTop < scrollingThreshold) ||
hiPri ||
distBottom < 0 ||
(velocity > 2 && distBottom < scrollingThreshold);
}
// Only trigger high-priority updates if we've actually rendered cells,
@@ -1562,6 +1639,7 @@ class CellRenderer extends React.Component<
renderItem: renderItemType,
},
prevCellKey: ?string
prevCellKey: ?string,
},
$FlowFixMeState,
> {
@@ -1639,6 +1717,9 @@ class CellRenderer extends React.Component<
separators: this._separators,
});
const onLayout =
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.68 was deployed. To see the error delete this
* comment and run Flow. */
getItemLayout && !parentProps.debug && !fillRateHelper.enabled()
? undefined
: this.props.onLayout;
@@ -1702,15 +1783,6 @@ const styles = StyleSheet.create({
horizontallyInverted: {
transform: [{scaleX: -1}],
},
row: {
flexDirection: 'row'
},
rowReverse: {
flexDirection: 'row-reverse'
},
columnReverse: {
flexDirection: 'column-reverse'
}
});
export default VirtualizedList;