Extract VirtualizedListCellRenderer

Summary:
VirtualizedList is large and complicated, getting larger and more complicated. This splits out a subcomponent, the cell renderer into its own file, since it is relatively isolated already. This uses the copy from VirtualizedList_EXPERIMENTAL, whose only real difference is exposing focus capture events to the containing VirtualizedList.

Changelog:
[Internal][Changed] - Extract VirtualizedListCellRenderer

Reviewed By: rshest

Differential Revision: D39648087

fbshipit-source-id: bb7c2eff0c658713c256650596f86e8788019baf
This commit is contained in:
Nick Gerleman 2022-09-20 05:21:08 -07:00 коммит произвёл Facebook GitHub Bot
Родитель bc5cb7cd79
Коммит 8f0975a3b7
3 изменённых файлов: 268 добавлений и 466 удалений

Просмотреть файл

@ -33,6 +33,7 @@ import {
keyExtractor as defaultKeyExtractor, keyExtractor as defaultKeyExtractor,
} from './VirtualizeUtils'; } from './VirtualizeUtils';
import * as VirtualizedListInjection from './VirtualizedListInjection'; import * as VirtualizedListInjection from './VirtualizedListInjection';
import CellRenderer from './VirtualizedListCellRenderer';
import * as React from 'react'; import * as React from 'react';
import ChildListCollection from './ChildListCollection'; import ChildListCollection from './ChildListCollection';
@ -879,7 +880,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
} }
_averageCellLength = 0; _averageCellLength = 0;
_cellRefs: {[string]: null | CellRenderer} = {}; _cellRefs: {[string]: null | CellRenderer<any>} = {};
_fillRateHelper: FillRateHelper; _fillRateHelper: FillRateHelper;
_frames: { _frames: {
[string]: { [string]: {
@ -1599,222 +1600,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
} }
} }
type CellRendererProps = {
CellRendererComponent?: ?React.ComponentType<any>,
ItemSeparatorComponent: ?React.ComponentType<
any | {highlighted: boolean, leadingItem: ?Item},
>,
ListItemComponent?: ?(React.ComponentType<any> | React.Element<any>),
cellKey: string,
debug?: ?boolean,
fillRateHelper: FillRateHelper,
getItemLayout?: (
data: any,
index: number,
) => {
length: number,
offset: number,
index: number,
...
},
horizontal: ?boolean,
index: number,
inversionStyle: ViewStyleProp,
item: Item,
// This is extracted by ScrollViewStickyHeader
onCellLayout: (event: Object, cellKey: string, index: number) => void,
onUnmount: (cellKey: string) => void,
onUpdateSeparators: (cellKeys: Array<?string>, props: Object) => void,
prevCellKey: ?string,
renderItem?: ?RenderItemType<Item>,
...
};
type CellRendererState = {
separatorProps: $ReadOnly<{|
highlighted: boolean,
leadingItem: ?Item,
|}>,
...
};
class CellRenderer extends React.Component<
CellRendererProps,
CellRendererState,
> {
// $FlowFixMe[missing-local-annot]
state = {
separatorProps: {
highlighted: false,
leadingItem: this.props.item,
},
};
static getDerivedStateFromProps(
props: CellRendererProps,
prevState: CellRendererState,
): ?CellRendererState {
return {
separatorProps: {
...prevState.separatorProps,
leadingItem: props.item,
},
};
}
// TODO: consider factoring separator stuff out of VirtualizedList into FlatList since it's not
// reused by SectionList and we can keep VirtualizedList simpler.
// $FlowFixMe[missing-local-annot]
_separators = {
highlight: () => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators([cellKey, prevCellKey], {
highlighted: true,
});
},
unhighlight: () => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators([cellKey, prevCellKey], {
highlighted: false,
});
},
updateProps: (select: 'leading' | 'trailing', newProps: Object) => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators(
[select === 'leading' ? prevCellKey : cellKey],
newProps,
);
},
};
updateSeparatorProps(newProps: Object) {
this.setState(state => ({
separatorProps: {...state.separatorProps, ...newProps},
}));
}
componentWillUnmount() {
this.props.onUnmount(this.props.cellKey);
}
_onLayout = (nativeEvent: LayoutEvent): void => {
this.props.onCellLayout &&
this.props.onCellLayout(
nativeEvent,
this.props.cellKey,
this.props.index,
);
};
_renderElement(
renderItem: any,
ListItemComponent: any,
item: any,
index: any,
// $FlowFixMe[missing-local-annot]
) {
if (renderItem && ListItemComponent) {
console.warn(
'VirtualizedList: Both ListItemComponent and renderItem props are present. ListItemComponent will take' +
' precedence over renderItem.',
);
}
if (ListItemComponent) {
/* $FlowFixMe[not-a-component] (>=0.108.0 site=react_native_fb) This
* comment suppresses an error found when Flow v0.108 was deployed. To
* see the error, delete this comment and run Flow. */
/* $FlowFixMe[incompatible-type-arg] (>=0.108.0 site=react_native_fb)
* This comment suppresses an error found when Flow v0.108 was deployed.
* To see the error, delete this comment and run Flow. */
return React.createElement(ListItemComponent, {
item,
index,
separators: this._separators,
});
}
if (renderItem) {
return renderItem({
item,
index,
separators: this._separators,
});
}
invariant(
false,
'VirtualizedList: Either ListItemComponent or renderItem props are required but none were found.',
);
}
// $FlowFixMe[missing-local-annot]
render() {
const {
CellRendererComponent,
ItemSeparatorComponent,
ListItemComponent,
debug,
fillRateHelper,
getItemLayout,
horizontal,
item,
index,
inversionStyle,
renderItem,
} = this.props;
const element = this._renderElement(
renderItem,
ListItemComponent,
item,
index,
);
const onLayout =
(getItemLayout && !debug && !fillRateHelper.enabled()) ||
!this.props.onCellLayout
? undefined
: this._onLayout;
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
// called explicitly by `ScrollViewStickyHeader`.
const itemSeparator = React.isValidElement(ItemSeparatorComponent)
? ItemSeparatorComponent
: ItemSeparatorComponent && (
<ItemSeparatorComponent {...this.state.separatorProps} />
);
const cellStyle = inversionStyle
? horizontal
? [styles.rowReverse, inversionStyle]
: [styles.columnReverse, inversionStyle]
: horizontal
? [styles.row, inversionStyle]
: inversionStyle;
const result = !CellRendererComponent ? (
/* $FlowFixMe[incompatible-type-arg] (>=0.89.0 site=react_native_fb) *
This comment suppresses an error found when Flow v0.89 was deployed. *
To see the error, delete this comment and run Flow. */
<View style={cellStyle} onLayout={onLayout}>
{element}
{itemSeparator}
</View>
) : (
<CellRendererComponent
{...this.props}
style={cellStyle}
onLayout={onLayout}>
{element}
{itemSeparator}
</CellRendererComponent>
);
return (
<VirtualizedListCellContextProvider cellKey={this.props.cellKey}>
{result}
</VirtualizedListCellContextProvider>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
verticallyInverted: { verticallyInverted: {
transform: [{scaleY: -1}], transform: [{scaleY: -1}],
@ -1822,15 +1607,6 @@ const styles = StyleSheet.create({
horizontallyInverted: { horizontallyInverted: {
transform: [{scaleX: -1}], transform: [{scaleX: -1}],
}, },
row: {
flexDirection: 'row',
},
rowReverse: {
flexDirection: 'row-reverse',
},
columnReverse: {
flexDirection: 'column-reverse',
},
debug: { debug: {
flex: 1, flex: 1,
}, },

Просмотреть файл

@ -0,0 +1,261 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import * as React from 'react';
import type {ViewStyleProp} from '../StyleSheet/StyleSheet';
import type {FocusEvent, LayoutEvent} from '../Types/CoreEventTypes';
import type {RenderItemType} from './VirtualizedListProps';
import type FillRateHelper from './FillRateHelper';
import {VirtualizedListCellContextProvider} from './VirtualizedListContext.js';
import View from '../Components/View/View';
import StyleSheet from '../StyleSheet/StyleSheet';
import invariant from 'invariant';
export type Props<ItemT> = {
CellRendererComponent?: ?React.ComponentType<any>,
ItemSeparatorComponent: ?React.ComponentType<
any | {highlighted: boolean, leadingItem: ?ItemT},
>,
ListItemComponent?: ?(React.ComponentType<any> | React.Element<any>),
cellKey: string,
debug?: ?boolean,
fillRateHelper: FillRateHelper,
getItemLayout?: (
data: any,
index: number,
) => {
length: number,
offset: number,
index: number,
...
},
horizontal: ?boolean,
index: number,
inversionStyle: ViewStyleProp,
item: ItemT,
onCellLayout: (event: LayoutEvent, cellKey: string, index: number) => void,
onCellFocusCapture?: (event: FocusEvent) => void,
onUnmount: (cellKey: string) => void,
onUpdateSeparators: (
cellKeys: Array<?string>,
props: $Shape<SeparatorProps<ItemT>>,
) => void,
prevCellKey: ?string,
renderItem?: ?RenderItemType<ItemT>,
...
};
type SeparatorProps<ItemT> = $ReadOnly<{|
highlighted: boolean,
leadingItem: ?ItemT,
|}>;
type State<ItemT> = {
separatorProps: SeparatorProps<ItemT>,
...
};
export default class CellRenderer<ItemT> extends React.Component<
Props<ItemT>,
State<ItemT>,
> {
state: State<ItemT> = {
separatorProps: {
highlighted: false,
leadingItem: this.props.item,
},
};
static getDerivedStateFromProps(
props: Props<ItemT>,
prevState: State<ItemT>,
): ?State<ItemT> {
return {
separatorProps: {
...prevState.separatorProps,
leadingItem: props.item,
},
};
}
// TODO: consider factoring separator stuff out of VirtualizedList into FlatList since it's not
// reused by SectionList and we can keep VirtualizedList simpler.
// $FlowFixMe[missing-local-annot]
_separators = {
highlight: () => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators([cellKey, prevCellKey], {
highlighted: true,
});
},
unhighlight: () => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators([cellKey, prevCellKey], {
highlighted: false,
});
},
updateProps: (
select: 'leading' | 'trailing',
newProps: SeparatorProps<ItemT>,
) => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators(
[select === 'leading' ? prevCellKey : cellKey],
newProps,
);
},
};
updateSeparatorProps(newProps: SeparatorProps<ItemT>) {
this.setState(state => ({
separatorProps: {...state.separatorProps, ...newProps},
}));
}
componentWillUnmount() {
this.props.onUnmount(this.props.cellKey);
}
_onLayout = (nativeEvent: LayoutEvent): void => {
this.props.onCellLayout &&
this.props.onCellLayout(
nativeEvent,
this.props.cellKey,
this.props.index,
);
};
_renderElement(
renderItem: ?RenderItemType<ItemT>,
ListItemComponent: any,
item: ItemT,
index: number,
): React.Node {
if (renderItem && ListItemComponent) {
console.warn(
'VirtualizedList: Both ListItemComponent and renderItem props are present. ListItemComponent will take' +
' precedence over renderItem.',
);
}
if (ListItemComponent) {
/* $FlowFixMe[not-a-component] (>=0.108.0 site=react_native_fb) This
* comment suppresses an error found when Flow v0.108 was deployed. To
* see the error, delete this comment and run Flow. */
/* $FlowFixMe[incompatible-type-arg] (>=0.108.0 site=react_native_fb)
* This comment suppresses an error found when Flow v0.108 was deployed.
* To see the error, delete this comment and run Flow. */
return React.createElement(ListItemComponent, {
item,
index,
separators: this._separators,
});
}
if (renderItem) {
return renderItem({
item,
index,
separators: this._separators,
});
}
invariant(
false,
'VirtualizedList: Either ListItemComponent or renderItem props are required but none were found.',
);
}
render(): React.Node {
const {
CellRendererComponent,
ItemSeparatorComponent,
ListItemComponent,
debug,
fillRateHelper,
getItemLayout,
horizontal,
item,
index,
inversionStyle,
onCellFocusCapture,
renderItem,
} = this.props;
const element = this._renderElement(
renderItem,
ListItemComponent,
item,
index,
);
const onLayout =
(getItemLayout && !debug && !fillRateHelper.enabled()) ||
!this.props.onCellLayout
? undefined
: this._onLayout;
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
// called explicitly by `ScrollViewStickyHeader`.
const itemSeparator = React.isValidElement(ItemSeparatorComponent)
? ItemSeparatorComponent
: ItemSeparatorComponent && (
<ItemSeparatorComponent {...this.state.separatorProps} />
);
const cellStyle = inversionStyle
? horizontal
? [styles.rowReverse, inversionStyle]
: [styles.columnReverse, inversionStyle]
: horizontal
? [styles.row, inversionStyle]
: inversionStyle;
const result = !CellRendererComponent ? (
<View
style={cellStyle}
onLayout={onLayout}
onFocusCapture={onCellFocusCapture}
/* $FlowFixMe[incompatible-type-arg] (>=0.89.0 site=react_native_fb) *
This comment suppresses an error found when Flow v0.89 was deployed. *
To see the error, delete this comment and run Flow. */
>
{element}
{itemSeparator}
</View>
) : (
<CellRendererComponent
{...this.props}
style={cellStyle}
onLayout={onLayout}
onFocusCapture={onCellFocusCapture}>
{element}
{itemSeparator}
</CellRendererComponent>
);
return (
<VirtualizedListCellContextProvider cellKey={this.props.cellKey}>
{result}
</VirtualizedListCellContextProvider>
);
}
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
},
rowReverse: {
flexDirection: 'row-reverse',
},
columnReverse: {
flexDirection: 'column-reverse',
},
});

Просмотреть файл

@ -10,11 +10,7 @@
import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type {ScrollResponderType} from '../Components/ScrollView/ScrollView';
import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; import type {ViewStyleProp} from '../StyleSheet/StyleSheet';
import type { import type {LayoutEvent, ScrollEvent} from '../Types/CoreEventTypes';
FocusEvent,
LayoutEvent,
ScrollEvent,
} from '../Types/CoreEventTypes';
import type {ViewToken} from './ViewabilityHelper'; import type {ViewToken} from './ViewabilityHelper';
import type { import type {
@ -40,9 +36,10 @@ import {
import * as React from 'react'; import * as React from 'react';
import {CellRenderMask} from './CellRenderMask'; import {CellRenderMask} from './CellRenderMask';
import ChildListCollection from './ChildListCollection';
import clamp from '../Utilities/clamp'; import clamp from '../Utilities/clamp';
import StateSafePureComponent from './StateSafePureComponent'; import StateSafePureComponent from './StateSafePureComponent';
import ChildListCollection from './ChildListCollection'; import CellRenderer from './VirtualizedListCellRenderer';
const RefreshControl = require('../Components/RefreshControl/RefreshControl'); const RefreshControl = require('../Components/RefreshControl/RefreshControl');
const ScrollView = require('../Components/ScrollView/ScrollView'); const ScrollView = require('../Components/ScrollView/ScrollView');
@ -728,7 +725,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
prevCellKey={prevCellKey} prevCellKey={prevCellKey}
onCellLayout={this._onCellLayout} onCellLayout={this._onCellLayout}
onUpdateSeparators={this._onUpdateSeparators} onUpdateSeparators={this._onUpdateSeparators}
onFocusCapture={e => this._onCellFocusCapture(key)} onCellFocusCapture={e => this._onCellFocusCapture(key)}
onUnmount={this._onCellUnmount} onUnmount={this._onCellUnmount}
ref={ref => { ref={ref => {
this._cellRefs[key] = ref; this._cellRefs[key] = ref;
@ -1080,7 +1077,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
} }
_averageCellLength = 0; _averageCellLength = 0;
_cellRefs: {[string]: null | CellRenderer} = {}; _cellRefs: {[string]: null | CellRenderer<any>} = {};
_fillRateHelper: FillRateHelper; _fillRateHelper: FillRateHelper;
_frames: { _frames: {
[string]: { [string]: {
@ -1843,229 +1840,6 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
} }
} }
type CellRendererProps = {
CellRendererComponent?: ?React.ComponentType<any>,
ItemSeparatorComponent: ?React.ComponentType<
any | {highlighted: boolean, leadingItem: ?Item},
>,
ListItemComponent?: ?(React.ComponentType<any> | React.Element<any>),
cellKey: string,
debug?: ?boolean,
fillRateHelper: FillRateHelper,
getItemLayout?: (
data: any,
index: number,
) => {
length: number,
offset: number,
index: number,
...
},
horizontal: ?boolean,
index: number,
inversionStyle: ViewStyleProp,
item: Item,
// This is extracted by ScrollViewStickyHeader
onCellLayout: (event: Object, cellKey: string, index: number) => void,
onUnmount: (cellKey: string) => void,
onUpdateSeparators: (cellKeys: Array<?string>, props: Object) => void,
prevCellKey: ?string,
onFocusCapture: (event: FocusEvent) => void,
renderItem?: ?RenderItemType<Item>,
...
};
type CellRendererState = {
separatorProps: $ReadOnly<{|
highlighted: boolean,
leadingItem: ?Item,
|}>,
...
};
class CellRenderer extends React.Component<
CellRendererProps,
CellRendererState,
> {
// $FlowFixMe[missing-local-annot]
state = {
separatorProps: {
highlighted: false,
leadingItem: this.props.item,
},
};
static getDerivedStateFromProps(
props: CellRendererProps,
prevState: CellRendererState,
): ?CellRendererState {
return {
separatorProps: {
...prevState.separatorProps,
leadingItem: props.item,
},
};
}
// TODO: consider factoring separator stuff out of VirtualizedList into FlatList since it's not
// reused by SectionList and we can keep VirtualizedList simpler.
// $FlowFixMe[missing-local-annot]
_separators = {
highlight: () => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators([cellKey, prevCellKey], {
highlighted: true,
});
},
unhighlight: () => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators([cellKey, prevCellKey], {
highlighted: false,
});
},
updateProps: (select: 'leading' | 'trailing', newProps: Object) => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators(
[select === 'leading' ? prevCellKey : cellKey],
newProps,
);
},
};
updateSeparatorProps(newProps: Object) {
this.setState(state => ({
separatorProps: {...state.separatorProps, ...newProps},
}));
}
componentWillUnmount() {
this.props.onUnmount(this.props.cellKey);
}
_onLayout = (nativeEvent: LayoutEvent): void => {
this.props.onCellLayout &&
this.props.onCellLayout(
nativeEvent,
this.props.cellKey,
this.props.index,
);
};
_renderElement(
renderItem: any,
ListItemComponent: any,
item: any,
index: any,
// $FlowFixMe[missing-local-annot]
) {
if (renderItem && ListItemComponent) {
console.warn(
'VirtualizedList: Both ListItemComponent and renderItem props are present. ListItemComponent will take' +
' precedence over renderItem.',
);
}
if (ListItemComponent) {
/* $FlowFixMe[not-a-component] (>=0.108.0 site=react_native_fb) This
* comment suppresses an error found when Flow v0.108 was deployed. To
* see the error, delete this comment and run Flow. */
/* $FlowFixMe[incompatible-type-arg] (>=0.108.0 site=react_native_fb)
* This comment suppresses an error found when Flow v0.108 was deployed.
* To see the error, delete this comment and run Flow. */
return React.createElement(ListItemComponent, {
item,
index,
separators: this._separators,
});
}
if (renderItem) {
return renderItem({
item,
index,
separators: this._separators,
});
}
invariant(
false,
'VirtualizedList: Either ListItemComponent or renderItem props are required but none were found.',
);
}
// $FlowFixMe[missing-local-annot]
render() {
const {
CellRendererComponent,
ItemSeparatorComponent,
ListItemComponent,
debug,
fillRateHelper,
getItemLayout,
horizontal,
item,
index,
inversionStyle,
onFocusCapture,
renderItem,
} = this.props;
const element = this._renderElement(
renderItem,
ListItemComponent,
item,
index,
);
const onLayout =
(getItemLayout && !debug && !fillRateHelper.enabled()) ||
!this.props.onCellLayout
? undefined
: this._onLayout;
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
// called explicitly by `ScrollViewStickyHeader`.
const itemSeparator = React.isValidElement(ItemSeparatorComponent)
? ItemSeparatorComponent
: ItemSeparatorComponent && (
<ItemSeparatorComponent {...this.state.separatorProps} />
);
const cellStyle = inversionStyle
? horizontal
? [styles.rowReverse, inversionStyle]
: [styles.columnReverse, inversionStyle]
: horizontal
? [styles.row, inversionStyle]
: inversionStyle;
const result = !CellRendererComponent ? (
<View
style={cellStyle}
onLayout={onLayout}
onFocusCapture={onFocusCapture}
/* $FlowFixMe[incompatible-type-arg] (>=0.89.0 site=react_native_fb) *
This comment suppresses an error found when Flow v0.89 was deployed. *
To see the error, delete this comment and run Flow. */
>
{element}
{itemSeparator}
</View>
) : (
<CellRendererComponent
{...this.props}
style={cellStyle}
onLayout={onLayout}
onFocusCapture={onFocusCapture}>
{element}
{itemSeparator}
</CellRendererComponent>
);
return (
<VirtualizedListCellContextProvider cellKey={this.props.cellKey}>
{result}
</VirtualizedListCellContextProvider>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
verticallyInverted: { verticallyInverted: {
transform: [{scaleY: -1}], transform: [{scaleY: -1}],
@ -2073,15 +1847,6 @@ const styles = StyleSheet.create({
horizontallyInverted: { horizontallyInverted: {
transform: [{scaleX: -1}], transform: [{scaleX: -1}],
}, },
row: {
flexDirection: 'row',
},
rowReverse: {
flexDirection: 'row-reverse',
},
columnReverse: {
flexDirection: 'column-reverse',
},
debug: { debug: {
flex: 1, flex: 1,
}, },