mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-01-12 22:50:10 +08:00
- Externalise and handle any sort of data blob (#20787)
Summary: Fixes #20770 Pull Request resolved: https://github.com/facebook/react-native/pull/20787 Reviewed By: sahrens Differential Revision: D9485598 Pulled By: cpojer fbshipit-source-id: edddebf6b5e1ca396ab1a519baf019c1e5188d44
This commit is contained in:
committed by
Facebook Github Bot
parent
f81d77c102
commit
1946aee3d9
@@ -15,38 +15,14 @@ const ScrollView = require('ScrollView');
|
||||
const VirtualizedSectionList = require('VirtualizedSectionList');
|
||||
|
||||
import type {ViewToken} from 'ViewabilityHelper';
|
||||
import type {Props as VirtualizedSectionListProps} from 'VirtualizedSectionList';
|
||||
import type {
|
||||
SectionBase as _SectionBase,
|
||||
Props as VirtualizedSectionListProps,
|
||||
} from 'VirtualizedSectionList';
|
||||
|
||||
type Item = any;
|
||||
|
||||
export type SectionBase<SectionItemT> = {
|
||||
/**
|
||||
* The data for rendering items in this section.
|
||||
*/
|
||||
data: $ReadOnlyArray<SectionItemT>,
|
||||
/**
|
||||
* Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections,
|
||||
* the array index will be used by default.
|
||||
*/
|
||||
key?: string,
|
||||
|
||||
// Optional props will override list-wide props just for this section.
|
||||
renderItem?: ?(info: {
|
||||
item: SectionItemT,
|
||||
index: number,
|
||||
section: SectionBase<SectionItemT>,
|
||||
separators: {
|
||||
highlight: () => void,
|
||||
unhighlight: () => void,
|
||||
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
|
||||
},
|
||||
}) => ?React.Element<any>,
|
||||
ItemSeparatorComponent?: ?React.ComponentType<any>,
|
||||
keyExtractor?: (item: SectionItemT) => string,
|
||||
|
||||
// TODO: support more optional/override props
|
||||
// onViewableItemsChanged?: ...
|
||||
};
|
||||
export type SectionBase<SectionItemT> = _SectionBase<SectionItemT>;
|
||||
|
||||
type RequiredProps<SectionT: SectionBase<any>> = {
|
||||
/**
|
||||
@@ -326,10 +302,17 @@ class SectionList<SectionT: SectionBase<any>> extends React.PureComponent<
|
||||
}
|
||||
|
||||
render() {
|
||||
/* $FlowFixMe(>=0.66.0 site=react_native_fb) This comment suppresses an
|
||||
* error found when Flow v0.66 was deployed. To see the error delete this
|
||||
* comment and run Flow. */
|
||||
return <VirtualizedSectionList {...this.props} ref={this._captureRef} />;
|
||||
return (
|
||||
/* $FlowFixMe(>=0.66.0 site=react_native_fb) This comment suppresses an
|
||||
* error found when Flow v0.66 was deployed. To see the error delete this
|
||||
* comment and run Flow. */
|
||||
<VirtualizedSectionList
|
||||
{...this.props}
|
||||
ref={this._captureRef}
|
||||
getItemCount={items => items.length}
|
||||
getItem={(items, index) => items[index]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_wrapperListRef: ?React.ElementRef<typeof VirtualizedSectionList>;
|
||||
|
||||
@@ -20,18 +20,23 @@ import type {ViewToken} from 'ViewabilityHelper';
|
||||
import type {Props as VirtualizedListProps} from 'VirtualizedList';
|
||||
|
||||
type Item = any;
|
||||
type SectionItem = any;
|
||||
|
||||
type SectionBase = {
|
||||
// Must be provided directly on each section.
|
||||
data: $ReadOnlyArray<SectionItem>,
|
||||
export type SectionBase<SectionItemT> = {
|
||||
/**
|
||||
* The data for rendering items in this section.
|
||||
*/
|
||||
data: $ReadOnlyArray<SectionItemT>,
|
||||
/**
|
||||
* Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections,
|
||||
* the array index will be used by default.
|
||||
*/
|
||||
key?: string,
|
||||
|
||||
// Optional props will override list-wide props just for this section.
|
||||
renderItem?: ?({
|
||||
item: SectionItem,
|
||||
renderItem?: ?(info: {
|
||||
item: SectionItemT,
|
||||
index: number,
|
||||
section: SectionBase,
|
||||
section: SectionBase<SectionItemT>,
|
||||
separators: {
|
||||
highlight: () => void,
|
||||
unhighlight: () => void,
|
||||
@@ -39,19 +44,14 @@ type SectionBase = {
|
||||
},
|
||||
}) => ?React.Element<any>,
|
||||
ItemSeparatorComponent?: ?React.ComponentType<any>,
|
||||
keyExtractor?: (item: SectionItem, index: ?number) => string,
|
||||
|
||||
// TODO: support more optional/override props
|
||||
// FooterComponent?: ?ReactClass<any>,
|
||||
// HeaderComponent?: ?ReactClass<any>,
|
||||
// onViewableItemsChanged?: ({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
||||
keyExtractor?: (item: SectionItemT, index?: ?number) => string,
|
||||
};
|
||||
|
||||
type RequiredProps<SectionT: SectionBase> = {
|
||||
type RequiredProps<SectionT: SectionBase<any>> = {
|
||||
sections: $ReadOnlyArray<SectionT>,
|
||||
};
|
||||
|
||||
type OptionalProps<SectionT: SectionBase> = {
|
||||
type OptionalProps<SectionT: SectionBase<any>> = {
|
||||
/**
|
||||
* Rendered after the last item in the last section.
|
||||
*/
|
||||
@@ -131,10 +131,9 @@ type State = {childProps: VirtualizedListProps};
|
||||
* hood. The only operation that might not scale well is concatting the data arrays of all the
|
||||
* sections when new props are received, which should be plenty fast for up to ~10,000 items.
|
||||
*/
|
||||
class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
|
||||
Props<SectionT>,
|
||||
State,
|
||||
> {
|
||||
class VirtualizedSectionList<
|
||||
SectionT: SectionBase<any>,
|
||||
> extends React.PureComponent<Props<SectionT>, State> {
|
||||
static defaultProps: DefaultProps = {
|
||||
...VirtualizedList.defaultProps,
|
||||
data: [],
|
||||
@@ -147,8 +146,8 @@ class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
|
||||
viewPosition?: number,
|
||||
}) {
|
||||
let index = Platform.OS === 'ios' ? params.itemIndex : params.itemIndex + 1;
|
||||
for (let ii = 0; ii < params.sectionIndex; ii++) {
|
||||
index += this.props.sections[ii].data.length + 2;
|
||||
for (let i = 0; i < params.sectionIndex; i++) {
|
||||
index += this.props.getItemCount(this.props.sections[i].data) + 2;
|
||||
}
|
||||
const toIndexParams = {
|
||||
...params,
|
||||
@@ -173,10 +172,12 @@ class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
|
||||
_computeState(props: Props<SectionT>): State {
|
||||
const offset = props.ListHeaderComponent ? 1 : 0;
|
||||
const stickyHeaderIndices = [];
|
||||
const itemCount = props.sections.reduce((v, section) => {
|
||||
stickyHeaderIndices.push(v + offset);
|
||||
return v + section.data.length + 2; // Add two for the section header and footer.
|
||||
}, 0);
|
||||
const itemCount = props.sections
|
||||
? props.sections.reduce((v, section) => {
|
||||
stickyHeaderIndices.push(v + offset);
|
||||
return v + props.getItemCount(section.data) + 2; // Add two for the section header and footer.
|
||||
}, 0)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
childProps: {
|
||||
@@ -185,7 +186,8 @@ class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
|
||||
ItemSeparatorComponent: undefined, // Rendered with renderItem
|
||||
data: props.sections,
|
||||
getItemCount: () => itemCount,
|
||||
getItem,
|
||||
// $FlowFixMe
|
||||
getItem: (sections, index) => getItem(props, sections, index),
|
||||
keyExtractor: this._keyExtractor,
|
||||
onViewableItemsChanged: props.onViewableItemsChanged
|
||||
? this._onViewableItemsChanged
|
||||
@@ -221,42 +223,41 @@ class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
|
||||
trailingSection?: ?SectionT,
|
||||
} {
|
||||
let itemIndex = index;
|
||||
const {sections} = this.props;
|
||||
for (let ii = 0; ii < sections.length; ii++) {
|
||||
const section = sections[ii];
|
||||
const key = section.key || String(ii);
|
||||
const {getItem, getItemCount, keyExtractor, sections} = this.props;
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
const sectionData = section.data;
|
||||
const key = section.key || String(i);
|
||||
itemIndex -= 1; // The section adds an item for the header
|
||||
if (itemIndex >= section.data.length + 1) {
|
||||
itemIndex -= section.data.length + 1; // The section adds an item for the footer.
|
||||
if (itemIndex >= getItemCount(sectionData) + 1) {
|
||||
itemIndex -= getItemCount(sectionData) + 1; // The section adds an item for the footer.
|
||||
} else if (itemIndex === -1) {
|
||||
return {
|
||||
section,
|
||||
key: key + ':header',
|
||||
index: null,
|
||||
header: true,
|
||||
trailingSection: sections[ii + 1],
|
||||
trailingSection: sections[i + 1],
|
||||
};
|
||||
} else if (itemIndex === section.data.length) {
|
||||
} else if (itemIndex === getItemCount(sectionData)) {
|
||||
return {
|
||||
section,
|
||||
key: key + ':footer',
|
||||
index: null,
|
||||
header: false,
|
||||
trailingSection: sections[ii + 1],
|
||||
trailingSection: sections[i + 1],
|
||||
};
|
||||
} else {
|
||||
const keyExtractor = section.keyExtractor || this.props.keyExtractor;
|
||||
const extractor = section.keyExtractor || keyExtractor;
|
||||
return {
|
||||
section,
|
||||
key: key + ':' + keyExtractor(section.data[itemIndex], itemIndex),
|
||||
key:
|
||||
key + ':' + extractor(getItem(sectionData, itemIndex), itemIndex),
|
||||
index: itemIndex,
|
||||
leadingItem: section.data[itemIndex - 1],
|
||||
leadingSection: sections[ii - 1],
|
||||
trailingItem:
|
||||
section.data.length > itemIndex + 1
|
||||
? section.data[itemIndex + 1]
|
||||
: undefined,
|
||||
trailingSection: sections[ii + 1],
|
||||
leadingItem: getItem(sectionData, itemIndex - 1),
|
||||
leadingSection: sections[i - 1],
|
||||
trailingItem: getItem(sectionData, itemIndex + 1),
|
||||
trailingSection: sections[i + 1],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -358,7 +359,8 @@ class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
|
||||
info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent;
|
||||
const {SectionSeparatorComponent} = this.props;
|
||||
const isLastItemInList = index === this.state.childProps.getItemCount() - 1;
|
||||
const isLastItemInSection = info.index === info.section.data.length - 1;
|
||||
const isLastItemInSection =
|
||||
info.index === this.props.getItemCount(info.section.data) - 1;
|
||||
if (SectionSeparatorComponent && isLastItemInSection) {
|
||||
return SectionSeparatorComponent;
|
||||
}
|
||||
@@ -523,22 +525,29 @@ class ItemWithSeparator extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
function getItem(sections: ?$ReadOnlyArray<Item>, index: number): ?Item {
|
||||
function getItem(
|
||||
props: Props<SectionBase<any>>,
|
||||
sections: ?$ReadOnlyArray<Item>,
|
||||
index: number,
|
||||
): ?Item {
|
||||
if (!sections) {
|
||||
return null;
|
||||
}
|
||||
let itemIdx = index - 1;
|
||||
for (let ii = 0; ii < sections.length; ii++) {
|
||||
if (itemIdx === -1 || itemIdx === sections[ii].data.length) {
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
const sectionData = section.data;
|
||||
const itemCount = props.getItemCount(sectionData);
|
||||
if (itemIdx === -1 || itemIdx === itemCount) {
|
||||
// We intend for there to be overflow by one on both ends of the list.
|
||||
// This will be for headers and footers. When returning a header or footer
|
||||
// item the section itself is the item.
|
||||
return sections[ii];
|
||||
} else if (itemIdx < sections[ii].data.length) {
|
||||
return section;
|
||||
} else if (itemIdx < itemCount) {
|
||||
// If we are in the bounds of the list's data then return the item.
|
||||
return sections[ii].data[itemIdx];
|
||||
return props.getItem(sectionData, itemIdx);
|
||||
} else {
|
||||
itemIdx -= sections[ii].data.length + 2; // Add two for the header and footer
|
||||
itemIdx -= itemCount + 2; // Add two for the header and footer
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
164
Libraries/Lists/__tests__/VirtualizedSectionList-test.js
Normal file
164
Libraries/Lists/__tests__/VirtualizedSectionList-test.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*
|
||||
* @format
|
||||
* @emails oncall+react_native
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const ReactTestRenderer = require('react-test-renderer');
|
||||
|
||||
const VirtualizedSectionList = require('VirtualizedSectionList');
|
||||
|
||||
describe('VirtualizedSectionList', () => {
|
||||
it('renders simple list', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedSectionList
|
||||
sections={[
|
||||
{title: 's1', data: [{key: 'i1'}, {key: 'i2'}, {key: 'i3'}]},
|
||||
]}
|
||||
renderItem={({item}) => <item value={item.key} />}
|
||||
getItem={(data, key) => data[key]}
|
||||
getItemCount={data => data.length}
|
||||
/>,
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders empty list', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedSectionList
|
||||
sections={[]}
|
||||
renderItem={({item}) => <item value={item.key} />}
|
||||
getItem={(data, key) => data[key]}
|
||||
getItemCount={data => data.length}
|
||||
/>,
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders null list', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedSectionList
|
||||
sections={undefined}
|
||||
renderItem={({item}) => <item value={item.key} />}
|
||||
getItem={(data, key) => data[key]}
|
||||
getItemCount={data => 0}
|
||||
/>,
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders empty list with empty component', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedSectionList
|
||||
sections={[]}
|
||||
ListEmptyComponent={() => <empty />}
|
||||
ListFooterComponent={() => <footer />}
|
||||
ListHeaderComponent={() => <header />}
|
||||
getItem={(data, key) => data[key]}
|
||||
getItemCount={data => data.length}
|
||||
renderItem={({item}) => <item value={item.key} />}
|
||||
/>,
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders list with empty component', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedSectionList
|
||||
sections={[{title: 's1', data: [{key: 'hello'}]}]}
|
||||
ListEmptyComponent={() => <empty />}
|
||||
getItem={(data, key) => data[key]}
|
||||
getItemCount={data => data.length}
|
||||
renderItem={({item}) => <item value={item.key} />}
|
||||
/>,
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders all the bells and whistles', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedSectionList
|
||||
ItemSeparatorComponent={() => <separator />}
|
||||
ListEmptyComponent={() => <empty />}
|
||||
ListFooterComponent={() => <footer />}
|
||||
ListHeaderComponent={() => <header />}
|
||||
sections={[
|
||||
{
|
||||
title: 's1',
|
||||
data: new Array(5).fill().map((_, ii) => ({id: String(ii)})),
|
||||
},
|
||||
]}
|
||||
getItem={(data, key) => data[key]}
|
||||
getItemCount={data => data.length}
|
||||
getItemLayout={({index}) => ({length: 50, offset: index * 50})}
|
||||
inverted={true}
|
||||
keyExtractor={(item, index) => item.id}
|
||||
onRefresh={jest.fn()}
|
||||
refreshing={false}
|
||||
renderItem={({item}) => <item value={item.id} />}
|
||||
/>,
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('handles separators correctly', () => {
|
||||
const infos = [];
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedSectionList
|
||||
ItemSeparatorComponent={props => <separator {...props} />}
|
||||
sections={[
|
||||
{title: 's0', data: [{key: 'i0'}, {key: 'i1'}, {key: 'i2'}]},
|
||||
]}
|
||||
renderItem={info => {
|
||||
infos.push(info);
|
||||
return <item title={info.item.key} />;
|
||||
}}
|
||||
getItem={(data, key) => data[key]}
|
||||
getItemCount={data => data.length}
|
||||
/>,
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
infos[1].separators.highlight();
|
||||
expect(component).toMatchSnapshot();
|
||||
infos[2].separators.updateProps('leading', {press: true});
|
||||
expect(component).toMatchSnapshot();
|
||||
infos[1].separators.unhighlight();
|
||||
});
|
||||
|
||||
it('handles nested lists', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedSectionList
|
||||
sections={[{title: 'outer', data: [{key: 'outer0'}, {key: 'outer1'}]}]}
|
||||
renderItem={outerInfo => (
|
||||
<VirtualizedSectionList
|
||||
sections={[
|
||||
{
|
||||
title: 'inner',
|
||||
data: [
|
||||
{key: outerInfo.item.key + ':inner0'},
|
||||
{key: outerInfo.item.key + ':inner1'},
|
||||
],
|
||||
},
|
||||
]}
|
||||
horizontal={outerInfo.item.key === 'outer1'}
|
||||
renderItem={innerInfo => {
|
||||
return <item title={innerInfo.item.key} />;
|
||||
}}
|
||||
getItem={(data, key) => data[key]}
|
||||
getItemCount={data => data.length}
|
||||
/>
|
||||
)}
|
||||
getItem={(data, key) => data[key]}
|
||||
getItemCount={data => data.length}
|
||||
/>,
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -176,6 +176,9 @@ module.exports = {
|
||||
get VirtualizedList() {
|
||||
return require('VirtualizedList');
|
||||
},
|
||||
get VirtualizedSectionList() {
|
||||
return require('VirtualizedSectionList');
|
||||
},
|
||||
|
||||
// APIs
|
||||
get ActionSheetIOS() {
|
||||
|
||||
Reference in New Issue
Block a user