mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-05-26 10:45:32 +08:00
Move new components out of Experimental directory
Summary: I think these are sufficiently baked. Also beef up comments. Reviewed By: yungsters Differential Revision: D4632604 fbshipit-source-id: 64ae6b240a05d62e418099f7403e1781f9b4717c
This commit is contained in:
committed by
Facebook Github Bot
parent
5facc23799
commit
7b35eb3fdb
376
Libraries/CustomComponents/Lists/FlatList.js
Normal file
376
Libraries/CustomComponents/Lists/FlatList.js
Normal file
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
|
||||
* all intellectual property and other proprietary rights, in and to the React
|
||||
* Native CustomComponents software (the "Software"). Subject to your
|
||||
* compliance with these terms, you are hereby granted a non-exclusive,
|
||||
* worldwide, royalty-free copyright license to (1) use and copy the Software;
|
||||
* and (2) reproduce and distribute the Software as part of your own software
|
||||
* ("Your Software"). Facebook reserves all rights not expressly granted to
|
||||
* you in this license agreement.
|
||||
*
|
||||
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
|
||||
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
|
||||
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @providesModule FlatList
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const MetroListView = require('MetroListView'); // Used as a fallback legacy option
|
||||
const React = require('React');
|
||||
const View = require('View');
|
||||
const VirtualizedList = require('VirtualizedList');
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
import type {StyleObj} from 'StyleSheetTypes';
|
||||
import type {ViewabilityConfig, ViewToken} from 'ViewabilityHelper';
|
||||
import type {Props as VirtualizedListProps} from 'VirtualizedList';
|
||||
|
||||
type RequiredProps<ItemT> = {
|
||||
/**
|
||||
* Takes an item from `data` and renders it into the list. Typicaly usage:
|
||||
*
|
||||
* _renderItem = ({item}) => (
|
||||
* <TouchableOpacity onPress={() => this._onPress(item)}>
|
||||
* <Text>{item.title}}</Text>
|
||||
* <TouchableOpacity/>
|
||||
* );
|
||||
* ...
|
||||
* <FlatList data={[{title: 'Title Text', key: 'item1'}]} renderItem={this._renderItem} />
|
||||
*
|
||||
* Provides additional metadata like `index` if you need it.
|
||||
*/
|
||||
renderItem: ({item: ItemT, index: number}) => ?React.Element<*>,
|
||||
/**
|
||||
* For simplicity, data is just a plain array. If you want to use something else, like an
|
||||
* immutable list, use the underlying `VirtualizedList` directly.
|
||||
*/
|
||||
data: ?Array<ItemT>,
|
||||
};
|
||||
type OptionalProps<ItemT> = {
|
||||
/**
|
||||
* Rendered at the bottom of all the items.
|
||||
*/
|
||||
FooterComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* Rendered at the top of all the items.
|
||||
*/
|
||||
HeaderComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* Rendered in between each item, but not at the top or bottom.
|
||||
*/
|
||||
SeparatorComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* getItemLayout is an optional optimizations that let us skip measurement of dynamic content if
|
||||
* you know the height of items a priori. getItemLayout is the most efficient, and is easy to use
|
||||
* if you have fixed height items, for example:
|
||||
*
|
||||
* getItemLayout={(data, index) => ({length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index})}
|
||||
*
|
||||
* Remember to include separator length (height or width) in your offset calculation if you
|
||||
* specify `SeparatorComponent`.
|
||||
*/
|
||||
getItemLayout?: (data: ?Array<ItemT>, index: number) =>
|
||||
{length: number, offset: number, index: number},
|
||||
/**
|
||||
* If true, renders items next to each other horizontally instead of stacked vertically.
|
||||
*/
|
||||
horizontal?: ?boolean,
|
||||
/**
|
||||
* 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
|
||||
* falls back to using the index, like react does.
|
||||
*/
|
||||
keyExtractor: (item: ItemT, index: number) => string,
|
||||
/**
|
||||
* Multiple columns can only be rendered with horizontal={false} and will zig-zag like a flexWrap
|
||||
* layout. Items should all be the same height - masonry layouts are not supported.
|
||||
*/
|
||||
numColumns: number,
|
||||
/**
|
||||
* Called once when the scroll position gets within onEndReachedThreshold of the rendered content.
|
||||
*/
|
||||
onEndReached?: ?({distanceFromEnd: number}) => void,
|
||||
onEndReachedThreshold?: ?number,
|
||||
/**
|
||||
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
|
||||
* sure to also set the `refreshing` prop correctly.
|
||||
*/
|
||||
onRefresh?: ?Function,
|
||||
/**
|
||||
* Called when the viewability of rows changes, as defined by the
|
||||
* `viewablePercentThreshold` prop.
|
||||
*/
|
||||
onViewableItemsChanged?: ?({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
||||
legacyImplementation?: ?boolean,
|
||||
/**
|
||||
* Set this true while waiting for new data from a refresh.
|
||||
*/
|
||||
refreshing?: ?boolean,
|
||||
/**
|
||||
* Optional custom style for multi-item rows generated when numColumns > 1
|
||||
*/
|
||||
columnWrapperStyle?: StyleObj,
|
||||
/**
|
||||
* Optional optimization to minimize re-rendering items.
|
||||
*/
|
||||
shouldItemUpdate: (
|
||||
prevInfo: {item: ItemT, index: number},
|
||||
nextInfo: {item: ItemT, index: number}
|
||||
) => boolean,
|
||||
/**
|
||||
* See ViewabilityHelper for flow type and further documentation.
|
||||
*/
|
||||
viewabilityConfig?: ViewabilityConfig,
|
||||
};
|
||||
type Props<ItemT> = RequiredProps<ItemT> & OptionalProps<ItemT> & VirtualizedListProps;
|
||||
|
||||
const defaultProps = {
|
||||
...VirtualizedList.defaultProps,
|
||||
getItem: undefined,
|
||||
getItemCount: undefined,
|
||||
numColumns: 1,
|
||||
};
|
||||
type DefaultProps = typeof defaultProps;
|
||||
|
||||
/**
|
||||
* A performant interface for rendering simple, flat lists, supporting the most handy features:
|
||||
*
|
||||
* - Fully cross-platform.
|
||||
* - Optional horizontal mode.
|
||||
* - Viewability callbacks.
|
||||
* - Footer support.
|
||||
* - Separator support.
|
||||
* - Pull to Refresh
|
||||
*
|
||||
* If you need sticky section header support, use ListView for now.
|
||||
*
|
||||
* Minimal Example:
|
||||
*
|
||||
* <FlatList
|
||||
* data={[{key: 'a'}, {key: 'b'}]}
|
||||
* renderItem={({item}) => <Text>{item.key}</Text>}
|
||||
* />
|
||||
*
|
||||
* Some notes for all of the `VirtualizedList` based components:
|
||||
* - Internal state is not preserved when content scrolls out of the render window. Make sure all
|
||||
* your data is captured in the item data or external stores like Flux, Redux, or Relay.
|
||||
* - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously
|
||||
* offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see
|
||||
* blank content. This is a tradeoff that can be adjusted to suit the needs of each application,
|
||||
* and we are working on improving it behind the scenes.
|
||||
* - By default, the list looks for a `key` prop on each item and uses that for the React key.
|
||||
* Alternatively, you can provide a custom keyExtractor prop.
|
||||
*/
|
||||
class FlatList<ItemT> extends React.PureComponent<DefaultProps, Props<ItemT>, void> {
|
||||
static defaultProps: DefaultProps = defaultProps;
|
||||
props: Props<ItemT>;
|
||||
/**
|
||||
* Scrolls to the end of the content. May be janky without getItemLayout prop.
|
||||
*/
|
||||
scrollToEnd(params?: ?{animated?: ?boolean}) {
|
||||
this._listRef.scrollToEnd(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls to the item at a the specified index such that it is positioned in the viewable area
|
||||
* such that viewPosition 0 places it at the top, 1 at the bottom, and 0.5 centered in the middle.
|
||||
*
|
||||
* May be janky without getItemLayout prop.
|
||||
*/
|
||||
scrollToIndex(params: {animated?: ?boolean, index: number, viewPosition?: number}) {
|
||||
this._listRef.scrollToIndex(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires linear scan through data - use scrollToIndex instead if possible. May be janky without
|
||||
* `getItemLayout` prop.
|
||||
*/
|
||||
scrollToItem(params: {animated?: ?boolean, item: ItemT, viewPosition?: number}) {
|
||||
this._listRef.scrollToItem(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to a specific content pixel offset, like a normal ScrollView.
|
||||
*/
|
||||
scrollToOffset(params: {animated?: ?boolean, offset: number}) {
|
||||
this._listRef.scrollToOffset(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the list an interaction has occured, which should trigger viewability calculations, e.g.
|
||||
* if waitForInteractions is true and the user has not scrolled. This is typically called by taps
|
||||
* on items or by navigation actions.
|
||||
*/
|
||||
recordInteraction() {
|
||||
this._listRef.recordInteraction();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._checkProps(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props<ItemT>) {
|
||||
this._checkProps(nextProps);
|
||||
}
|
||||
|
||||
_hasWarnedLegacy = false;
|
||||
_listRef: VirtualizedList;
|
||||
|
||||
_captureRef = (ref) => { this._listRef = ref; };
|
||||
|
||||
_checkProps(props: Props<ItemT>) {
|
||||
const {
|
||||
getItem,
|
||||
getItemCount,
|
||||
horizontal,
|
||||
legacyImplementation,
|
||||
numColumns,
|
||||
columnWrapperStyle,
|
||||
} = props;
|
||||
invariant(!getItem && !getItemCount, 'FlatList does not support custom data formats.');
|
||||
if (numColumns > 1) {
|
||||
invariant(!horizontal, 'numColumns does not support horizontal.');
|
||||
} else {
|
||||
invariant(!columnWrapperStyle, 'columnWrapperStyle not supported for single column lists');
|
||||
}
|
||||
if (legacyImplementation) {
|
||||
invariant(numColumns === 1, 'Legacy list does not support multiple columns.');
|
||||
// Warning: may not have full feature parity and is meant more for debugging and performance
|
||||
// comparison.
|
||||
if (!this._hasWarnedLegacy) {
|
||||
console.warn(
|
||||
'FlatList: Using legacyImplementation - some features not supported and performance ' +
|
||||
'may suffer'
|
||||
);
|
||||
this._hasWarnedLegacy = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getItem = (data: Array<ItemT>, index: number): ItemT | Array<ItemT> => {
|
||||
const {numColumns} = this.props;
|
||||
if (numColumns > 1) {
|
||||
const ret = [];
|
||||
for (let kk = 0; kk < numColumns; kk++) {
|
||||
const item = data[index * numColumns + kk];
|
||||
item && ret.push(item);
|
||||
}
|
||||
return ret;
|
||||
} else {
|
||||
return data[index];
|
||||
}
|
||||
};
|
||||
|
||||
_getItemCount = (data?: ?Array<ItemT>): number => {
|
||||
return data ? Math.ceil(data.length / this.props.numColumns) : 0;
|
||||
};
|
||||
|
||||
_keyExtractor = (items: ItemT | Array<ItemT>, index: number): string => {
|
||||
const {keyExtractor, numColumns} = this.props;
|
||||
if (numColumns > 1) {
|
||||
invariant(
|
||||
Array.isArray(items),
|
||||
'FlatList: Encountered internal consistency error, expected each item to consist of an ' +
|
||||
'array with 1-%s columns; instead, received a single item.',
|
||||
numColumns,
|
||||
);
|
||||
return items.map((it, kk) => keyExtractor(it, index * numColumns + kk)).join(':');
|
||||
} else {
|
||||
return keyExtractor(items, index);
|
||||
}
|
||||
};
|
||||
|
||||
_pushMultiColumnViewable(arr: Array<ViewToken>, v: ViewToken): void {
|
||||
const {numColumns, keyExtractor} = this.props;
|
||||
v.item.forEach((item, ii) => {
|
||||
invariant(v.index != null, 'Missing index!');
|
||||
const index = v.index * numColumns + ii;
|
||||
arr.push({...v, item, key: keyExtractor(item, index), index});
|
||||
});
|
||||
}
|
||||
_onViewableItemsChanged = (info) => {
|
||||
const {numColumns, onViewableItemsChanged} = this.props;
|
||||
if (!onViewableItemsChanged) {
|
||||
return;
|
||||
}
|
||||
if (numColumns > 1) {
|
||||
const changed = [];
|
||||
const viewableItems = [];
|
||||
info.viewableItems.forEach((v) => this._pushMultiColumnViewable(viewableItems, v));
|
||||
info.changed.forEach((v) => this._pushMultiColumnViewable(changed, v));
|
||||
onViewableItemsChanged({viewableItems, changed});
|
||||
} else {
|
||||
onViewableItemsChanged(info);
|
||||
}
|
||||
};
|
||||
|
||||
_renderItem = (info: {item: ItemT | Array<ItemT>, index: number}) => {
|
||||
const {renderItem, numColumns, columnWrapperStyle} = this.props;
|
||||
if (numColumns > 1) {
|
||||
const {item, index} = info;
|
||||
invariant(Array.isArray(item), 'Expected array of items with numColumns > 1');
|
||||
return (
|
||||
<View style={[{flexDirection: 'row'}, columnWrapperStyle]}>
|
||||
{item.map((it, kk) => {
|
||||
const element = renderItem({item: it, index: index * numColumns + kk});
|
||||
return element && React.cloneElement(element, {key: kk});
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return renderItem(info);
|
||||
}
|
||||
};
|
||||
|
||||
_shouldItemUpdate = (prev, next) => {
|
||||
const {numColumns, shouldItemUpdate} = this.props;
|
||||
if (numColumns > 1) {
|
||||
return prev.item.length !== next.item.length ||
|
||||
prev.item.some((prevItem, ii) => shouldItemUpdate(
|
||||
{item: prevItem, index: prev.index + ii},
|
||||
{item: next.item[ii], index: next.index + ii},
|
||||
));
|
||||
} else {
|
||||
return shouldItemUpdate(prev, next);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.legacyImplementation) {
|
||||
return <MetroListView {...this.props} items={this.props.data} ref={this._captureRef} />;
|
||||
} else {
|
||||
return (
|
||||
<VirtualizedList
|
||||
{...this.props}
|
||||
renderItem={this._renderItem}
|
||||
getItem={this._getItem}
|
||||
getItemCount={this._getItemCount}
|
||||
keyExtractor={this._keyExtractor}
|
||||
ref={this._captureRef}
|
||||
shouldItemUpdate={this._shouldItemUpdate}
|
||||
onViewableItemsChanged={this.props.onViewableItemsChanged && this._onViewableItemsChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FlatList;
|
||||
176
Libraries/CustomComponents/Lists/MetroListView.js
Normal file
176
Libraries/CustomComponents/Lists/MetroListView.js
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
|
||||
* all intellectual property and other proprietary rights, in and to the React
|
||||
* Native CustomComponents software (the "Software"). Subject to your
|
||||
* compliance with these terms, you are hereby granted a non-exclusive,
|
||||
* worldwide, royalty-free copyright license to (1) use and copy the Software;
|
||||
* and (2) reproduce and distribute the Software as part of your own software
|
||||
* ("Your Software"). Facebook reserves all rights not expressly granted to
|
||||
* you in this license agreement.
|
||||
*
|
||||
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
|
||||
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
|
||||
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @providesModule MetroListView
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const ListView = require('ListView');
|
||||
const React = require('React');
|
||||
const RefreshControl = require('RefreshControl');
|
||||
const ScrollView = require('ScrollView');
|
||||
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
|
||||
type Item = any;
|
||||
|
||||
type NormalProps = {
|
||||
FooterComponent?: ReactClass<*>,
|
||||
renderItem: ({item: Item, index: number}) => ?React.Element<*>,
|
||||
renderSectionHeader?: ({section: Object}) => ?React.Element<*>,
|
||||
SeparatorComponent?: ?ReactClass<*>, // not supported yet
|
||||
|
||||
// Provide either `items` or `sections`
|
||||
items?: ?Array<Item>, // By default, an Item is assumed to be {key: string}
|
||||
sections?: ?Array<{key: string, data: Array<Item>}>,
|
||||
|
||||
/**
|
||||
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
|
||||
* sure to also set the `refreshing` prop correctly.
|
||||
*/
|
||||
onRefresh?: ?Function,
|
||||
/**
|
||||
* Set this true while waiting for new data from a refresh.
|
||||
*/
|
||||
refreshing?: boolean,
|
||||
};
|
||||
type DefaultProps = {
|
||||
shouldItemUpdate: (curr: {item: Item}, next: {item: Item}) => boolean,
|
||||
keyExtractor: (item: Item) => string,
|
||||
};
|
||||
type Props = NormalProps & DefaultProps;
|
||||
|
||||
/**
|
||||
* This is just a wrapper around the legacy ListView that matches the new API of FlatList, but with
|
||||
* some section support tacked on. It is recommended to just use FlatList directly, this component
|
||||
* is mostly for debugging and performance comparison.
|
||||
*/
|
||||
class MetroListView extends React.Component {
|
||||
props: Props;
|
||||
scrollToEnd(params?: ?{animated?: ?boolean}) {
|
||||
throw new Error('scrollToEnd not supported in legacy ListView.');
|
||||
}
|
||||
scrollToIndex(params: {animated?: ?boolean, index: number, viewPosition?: number}) {
|
||||
throw new Error('scrollToIndex not supported in legacy ListView.');
|
||||
}
|
||||
scrollToItem(params: {animated?: ?boolean, item: Item, viewPosition?: number}) {
|
||||
throw new Error('scrollToItem not supported in legacy ListView.');
|
||||
}
|
||||
scrollToOffset(params: {animated?: ?boolean, offset: number}) {
|
||||
const {animated, offset} = params;
|
||||
this._listRef.scrollTo(
|
||||
this.props.horizontal ? {x: offset, animated} : {y: offset, animated}
|
||||
);
|
||||
}
|
||||
static defaultProps: DefaultProps = {
|
||||
shouldItemUpdate: () => true,
|
||||
keyExtractor: (item, index) => item.key || index,
|
||||
renderScrollComponent: (props: Props) => {
|
||||
if (props.onRefresh) {
|
||||
return (
|
||||
<ScrollView
|
||||
{...props}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={props.refreshing}
|
||||
onRefresh={props.onRefresh}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <ScrollView {...props} />;
|
||||
}
|
||||
},
|
||||
};
|
||||
state = this._computeState(
|
||||
this.props,
|
||||
{
|
||||
ds: new ListView.DataSource({
|
||||
rowHasChanged: (itemA, itemB) => this.props.shouldItemUpdate({item: itemA}, {item: itemB}),
|
||||
sectionHeaderHasChanged: () => true,
|
||||
getSectionHeaderData: (dataBlob, sectionID) => this.state.sectionHeaderData[sectionID],
|
||||
}),
|
||||
sectionHeaderData: {},
|
||||
},
|
||||
);
|
||||
componentWillReceiveProps(newProps: Props) {
|
||||
this.setState((state) => this._computeState(newProps, state));
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<ListView
|
||||
{...this.props}
|
||||
dataSource={this.state.ds}
|
||||
ref={this._captureRef}
|
||||
renderRow={this._renderRow}
|
||||
renderFooter={this.props.FooterComponent && this._renderFooter}
|
||||
renderSectionHeader={this.props.sections && this._renderSectionHeader}
|
||||
renderSeparator={this.props.SeparatorComponent && this._renderSeparator}
|
||||
/>
|
||||
);
|
||||
}
|
||||
_listRef: ListView;
|
||||
_captureRef = (ref) => { this._listRef = ref; };
|
||||
_computeState(props: Props, state) {
|
||||
const sectionHeaderData = {};
|
||||
if (props.sections) {
|
||||
invariant(!props.items, 'Cannot have both sections and items props.');
|
||||
const sections = {};
|
||||
props.sections.forEach((sectionIn, ii) => {
|
||||
const sectionID = 's' + ii;
|
||||
sections[sectionID] = sectionIn.data;
|
||||
sectionHeaderData[sectionID] = sectionIn;
|
||||
});
|
||||
return {
|
||||
ds: state.ds.cloneWithRowsAndSections(sections),
|
||||
sectionHeaderData,
|
||||
};
|
||||
} else {
|
||||
invariant(!props.sections, 'Cannot have both sections and items props.');
|
||||
return {
|
||||
ds: state.ds.cloneWithRows(props.items),
|
||||
sectionHeaderData,
|
||||
};
|
||||
}
|
||||
}
|
||||
_renderFooter = () => <this.props.FooterComponent key="$footer" />;
|
||||
_renderRow = (item, sectionID, rowID, highlightRow) => {
|
||||
return this.props.renderItem({item, index: rowID});
|
||||
};
|
||||
_renderSectionHeader = (section, sectionID) => {
|
||||
const {renderSectionHeader} = this.props;
|
||||
invariant(renderSectionHeader, 'Must provide renderSectionHeader with sections prop');
|
||||
return renderSectionHeader({section});
|
||||
}
|
||||
_renderSeparator = (sID, rID) => <this.props.SeparatorComponent key={sID + rID} />;
|
||||
}
|
||||
|
||||
module.exports = MetroListView;
|
||||
158
Libraries/CustomComponents/Lists/SectionList.js
Normal file
158
Libraries/CustomComponents/Lists/SectionList.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
|
||||
* all intellectual property and other proprietary rights, in and to the React
|
||||
* Native CustomComponents software (the "Software"). Subject to your
|
||||
* compliance with these terms, you are hereby granted a non-exclusive,
|
||||
* worldwide, royalty-free copyright license to (1) use and copy the Software;
|
||||
* and (2) reproduce and distribute the Software as part of your own software
|
||||
* ("Your Software"). Facebook reserves all rights not expressly granted to
|
||||
* you in this license agreement.
|
||||
*
|
||||
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
|
||||
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
|
||||
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @providesModule SectionList
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const MetroListView = require('MetroListView');
|
||||
const React = require('React');
|
||||
const VirtualizedSectionList = require('VirtualizedSectionList');
|
||||
|
||||
import type {ViewToken} from 'ViewabilityHelper';
|
||||
import type {Props as VirtualizedSectionListProps} from 'VirtualizedSectionList';
|
||||
|
||||
type Item = any;
|
||||
|
||||
type SectionBase<SectionItemT> = {
|
||||
// Must be provided directly on each section.
|
||||
data: Array<SectionItemT>,
|
||||
key: string,
|
||||
|
||||
// Optional props will override list-wide props just for this section.
|
||||
renderItem?: ?({item: SectionItemT, index: number}) => ?React.Element<*>,
|
||||
SeparatorComponent?: ?ReactClass<*>,
|
||||
keyExtractor?: (item: SectionItemT) => string,
|
||||
|
||||
// TODO: support more optional/override props
|
||||
// FooterComponent?: ?ReactClass<*>,
|
||||
// HeaderComponent?: ?ReactClass<*>,
|
||||
// onViewableItemsChanged?: ({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
||||
};
|
||||
|
||||
type RequiredProps<SectionT: SectionBase<*>> = {
|
||||
sections: Array<SectionT>,
|
||||
};
|
||||
|
||||
type OptionalProps<SectionT: SectionBase<*>> = {
|
||||
/**
|
||||
* Default renderer for every item in every section. Can be over-ridden on a per-section basis.
|
||||
*/
|
||||
renderItem: ({item: Item, index: number}) => ?React.Element<*>,
|
||||
/**
|
||||
* Rendered in between adjacent Items within each section.
|
||||
*/
|
||||
ItemSeparatorComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* Rendered at the very beginning of the list.
|
||||
*/
|
||||
ListHeaderComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* Rendered at the very end of the list.
|
||||
*/
|
||||
ListFooterComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* Rendered at the top of each section. Sticky headers are not yet supported.
|
||||
*/
|
||||
renderSectionHeader?: ?({section: SectionT}) => ?React.Element<*>,
|
||||
/**
|
||||
* Rendered in between each section.
|
||||
*/
|
||||
SectionSeparatorComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* Warning: Virtualization can drastically improve memory consumption for long lists, but trashes
|
||||
* the state of items when they scroll out of the render window, so make sure all relavent data is
|
||||
* stored outside of the recursive `renderItem` instance tree.
|
||||
*/
|
||||
enableVirtualization?: ?boolean,
|
||||
keyExtractor: (item: Item, index: number) => string,
|
||||
onEndReached?: ?({distanceFromEnd: number}) => void,
|
||||
/**
|
||||
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
|
||||
* sure to also set the `refreshing` prop correctly.
|
||||
*/
|
||||
onRefresh?: ?Function,
|
||||
/**
|
||||
* Called when the viewability of rows changes, as defined by the
|
||||
* `viewabilityConfig` prop.
|
||||
*/
|
||||
onViewableItemsChanged?: ?({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
||||
/**
|
||||
* Set this true while waiting for new data from a refresh.
|
||||
*/
|
||||
refreshing?: ?boolean,
|
||||
/**
|
||||
* This is an optional optimization to minimize re-rendering items.
|
||||
*/
|
||||
shouldItemUpdate: (
|
||||
prevProps: {item: Item, index: number},
|
||||
nextProps: {item: Item, index: number}
|
||||
) => boolean,
|
||||
};
|
||||
|
||||
type Props<SectionT> = RequiredProps<SectionT>
|
||||
& OptionalProps<SectionT>
|
||||
& VirtualizedSectionListProps<SectionT>;
|
||||
|
||||
type DefaultProps = typeof VirtualizedSectionList.defaultProps;
|
||||
|
||||
/**
|
||||
* A performant interface for rendering sectioned lists, supporting the most handy features:
|
||||
*
|
||||
* - Fully cross-platform.
|
||||
* - Viewability callbacks.
|
||||
* - Footer support.
|
||||
* - Separator support.
|
||||
* - Heterogeneous data and item support.
|
||||
* - Pull to Refresh.
|
||||
*
|
||||
* If you don't need section support and want a simpler interface, use FlatList.
|
||||
*/
|
||||
class SectionList<SectionT: SectionBase<*>>
|
||||
extends React.PureComponent<DefaultProps, Props<SectionT>, *>
|
||||
{
|
||||
props: Props<SectionT>;
|
||||
static defaultProps: DefaultProps = VirtualizedSectionList.defaultProps;
|
||||
|
||||
render() {
|
||||
const {ListFooterComponent, ListHeaderComponent, ItemSeparatorComponent} = this.props;
|
||||
const List = this.props.legacyImplementation ? MetroListView : VirtualizedSectionList;
|
||||
return (
|
||||
<List
|
||||
{...this.props}
|
||||
FooterComponent={ListFooterComponent}
|
||||
HeaderComponent={ListHeaderComponent}
|
||||
SeparatorComponent={ItemSeparatorComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SectionList;
|
||||
285
Libraries/CustomComponents/Lists/ViewabilityHelper.js
Normal file
285
Libraries/CustomComponents/Lists/ViewabilityHelper.js
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule ViewabilityHelper
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
export type ViewToken = {item: any, key: string, index: ?number, isViewable: boolean, section?: any};
|
||||
|
||||
export type ViewabilityConfig = {|
|
||||
/**
|
||||
* Minimum amount of time (in milliseconds) that an item must be physically viewable before the
|
||||
* viewability callback will be fired. A high number means that scrolling through content without
|
||||
* stopping will not mark the content as viewable.
|
||||
*/
|
||||
minimumViewTime?: number,
|
||||
|
||||
/**
|
||||
* Percent of viewport that must be covered for a partially occluded item to count as
|
||||
* "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means
|
||||
* that a single pixel in the viewport makes the item viewable, and a value of 100 means that
|
||||
* an item must be either entirely visible or cover the entire viewport to count as viewable.
|
||||
*/
|
||||
viewAreaCoveragePercentThreshold?: number,
|
||||
|
||||
/**
|
||||
* Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible,
|
||||
* rather than the fraction of the viewable area it covers.
|
||||
*/
|
||||
itemVisiblePercentThreshold?: number,
|
||||
|
||||
/**
|
||||
* Nothing is considered viewable until the user scrolls or `recordInteraction` is called after
|
||||
* render.
|
||||
*/
|
||||
waitForInteraction?: boolean,
|
||||
|
||||
/**
|
||||
* Criteria to filter out certain scroll events so they don't count as interactions. By default,
|
||||
* any non-zero scroll offset will be considered an interaction.
|
||||
*/
|
||||
scrollInteractionFilter?: {|
|
||||
minimumOffset?: number, // scrolls with an offset less than this are ignored.
|
||||
minimumElapsed?: number, // scrolls that happen before this are ignored.
|
||||
|},
|
||||
|};
|
||||
|
||||
/**
|
||||
* A Utility class for calculating viewable items based on current metrics like scroll position and
|
||||
* layout.
|
||||
*
|
||||
* An item is said to be in a "viewable" state when any of the following
|
||||
* is true for longer than `minViewTime` milliseconds (after an interaction if `waitForInteraction`
|
||||
* is true):
|
||||
*
|
||||
* - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item
|
||||
* visible in the view area >= `itemVisiblePercentThreshold`.
|
||||
* - Entirely visible on screen
|
||||
*/
|
||||
class ViewabilityHelper {
|
||||
_config: ViewabilityConfig;
|
||||
_hasInteracted: boolean = false;
|
||||
_lastUpdateTime: number = 0;
|
||||
_timers: Set<number> = new Set();
|
||||
_viewableIndices: Array<number> = [];
|
||||
_viewableItems: Map<string, ViewToken> = new Map();
|
||||
|
||||
constructor(config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0}) {
|
||||
invariant(
|
||||
config.scrollInteractionFilter == null || config.waitForInteraction,
|
||||
'scrollInteractionFilter only works in conjunction with waitForInteraction',
|
||||
);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup, e.g. on unmount. Clears any pending timers.
|
||||
*/
|
||||
dispose() {
|
||||
this._timers.forEach(clearTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which items are viewable based on the current metrics and config.
|
||||
*/
|
||||
computeViewableItems(
|
||||
itemCount: number,
|
||||
scrollOffset: number,
|
||||
viewportHeight: number,
|
||||
getFrameMetrics: (index: number) => ?{length: number, offset: number},
|
||||
renderRange?: {first: number, last: number}, // Optional optimization to reduce the scan size
|
||||
): Array<number> {
|
||||
const {itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold} = this._config;
|
||||
const viewAreaMode = viewAreaCoveragePercentThreshold != null;
|
||||
const viewablePercentThreshold = viewAreaMode ?
|
||||
viewAreaCoveragePercentThreshold :
|
||||
itemVisiblePercentThreshold;
|
||||
invariant(
|
||||
viewablePercentThreshold != null &&
|
||||
(itemVisiblePercentThreshold != null) !== (viewAreaCoveragePercentThreshold != null),
|
||||
'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold',
|
||||
);
|
||||
const viewableIndices = [];
|
||||
if (itemCount === 0) {
|
||||
return viewableIndices;
|
||||
}
|
||||
let firstVisible = -1;
|
||||
const {first, last} = renderRange || {first: 0, last: itemCount - 1};
|
||||
invariant(
|
||||
last < itemCount,
|
||||
'Invalid render range ' + JSON.stringify({renderRange, itemCount})
|
||||
);
|
||||
for (let idx = first; idx <= last; idx++) {
|
||||
const metrics = getFrameMetrics(idx);
|
||||
if (!metrics) {
|
||||
continue;
|
||||
}
|
||||
const top = metrics.offset - scrollOffset;
|
||||
const bottom = top + metrics.length;
|
||||
if ((top < viewportHeight) && (bottom > 0)) {
|
||||
firstVisible = idx;
|
||||
if (_isViewable(
|
||||
viewAreaMode,
|
||||
viewablePercentThreshold,
|
||||
top,
|
||||
bottom,
|
||||
viewportHeight,
|
||||
metrics.length,
|
||||
)) {
|
||||
viewableIndices.push(idx);
|
||||
}
|
||||
} else if (firstVisible >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return viewableIndices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out which items are viewable and how that has changed from before and calls
|
||||
* `onViewableItemsChanged` as appropriate.
|
||||
*/
|
||||
onUpdate(
|
||||
itemCount: number,
|
||||
scrollOffset: number,
|
||||
viewportHeight: number,
|
||||
getFrameMetrics: (index: number) => ?{length: number, offset: number},
|
||||
createViewToken: (index: number, isViewable: boolean) => ViewToken,
|
||||
onViewableItemsChanged: ({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
||||
renderRange?: {first: number, last: number}, // Optional optimization to reduce the scan size
|
||||
): void {
|
||||
const updateTime = Date.now();
|
||||
if (this._lastUpdateTime === 0 && getFrameMetrics(0)) {
|
||||
// Only count updates after the first item is rendered and has a frame.
|
||||
this._lastUpdateTime = updateTime;
|
||||
}
|
||||
const updateElapsed = this._lastUpdateTime ? updateTime - this._lastUpdateTime : 0;
|
||||
if (this._config.waitForInteraction && !this._hasInteracted && scrollOffset !== 0) {
|
||||
const filter = this._config.scrollInteractionFilter;
|
||||
if (filter) {
|
||||
if ((filter.minimumOffset == null || scrollOffset >= filter.minimumOffset) &&
|
||||
(filter.minimumElapsed == null || updateElapsed >= filter.minimumElapsed)) {
|
||||
this._hasInteracted = true;
|
||||
}
|
||||
} else {
|
||||
this._hasInteracted = true;
|
||||
}
|
||||
}
|
||||
if (this._config.waitForInteraction && !this._hasInteracted) {
|
||||
return;
|
||||
}
|
||||
let viewableIndices = [];
|
||||
if (itemCount) {
|
||||
viewableIndices = this.computeViewableItems(
|
||||
itemCount,
|
||||
scrollOffset,
|
||||
viewportHeight,
|
||||
getFrameMetrics,
|
||||
renderRange,
|
||||
);
|
||||
}
|
||||
if (this._viewableIndices.length === viewableIndices.length &&
|
||||
this._viewableIndices.every((v, ii) => v === viewableIndices[ii])) {
|
||||
// We might get a lot of scroll events where visibility doesn't change and we don't want to do
|
||||
// extra work in those cases.
|
||||
return;
|
||||
}
|
||||
this._viewableIndices = viewableIndices;
|
||||
this._lastUpdateTime = updateTime;
|
||||
if (this._config.minViewTime && updateElapsed < this._config.minViewTime) {
|
||||
const handle = setTimeout(
|
||||
() => {
|
||||
this._timers.delete(handle);
|
||||
this._onUpdateSync(viewableIndices, onViewableItemsChanged, createViewToken);
|
||||
},
|
||||
this._config.minViewTime,
|
||||
);
|
||||
this._timers.add(handle);
|
||||
} else {
|
||||
this._onUpdateSync(viewableIndices, onViewableItemsChanged, createViewToken);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that an interaction has happened even if there has been no scroll.
|
||||
*/
|
||||
recordInteraction() {
|
||||
this._hasInteracted = true;
|
||||
}
|
||||
|
||||
_onUpdateSync(viewableIndicesToCheck, onViewableItemsChanged, createViewToken) {
|
||||
// Filter out indices that have gone out of view since this call was scheduled.
|
||||
viewableIndicesToCheck = viewableIndicesToCheck.filter(
|
||||
(ii) => this._viewableIndices.includes(ii)
|
||||
);
|
||||
const prevItems = this._viewableItems;
|
||||
const nextItems = new Map(
|
||||
viewableIndicesToCheck.map(ii => {
|
||||
const viewable = createViewToken(ii, true);
|
||||
return [viewable.key, viewable];
|
||||
})
|
||||
);
|
||||
|
||||
const changed = [];
|
||||
for (const [key, viewable] of nextItems) {
|
||||
if (!prevItems.has(key)) {
|
||||
changed.push(viewable);
|
||||
}
|
||||
}
|
||||
for (const [key, viewable] of prevItems) {
|
||||
if (!nextItems.has(key)) {
|
||||
changed.push({...viewable, isViewable: false});
|
||||
}
|
||||
}
|
||||
if (changed.length > 0) {
|
||||
this._viewableItems = nextItems;
|
||||
onViewableItemsChanged({viewableItems: Array.from(nextItems.values()), changed});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function _isViewable(
|
||||
viewAreaMode: boolean,
|
||||
viewablePercentThreshold: number,
|
||||
top: number,
|
||||
bottom: number,
|
||||
viewportHeight: number,
|
||||
itemLength: number,
|
||||
): bool {
|
||||
if (_isEntirelyVisible(top, bottom, viewportHeight)) {
|
||||
return true;
|
||||
} else {
|
||||
const pixels = _getPixelsVisible(top, bottom, viewportHeight);
|
||||
const percent = 100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength);
|
||||
return percent >= viewablePercentThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
function _getPixelsVisible(
|
||||
top: number,
|
||||
bottom: number,
|
||||
viewportHeight: number
|
||||
): number {
|
||||
const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0);
|
||||
return Math.max(0, visibleHeight);
|
||||
}
|
||||
|
||||
function _isEntirelyVisible(
|
||||
top: number,
|
||||
bottom: number,
|
||||
viewportHeight: number
|
||||
): bool {
|
||||
return top >= 0 && bottom <= viewportHeight && bottom > top;
|
||||
}
|
||||
|
||||
module.exports = ViewabilityHelper;
|
||||
163
Libraries/CustomComponents/Lists/VirtualizeUtils.js
Normal file
163
Libraries/CustomComponents/Lists/VirtualizeUtils.js
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule VirtualizeUtils
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
/**
|
||||
* Used to find the indices of the frames that overlap the given offsets. Useful for finding the
|
||||
* items that bound different windows of content, such as the visible area or the buffered overscan
|
||||
* area.
|
||||
*/
|
||||
function elementsThatOverlapOffsets(
|
||||
offsets: Array<number>,
|
||||
itemCount: number,
|
||||
getFrameMetrics: (index: number) => {length: number, offset: number},
|
||||
): Array<number> {
|
||||
const out = [];
|
||||
for (let ii = 0; ii < itemCount; ii++) {
|
||||
const frame = getFrameMetrics(ii);
|
||||
const trailingOffset = frame.offset + frame.length;
|
||||
for (let kk = 0; kk < offsets.length; kk++) {
|
||||
if (out[kk] == null && trailingOffset >= offsets[kk]) {
|
||||
out[kk] = ii;
|
||||
if (kk === offsets.length - 1) {
|
||||
invariant(
|
||||
out.length === offsets.length,
|
||||
'bad offsets input, should be in increasing order ' + JSON.stringify(offsets)
|
||||
);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the number of elements in the `next` range that are new compared to the `prev` range.
|
||||
* Handy for calculating how many new items will be rendered when the render window changes so we
|
||||
* can restrict the number of new items render at once so that content can appear on the screen
|
||||
* faster.
|
||||
*/
|
||||
function newRangeCount(
|
||||
prev: {first: number, last: number},
|
||||
next: {first: number, last: number},
|
||||
): number {
|
||||
return (next.last - next.first + 1) -
|
||||
Math.max(
|
||||
0,
|
||||
1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom logic for determining which items should be rendered given the current frame and scroll
|
||||
* metrics, as well as the previous render state. The algorithm may evolve over time, but generally
|
||||
* prioritizes the visible area first, then expands that with overscan regions ahead and behind,
|
||||
* biased in the direction of scroll.
|
||||
*/
|
||||
function computeWindowedRenderLimits(
|
||||
props: {
|
||||
data: any,
|
||||
getItemCount: (data: any) => number,
|
||||
maxToRenderPerBatch: number,
|
||||
windowSize: number,
|
||||
},
|
||||
prev: {first: number, last: number},
|
||||
getFrameMetricsApprox: (index: number) => {length: number, offset: number},
|
||||
scrollMetrics: {dt: number, offset: number, velocity: number, visibleLength: number},
|
||||
): {first: number, last: number} {
|
||||
const {data, getItemCount, maxToRenderPerBatch, windowSize} = props;
|
||||
const itemCount = getItemCount(data);
|
||||
if (itemCount === 0) {
|
||||
return prev;
|
||||
}
|
||||
const {offset, velocity, visibleLength} = scrollMetrics;
|
||||
|
||||
// Start with visible area, then compute maximum overscan region by expanding from there, biased
|
||||
// in the direction of scroll. Total overscan area is capped, which should cap memory consumption
|
||||
// too.
|
||||
const visibleBegin = Math.max(0, offset);
|
||||
const visibleEnd = visibleBegin + visibleLength;
|
||||
const overscanLength = (windowSize - 1) * visibleLength;
|
||||
const leadFactor = Math.max(0, Math.min(1, velocity / 5 + 0.5));
|
||||
const overscanBegin = Math.max(0, visibleBegin - (1 - leadFactor) * overscanLength);
|
||||
const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);
|
||||
|
||||
// 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],
|
||||
props.getItemCount(props.data),
|
||||
getFrameMetricsApprox,
|
||||
);
|
||||
overscanFirst = overscanFirst == null ? 0 : overscanFirst;
|
||||
first = first == null ? Math.max(0, overscanFirst) : first;
|
||||
overscanLast = overscanLast == null ? (itemCount - 1) : overscanLast;
|
||||
last = last == null ? Math.min(overscanLast, first + maxToRenderPerBatch - 1) : last;
|
||||
const visible = {first, last};
|
||||
|
||||
// We want to limit the number of new cells we're rendering per batch so that we can fill the
|
||||
// content on the screen quickly. If we rendered the entire overscan window at once, the user
|
||||
// could be staring at white space for a long time waiting for a bunch of offscreen content to
|
||||
// render.
|
||||
let newCellCount = newRangeCount(prev, visible);
|
||||
|
||||
while (true) {
|
||||
if (first <= overscanFirst && last >= overscanLast) {
|
||||
// If we fill the entire overscan range, we're done.
|
||||
break;
|
||||
}
|
||||
const maxNewCells = newCellCount >= maxToRenderPerBatch;
|
||||
const firstWillAddMore = first <= prev.first || first > prev.last;
|
||||
const firstShouldIncrement = first > overscanFirst && (!maxNewCells || !firstWillAddMore);
|
||||
const lastWillAddMore = last >= prev.last || last < prev.first;
|
||||
const lastShouldIncrement = last < overscanLast && (!maxNewCells || !lastWillAddMore);
|
||||
if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) {
|
||||
// We only want to stop if we've hit maxNewCells AND we cannot increment first or last
|
||||
// without rendering new items. This let's us preserve as many already rendered items as
|
||||
// possible, reducing render churn and keeping the rendered overscan range as large as
|
||||
// possible.
|
||||
break;
|
||||
}
|
||||
if (firstShouldIncrement) {
|
||||
if (firstWillAddMore) {
|
||||
newCellCount++;
|
||||
}
|
||||
first--;
|
||||
}
|
||||
if (lastShouldIncrement) {
|
||||
if (lastWillAddMore) {
|
||||
newCellCount++;
|
||||
}
|
||||
last++;
|
||||
}
|
||||
}
|
||||
if (!(
|
||||
last >= first &&
|
||||
first >= 0 && last < itemCount &&
|
||||
first >= overscanFirst && last <= overscanLast &&
|
||||
first <= visible.first && last >= visible.last
|
||||
)) {
|
||||
throw new Error('Bad window calculation ' +
|
||||
JSON.stringify({first, last, itemCount, overscanFirst, overscanLast, visible}));
|
||||
}
|
||||
return {first, last};
|
||||
}
|
||||
|
||||
const VirtualizeUtils = {
|
||||
computeWindowedRenderLimits,
|
||||
elementsThatOverlapOffsets,
|
||||
newRangeCount,
|
||||
};
|
||||
|
||||
module.exports = VirtualizeUtils;
|
||||
671
Libraries/CustomComponents/Lists/VirtualizedList.js
Normal file
671
Libraries/CustomComponents/Lists/VirtualizedList.js
Normal file
@@ -0,0 +1,671 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
|
||||
* all intellectual property and other proprietary rights, in and to the React
|
||||
* Native CustomComponents software (the "Software"). Subject to your
|
||||
* compliance with these terms, you are hereby granted a non-exclusive,
|
||||
* worldwide, royalty-free copyright license to (1) use and copy the Software;
|
||||
* and (2) reproduce and distribute the Software as part of your own software
|
||||
* ("Your Software"). Facebook reserves all rights not expressly granted to
|
||||
* you in this license agreement.
|
||||
*
|
||||
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
|
||||
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
|
||||
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @providesModule VirtualizedList
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const Batchinator = require('Batchinator');
|
||||
const React = require('React');
|
||||
const RefreshControl = require('RefreshControl');
|
||||
const ScrollView = require('ScrollView');
|
||||
const View = require('View');
|
||||
const ViewabilityHelper = require('ViewabilityHelper');
|
||||
|
||||
const infoLog = require('infoLog');
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
|
||||
const {computeWindowedRenderLimits} = require('VirtualizeUtils');
|
||||
|
||||
import type {ViewabilityConfig, ViewToken} from 'ViewabilityHelper';
|
||||
|
||||
type Item = any;
|
||||
type renderItemType = ({item: Item, index: number}) => ?React.Element<*>;
|
||||
|
||||
/**
|
||||
* Renders a virtual list of items given a data blob and accessor functions. Items that are outside
|
||||
* the render window (except for the initial items at the top) are 'virtualized' e.g. unmounted or
|
||||
* never rendered in the first place. This improves performance and saves memory for large data
|
||||
* sets, but will reset state on items that scroll too far out of the render window.
|
||||
*
|
||||
* TODO: Note that LayoutAnimation and sticky section headers both have bugs when used with this and
|
||||
* are therefor not supported, but new Animated impl might work?
|
||||
* https://github.com/facebook/react-native/pull/11315
|
||||
*
|
||||
* TODO: removeClippedSubviews might not be necessary and may cause bugs?
|
||||
*
|
||||
*/
|
||||
type RequiredProps = {
|
||||
renderItem: renderItemType,
|
||||
/**
|
||||
* The default accessor functions assume this is an Array<{key: string}> but you can override
|
||||
* getItem, getItemCount, and keyExtractor to handle any type of index-based data.
|
||||
*/
|
||||
data?: any,
|
||||
};
|
||||
type OptionalProps = {
|
||||
FooterComponent?: ?ReactClass<*>,
|
||||
HeaderComponent?: ?ReactClass<*>,
|
||||
SeparatorComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* `debug` will turn on extra logging and visual overlays to aid with debugging both usage and
|
||||
* implementation.
|
||||
*/
|
||||
debug?: ?boolean,
|
||||
/**
|
||||
* DEPRECATED: Virtualization provides significant performance and memory optimizations, but fully
|
||||
* unmounts react instances that are outside of the render window. You should only need to disable
|
||||
* this for debugging purposes.
|
||||
*/
|
||||
disableVirtualization: boolean,
|
||||
getItem: (items: any, index: number) => ?Item,
|
||||
getItemCount: (items: any) => number,
|
||||
getItemLayout?: (items: any, index: number) =>
|
||||
{length: number, offset: number, index: number}, // e.g. height, y
|
||||
horizontal?: ?boolean,
|
||||
initialNumToRender: number,
|
||||
keyExtractor: (item: Item, index: number) => string,
|
||||
maxToRenderPerBatch: number,
|
||||
onEndReached?: ?({distanceFromEnd: number}) => void,
|
||||
onEndReachedThreshold?: ?number, // units of visible length
|
||||
onLayout?: ?Function,
|
||||
/**
|
||||
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
|
||||
* sure to also set the `refreshing` prop correctly.
|
||||
*/
|
||||
onRefresh?: ?Function,
|
||||
/**
|
||||
* Called when the viewability of rows changes, as defined by the
|
||||
* `viewabilityConfig` prop.
|
||||
*/
|
||||
onViewableItemsChanged?: ?({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
||||
/**
|
||||
* Set this true while waiting for new data from a refresh.
|
||||
*/
|
||||
refreshing?: ?boolean,
|
||||
removeClippedSubviews?: boolean,
|
||||
renderScrollComponent: (props: Object) => React.Element<*>,
|
||||
shouldItemUpdate: (
|
||||
props: {item: Item, index: number},
|
||||
nextProps: {item: Item, index: number}
|
||||
) => boolean,
|
||||
updateCellsBatchingPeriod: number,
|
||||
viewabilityConfig?: ViewabilityConfig,
|
||||
windowSize: number, // units of visible length
|
||||
};
|
||||
export type Props = RequiredProps & OptionalProps;
|
||||
|
||||
let _usedIndexForKey = false;
|
||||
|
||||
class VirtualizedList extends React.PureComponent<OptionalProps, Props, *> {
|
||||
props: Props;
|
||||
|
||||
// scrollToEnd may be janky without getItemLayout prop
|
||||
scrollToEnd(params?: ?{animated?: ?boolean}) {
|
||||
const animated = params ? params.animated : true;
|
||||
const veryLast = this.props.getItemCount(this.props.data) - 1;
|
||||
const frame = this._getFrameMetricsApprox(veryLast);
|
||||
const offset = frame.offset + frame.length + this._footerLength -
|
||||
this._scrollMetrics.visibleLength;
|
||||
this._scrollRef.scrollTo(
|
||||
this.props.horizontal ? {x: offset, animated} : {y: offset, animated}
|
||||
);
|
||||
}
|
||||
|
||||
// scrollToIndex may be janky without getItemLayout prop
|
||||
scrollToIndex(params: {animated?: ?boolean, index: number, viewPosition?: number}) {
|
||||
const {data, horizontal, getItemCount} = this.props;
|
||||
const {animated, index, viewPosition} = params;
|
||||
if (!(index >= 0 && index < getItemCount(data))) {
|
||||
console.warn('scrollToIndex out of range ' + index);
|
||||
return;
|
||||
}
|
||||
const frame = this._getFrameMetricsApprox(index);
|
||||
const offset = Math.max(
|
||||
0,
|
||||
frame.offset - (viewPosition || 0) * (this._scrollMetrics.visibleLength - frame.length),
|
||||
);
|
||||
this._scrollRef.scrollTo(horizontal ? {x: offset, animated} : {y: offset, animated});
|
||||
}
|
||||
|
||||
// scrollToItem may be janky without getItemLayout prop. Required linear scan through items -
|
||||
// use scrollToIndex instead if possible.
|
||||
scrollToItem(params: {animated?: ?boolean, item: Item, viewPosition?: number}) {
|
||||
const {item} = params;
|
||||
const {data, getItem, getItemCount} = this.props;
|
||||
const itemCount = getItemCount(data);
|
||||
for (let index = 0; index < itemCount; index++) {
|
||||
if (getItem(data, index) === item) {
|
||||
this.scrollToIndex({...params, index});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollToOffset(params: {animated?: ?boolean, offset: number}) {
|
||||
const {animated, offset} = params;
|
||||
this._scrollRef.scrollTo(
|
||||
this.props.horizontal ? {x: offset, animated} : {y: offset, animated}
|
||||
);
|
||||
}
|
||||
|
||||
recordInteraction() {
|
||||
this._viewabilityHelper.recordInteraction();
|
||||
this._updateViewableItems(this.props.data);
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
disableVirtualization: false,
|
||||
getItem: (data: any, index: number) => data[index],
|
||||
getItemCount: (data: any) => data ? data.length : 0,
|
||||
horizontal: false,
|
||||
initialNumToRender: 10,
|
||||
keyExtractor: (item: Item, index: number) => {
|
||||
if (item.key != null) {
|
||||
return item.key;
|
||||
}
|
||||
_usedIndexForKey = true;
|
||||
return String(index);
|
||||
},
|
||||
maxToRenderPerBatch: 10,
|
||||
onEndReached: () => {},
|
||||
onEndReachedThreshold: 2, // multiples of length
|
||||
removeClippedSubviews: true,
|
||||
renderScrollComponent: (props: Props) => {
|
||||
if (props.onRefresh) {
|
||||
invariant(
|
||||
typeof props.refreshing === 'boolean',
|
||||
'`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' +
|
||||
JSON.stringify(props.refreshing) + '`',
|
||||
);
|
||||
return (
|
||||
<ScrollView
|
||||
{...props}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={props.refreshing}
|
||||
onRefresh={props.onRefresh}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <ScrollView {...props} />;
|
||||
}
|
||||
},
|
||||
shouldItemUpdate: (
|
||||
props: {item: Item, index: number},
|
||||
nextProps: {item: Item, index: number},
|
||||
) => true,
|
||||
updateCellsBatchingPeriod: 50,
|
||||
windowSize: 21, // multiples of length
|
||||
};
|
||||
|
||||
state = {
|
||||
first: 0,
|
||||
last: this.props.initialNumToRender,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
invariant(
|
||||
!props.onScroll || !props.onScroll.__isNative,
|
||||
'VirtualizedList does not support AnimatedEvent with onScroll and useNativeDriver',
|
||||
);
|
||||
this._updateCellsToRenderBatcher = new Batchinator(
|
||||
this._updateCellsToRender,
|
||||
this.props.updateCellsBatchingPeriod,
|
||||
);
|
||||
this._viewabilityHelper = new ViewabilityHelper(this.props.viewabilityConfig);
|
||||
this.state = {
|
||||
first: 0,
|
||||
last: Math.min(this.props.getItemCount(this.props.data), this.props.initialNumToRender) - 1,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._updateViewableItems(null);
|
||||
this._updateCellsToRenderBatcher.dispose();
|
||||
this._viewabilityHelper.dispose();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps: Props) {
|
||||
const {data, getItemCount, maxToRenderPerBatch} = newProps;
|
||||
// first and last could be stale (e.g. if a new, shorter items props is passed in), so we make
|
||||
// sure we're rendering a reasonable range here.
|
||||
this.setState({
|
||||
first: Math.max(0, Math.min(this.state.first, getItemCount(data) - 1 - maxToRenderPerBatch)),
|
||||
last: Math.max(0, Math.min(this.state.last, getItemCount(data) - 1)),
|
||||
});
|
||||
this._updateCellsToRenderBatcher.schedule();
|
||||
}
|
||||
|
||||
_pushCells(cells, first, last) {
|
||||
const {SeparatorComponent, data, getItem, getItemCount, keyExtractor} = this.props;
|
||||
const end = getItemCount(data) - 1;
|
||||
last = Math.min(end, last);
|
||||
for (let ii = first; ii <= last; ii++) {
|
||||
const item = getItem(data, ii);
|
||||
invariant(item, 'No item for index ' + ii);
|
||||
const key = keyExtractor(item, ii);
|
||||
cells.push(
|
||||
<CellRenderer
|
||||
cellKey={key}
|
||||
index={ii}
|
||||
item={item}
|
||||
key={key}
|
||||
onLayout={this._onCellLayout}
|
||||
onUnmount={this._onCellUnmount}
|
||||
parentProps={this.props}
|
||||
/>
|
||||
);
|
||||
if (SeparatorComponent && ii < end) {
|
||||
cells.push(<SeparatorComponent key={'sep' + ii}/>);
|
||||
}
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const {FooterComponent, HeaderComponent} = this.props;
|
||||
const {data, disableVirtualization, horizontal} = this.props;
|
||||
const cells = [];
|
||||
if (HeaderComponent) {
|
||||
cells.push(
|
||||
<View key="$header" onLayout={this._onLayoutHeader}>
|
||||
<HeaderComponent />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const itemCount = this.props.getItemCount(data);
|
||||
if (itemCount > 0) {
|
||||
_usedIndexForKey = false;
|
||||
const lastInitialIndex = this.props.initialNumToRender - 1;
|
||||
const {first, last} = this.state;
|
||||
this._pushCells(cells, 0, lastInitialIndex);
|
||||
if (!disableVirtualization && first > lastInitialIndex) {
|
||||
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
|
||||
const firstSpace = this._getFrameMetricsApprox(first).offset -
|
||||
(initBlock.offset + initBlock.length);
|
||||
cells.push(
|
||||
<View key="$lead_spacer" style={{[!horizontal ? 'height' : 'width']: firstSpace}} />
|
||||
);
|
||||
}
|
||||
this._pushCells(cells, Math.max(lastInitialIndex + 1, first), last);
|
||||
if (!this._hasWarned.keys && _usedIndexForKey) {
|
||||
console.warn(
|
||||
'VirtualizedList: missing keys for items, make sure to specify a key property on each ' +
|
||||
'item or provide a custom keyExtractor.'
|
||||
);
|
||||
this._hasWarned.keys = true;
|
||||
}
|
||||
if (!disableVirtualization && last < itemCount - 1) {
|
||||
const lastFrame = this._getFrameMetricsApprox(last);
|
||||
const end = this.props.getItemLayout ?
|
||||
itemCount - 1 :
|
||||
Math.min(itemCount - 1, this._highestMeasuredFrameIndex);
|
||||
const endFrame = this._getFrameMetricsApprox(end);
|
||||
const tailSpacerLength =
|
||||
(endFrame.offset + endFrame.length) -
|
||||
(lastFrame.offset + lastFrame.length);
|
||||
cells.push(
|
||||
<View key="$tail_spacer" style={{[!horizontal ? 'height' : 'width']: tailSpacerLength}} />
|
||||
);
|
||||
}
|
||||
}
|
||||
if (FooterComponent) {
|
||||
cells.push(
|
||||
<View key="$footer" onLayout={this._onLayoutFooter}>
|
||||
<FooterComponent />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const ret = React.cloneElement(
|
||||
this.props.renderScrollComponent(this.props),
|
||||
{
|
||||
onContentSizeChange: this._onContentSizeChange,
|
||||
onLayout: this._onLayout,
|
||||
onScroll: this._onScroll,
|
||||
ref: this._captureScrollRef,
|
||||
scrollEventThrottle: 50, // TODO: Android support
|
||||
},
|
||||
cells,
|
||||
);
|
||||
if (this.props.debug) {
|
||||
return <View style={{flex: 1}}>{ret}{this._renderDebugOverlay()}</View>;
|
||||
} else {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._updateCellsToRenderBatcher.schedule();
|
||||
}
|
||||
|
||||
_averageCellLength = 0;
|
||||
_hasWarned = {};
|
||||
_highestMeasuredFrameIndex = 0;
|
||||
_headerLength = 0;
|
||||
_frames = {};
|
||||
_footerLength = 0;
|
||||
_scrollMetrics = {
|
||||
visibleLength: 0, contentLength: 0, offset: 0, dt: 10, velocity: 0, timestamp: 0,
|
||||
};
|
||||
_scrollRef = (null: any);
|
||||
_sentEndForContentLength = 0;
|
||||
_totalCellLength = 0;
|
||||
_totalCellsMeasured = 0;
|
||||
_updateCellsToRenderBatcher: Batchinator;
|
||||
_viewabilityHelper: ViewabilityHelper;
|
||||
|
||||
_captureScrollRef = (ref) => {
|
||||
this._scrollRef = ref;
|
||||
};
|
||||
|
||||
_onCellLayout = (e, cellKey, index) => {
|
||||
const layout = e.nativeEvent.layout;
|
||||
const next = {
|
||||
offset: this._selectOffset(layout),
|
||||
length: this._selectLength(layout),
|
||||
index,
|
||||
inLayout: true,
|
||||
};
|
||||
const curr = this._frames[cellKey];
|
||||
if (!curr ||
|
||||
next.offset !== curr.offset ||
|
||||
next.length !== curr.length ||
|
||||
index !== curr.index
|
||||
) {
|
||||
this._totalCellLength += next.length - (curr ? curr.length : 0);
|
||||
this._totalCellsMeasured += (curr ? 0 : 1);
|
||||
this._averageCellLength = this._totalCellLength / this._totalCellsMeasured;
|
||||
this._frames[cellKey] = next;
|
||||
this._highestMeasuredFrameIndex = Math.max(this._highestMeasuredFrameIndex, index);
|
||||
this._updateCellsToRenderBatcher.schedule();
|
||||
}
|
||||
};
|
||||
|
||||
_onCellUnmount = (cellKey: string) => {
|
||||
const curr = this._frames[cellKey];
|
||||
if (curr) {
|
||||
this._frames[cellKey] = {...curr, inLayout: false};
|
||||
}
|
||||
};
|
||||
|
||||
_onLayout = (e: Object) => {
|
||||
this._scrollMetrics.visibleLength = this._selectLength(e.nativeEvent.layout);
|
||||
this.props.onLayout && this.props.onLayout(e);
|
||||
this._updateCellsToRenderBatcher.schedule();
|
||||
};
|
||||
|
||||
_onLayoutFooter = (e) => {
|
||||
this._footerLength = this._selectLength(e.nativeEvent.layout);
|
||||
};
|
||||
|
||||
_onLayoutHeader = (e) => {
|
||||
this._headerLength = this._selectLength(e.nativeEvent.layout);
|
||||
};
|
||||
|
||||
_renderDebugOverlay() {
|
||||
const normalize = this._scrollMetrics.visibleLength / this._scrollMetrics.contentLength;
|
||||
const framesInLayout = [];
|
||||
const itemCount = this.props.getItemCount(this.props.data);
|
||||
for (let ii = 0; ii < itemCount; ii++) {
|
||||
const frame = this._getFrameMetricsApprox(ii);
|
||||
if (frame.inLayout) {
|
||||
framesInLayout.push(frame);
|
||||
}
|
||||
}
|
||||
const windowTop = this._getFrameMetricsApprox(this.state.first).offset;
|
||||
const frameLast = this._getFrameMetricsApprox(this.state.last);
|
||||
const windowLen = frameLast.offset + frameLast.length - windowTop;
|
||||
const visTop = this._scrollMetrics.offset;
|
||||
const visLen = this._scrollMetrics.visibleLength;
|
||||
const baseStyle = {position: 'absolute', top: 0, right: 0};
|
||||
return (
|
||||
<View style={{...baseStyle, bottom: 0, width: 20, borderColor: 'blue', borderWidth: 1}}>
|
||||
{framesInLayout.map((f, ii) =>
|
||||
<View key={'f' + ii} style={{
|
||||
...baseStyle,
|
||||
left: 0,
|
||||
top: f.offset * normalize,
|
||||
height: f.length * normalize,
|
||||
backgroundColor: 'orange',
|
||||
}} />
|
||||
)}
|
||||
<View style={{
|
||||
...baseStyle,
|
||||
left: 0,
|
||||
top: windowTop * normalize,
|
||||
height: windowLen * normalize,
|
||||
borderColor: 'green',
|
||||
borderWidth: 2,
|
||||
}} />
|
||||
<View style={{
|
||||
...baseStyle,
|
||||
left: 0,
|
||||
top: visTop * normalize,
|
||||
height: visLen * normalize,
|
||||
borderColor: 'red',
|
||||
borderWidth: 2,
|
||||
}} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_selectLength(metrics: {height: number, width: number}): number {
|
||||
return !this.props.horizontal ? metrics.height : metrics.width;
|
||||
}
|
||||
|
||||
_selectOffset(metrics: {x: number, y: number}): number {
|
||||
return !this.props.horizontal ? metrics.y : metrics.x;
|
||||
}
|
||||
|
||||
_onContentSizeChange = (width: number, height: number) => {
|
||||
this._scrollMetrics.contentLength = this._selectLength({height, width});
|
||||
this._updateCellsToRenderBatcher.schedule();
|
||||
};
|
||||
|
||||
_onScroll = (e: Object) => {
|
||||
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);
|
||||
const dt = Math.max(1, timestamp - this._scrollMetrics.timestamp);
|
||||
if (dt > 500 && this._scrollMetrics.dt > 500 && (contentLength > (5 * visibleLength)) &&
|
||||
!this._hasWarned.perf) {
|
||||
infoLog(
|
||||
'VirtualizedList: You have a large list that is slow to update - make sure ' +
|
||||
'shouldItemUpdate is implemented effectively and consider getItemLayout, PureComponent, ' +
|
||||
'etc.',
|
||||
{dt, prevDt: this._scrollMetrics.dt, contentLength},
|
||||
);
|
||||
this._hasWarned.perf = true;
|
||||
}
|
||||
const dOffset = offset - this._scrollMetrics.offset;
|
||||
const velocity = dOffset / dt;
|
||||
this._scrollMetrics = {contentLength, dt, offset, timestamp, velocity, visibleLength};
|
||||
const {data, getItemCount, onEndReached, onEndReachedThreshold, windowSize} = this.props;
|
||||
this._updateViewableItems(data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const distanceFromEnd = contentLength - visibleLength - offset;
|
||||
const itemCount = getItemCount(data);
|
||||
if (distanceFromEnd < onEndReachedThreshold * visibleLength &&
|
||||
this._scrollMetrics.contentLength !== this._sentEndForContentLength &&
|
||||
this.state.last === itemCount - 1) {
|
||||
// Only call onEndReached for a given content length once.
|
||||
this._sentEndForContentLength = this._scrollMetrics.contentLength;
|
||||
onEndReached({distanceFromEnd});
|
||||
}
|
||||
const {first, last} = this.state;
|
||||
if ((first > 0 && velocity < 0) || (last < itemCount - 1 && velocity > 0)) {
|
||||
const distanceToContentEdge = Math.min(
|
||||
Math.abs(this._getFrameMetricsApprox(first).offset - offset),
|
||||
Math.abs(this._getFrameMetricsApprox(last).offset - (offset + visibleLength)),
|
||||
);
|
||||
const hiPri = distanceToContentEdge < (windowSize * visibleLength / 4);
|
||||
if (hiPri) {
|
||||
// Don't worry about interactions when scrolling quickly; focus on filling content as fast
|
||||
// as possible.
|
||||
this._updateCellsToRenderBatcher.dispose({abort: true});
|
||||
this._updateCellsToRender();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._updateCellsToRenderBatcher.schedule();
|
||||
};
|
||||
|
||||
_updateCellsToRender = () => {
|
||||
const {data, disableVirtualization, getItemCount, onEndReachedThreshold} = this.props;
|
||||
this._updateViewableItems(data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
this.setState((state) => {
|
||||
let newState;
|
||||
if (!disableVirtualization) {
|
||||
newState = computeWindowedRenderLimits(
|
||||
this.props, state, this._getFrameMetricsApprox, this._scrollMetrics,
|
||||
);
|
||||
} else {
|
||||
const {contentLength, offset, visibleLength} = this._scrollMetrics;
|
||||
const distanceFromEnd = contentLength - visibleLength - offset;
|
||||
const renderAhead = distanceFromEnd < onEndReachedThreshold * visibleLength ?
|
||||
this.props.maxToRenderPerBatch : 0;
|
||||
newState = {
|
||||
first: 0,
|
||||
last: Math.min(state.last + renderAhead, getItemCount(data) - 1),
|
||||
};
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
_createViewToken = (index: number, isViewable: boolean): ViewToken => {
|
||||
const {data, getItem, keyExtractor} = this.props;
|
||||
const item = getItem(data, index);
|
||||
invariant(item, 'Missing item for index ' + index);
|
||||
return {index, item, key: keyExtractor(item, index), isViewable};
|
||||
};
|
||||
|
||||
_getFrameMetricsApprox = (index: number): {length: number, offset: number} => {
|
||||
const frame = this._getFrameMetrics(index);
|
||||
if (frame && frame.index === index) { // check for invalid frames due to row re-ordering
|
||||
return frame;
|
||||
} else {
|
||||
const {getItemLayout} = this.props;
|
||||
invariant(
|
||||
!getItemLayout,
|
||||
'Should not have to estimate frames when a measurement metrics function is provided'
|
||||
);
|
||||
return {
|
||||
length: this._averageCellLength,
|
||||
offset: this._averageCellLength * index,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
_getFrameMetrics = (index: number): ?{length: number, offset: number, index: number} => {
|
||||
const {data, getItem, getItemCount, getItemLayout, keyExtractor} = this.props;
|
||||
invariant(getItemCount(data) > index, 'Tried to get frame for out of range index ' + index);
|
||||
const item = getItem(data, index);
|
||||
let frame = item && this._frames[keyExtractor(item, index)];
|
||||
if (!frame || frame.index !== index) {
|
||||
if (getItemLayout) {
|
||||
frame = getItemLayout(data, index);
|
||||
}
|
||||
}
|
||||
return frame;
|
||||
};
|
||||
|
||||
_updateViewableItems(data: any) {
|
||||
const {getItemCount, onViewableItemsChanged} = this.props;
|
||||
if (!onViewableItemsChanged) {
|
||||
return;
|
||||
}
|
||||
this._viewabilityHelper.onUpdate(
|
||||
getItemCount(data),
|
||||
this._scrollMetrics.offset,
|
||||
this._scrollMetrics.visibleLength,
|
||||
this._getFrameMetrics,
|
||||
this._createViewToken,
|
||||
onViewableItemsChanged,
|
||||
this.state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CellRenderer extends React.Component {
|
||||
props: {
|
||||
cellKey: string,
|
||||
index: number,
|
||||
item: Item,
|
||||
onLayout: (event: Object, cellKey: string, index: number) => void,
|
||||
onUnmount: (cellKey: string) => void,
|
||||
parentProps: {
|
||||
renderItem: renderItemType,
|
||||
getItemLayout?: ?Function,
|
||||
shouldItemUpdate: (
|
||||
props: {item: Item, index: number},
|
||||
nextProps: {item: Item, index: number}
|
||||
) => boolean,
|
||||
},
|
||||
};
|
||||
_onLayout = (e) => {
|
||||
this.props.onLayout(e, this.props.cellKey, this.props.index);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.props.onUnmount(this.props.cellKey);
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const curr = {item: this.props.item, index: this.props.index};
|
||||
const next = {item: nextProps.item, index: nextProps.index};
|
||||
return nextProps.parentProps.shouldItemUpdate(curr, next);
|
||||
}
|
||||
render() {
|
||||
const {item, index, parentProps} = this.props;
|
||||
const {renderItem, getItemLayout} = parentProps;
|
||||
invariant(renderItem, 'no renderItem!');
|
||||
const element = renderItem({item, index});
|
||||
if (getItemLayout && !parentProps.debug) {
|
||||
return element;
|
||||
}
|
||||
return (
|
||||
<View onLayout={this._onLayout}>
|
||||
{element}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VirtualizedList;
|
||||
321
Libraries/CustomComponents/Lists/VirtualizedSectionList.js
Normal file
321
Libraries/CustomComponents/Lists/VirtualizedSectionList.js
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
|
||||
* all intellectual property and other proprietary rights, in and to the React
|
||||
* Native CustomComponents software (the "Software"). Subject to your
|
||||
* compliance with these terms, you are hereby granted a non-exclusive,
|
||||
* worldwide, royalty-free copyright license to (1) use and copy the Software;
|
||||
* and (2) reproduce and distribute the Software as part of your own software
|
||||
* ("Your Software"). Facebook reserves all rights not expressly granted to
|
||||
* you in this license agreement.
|
||||
*
|
||||
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
|
||||
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
|
||||
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @providesModule VirtualizedSectionList
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const View = require('View');
|
||||
const VirtualizedList = require('VirtualizedList');
|
||||
|
||||
const invariant = require('invariant');
|
||||
const warning = require('warning');
|
||||
|
||||
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: Array<SectionItem>,
|
||||
key: string,
|
||||
|
||||
// Optional props will override list-wide props just for this section.
|
||||
renderItem?: ?({item: SectionItem, index: number}) => ?React.Element<*>,
|
||||
SeparatorComponent?: ?ReactClass<*>,
|
||||
keyExtractor?: (item: SectionItem) => string,
|
||||
|
||||
// TODO: support more optional/override props
|
||||
// FooterComponent?: ?ReactClass<*>,
|
||||
// HeaderComponent?: ?ReactClass<*>,
|
||||
// onViewableItemsChanged?: ({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
||||
};
|
||||
|
||||
type RequiredProps<SectionT: SectionBase> = {
|
||||
sections: Array<SectionT>,
|
||||
};
|
||||
|
||||
type OptionalProps<SectionT: SectionBase> = {
|
||||
/**
|
||||
* Rendered after the last item in the last section.
|
||||
*/
|
||||
ListFooterComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* Rendered at the very beginning of the list.
|
||||
*/
|
||||
ListHeaderComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* Default renderer for every item in every section.
|
||||
*/
|
||||
renderItem: ({item: Item, index: number}) => ?React.Element<*>,
|
||||
/**
|
||||
* Rendered at the top of each section. In the future, a sticky option will be added.
|
||||
*/
|
||||
renderSectionHeader?: ?({section: SectionT}) => ?React.Element<*>,
|
||||
/**
|
||||
* Rendered at the bottom of every Section, except the very last one, in place of the normal
|
||||
* SeparatorComponent.
|
||||
*/
|
||||
SectionSeparatorComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* Rendered at the bottom of every Item except the very last one in the last section.
|
||||
*/
|
||||
ItemSeparatorComponent?: ?ReactClass<*>,
|
||||
/**
|
||||
* Warning: Virtualization can drastically improve memory consumption for long lists, but trashes
|
||||
* the state of items when they scroll out of the render window, so make sure all relavent data is
|
||||
* stored outside of the recursive `renderItem` instance tree.
|
||||
*/
|
||||
enableVirtualization?: ?boolean,
|
||||
keyExtractor: (item: Item, index: number) => string,
|
||||
onEndReached?: ?({distanceFromEnd: number}) => void,
|
||||
/**
|
||||
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
|
||||
* sure to also set the `refreshing` prop correctly.
|
||||
*/
|
||||
onRefresh?: ?Function,
|
||||
/**
|
||||
* Called when the viewability of rows changes, as defined by the
|
||||
* `viewabilityConfig` prop.
|
||||
*/
|
||||
onViewableItemsChanged?: ?({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
||||
/**
|
||||
* Set this true while waiting for new data from a refresh.
|
||||
*/
|
||||
refreshing?: ?boolean,
|
||||
/**
|
||||
* This is an optional optimization to minimize re-rendering items.
|
||||
*/
|
||||
shouldItemUpdate: (
|
||||
prevProps: {item: Item, index: number},
|
||||
nextProps: {item: Item, index: number}
|
||||
) => boolean,
|
||||
};
|
||||
|
||||
export type Props<SectionT> =
|
||||
RequiredProps<SectionT> &
|
||||
OptionalProps<SectionT> &
|
||||
VirtualizedListProps;
|
||||
|
||||
type DefaultProps = (typeof VirtualizedList.defaultProps) & {data: Array<Item>};
|
||||
type State = {childProps: VirtualizedListProps};
|
||||
|
||||
/**
|
||||
* Right now this just flattens everything into one list and uses VirtualizedList under the
|
||||
* 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<DefaultProps, Props<SectionT>, State>
|
||||
{
|
||||
props: Props<SectionT>;
|
||||
|
||||
state: State;
|
||||
|
||||
static defaultProps: DefaultProps = {
|
||||
...VirtualizedList.defaultProps,
|
||||
data: [],
|
||||
};
|
||||
|
||||
_keyExtractor = (item: Item, index: number) => {
|
||||
const info = this._subExtractor(index);
|
||||
return (info && info.key) || String(index);
|
||||
};
|
||||
|
||||
_subExtractor(
|
||||
index: number,
|
||||
): ?{
|
||||
section: SectionT,
|
||||
key: string, // Key of the section or combined key for section + item
|
||||
index: ?number, // Relative index within the section
|
||||
} {
|
||||
let itemIndex = index;
|
||||
const defaultKeyExtractor = this.props.keyExtractor;
|
||||
for (let ii = 0; ii < this.props.sections.length; ii++) {
|
||||
const section = this.props.sections[ii];
|
||||
const key = section.key;
|
||||
warning(
|
||||
key != null,
|
||||
'VirtualizedSectionList: A `section` you supplied is missing the `key` property.'
|
||||
);
|
||||
itemIndex -= 1; // The section itself is an item
|
||||
if (itemIndex >= section.data.length) {
|
||||
itemIndex -= section.data.length;
|
||||
} else if (itemIndex === -1) {
|
||||
return {section, key, index: null};
|
||||
} else {
|
||||
const keyExtractor = section.keyExtractor || defaultKeyExtractor;
|
||||
return {
|
||||
section,
|
||||
key: key + ':' + keyExtractor(section.data[itemIndex], itemIndex),
|
||||
index: itemIndex,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_convertViewable = (viewable: ViewToken): ?ViewToken => {
|
||||
invariant(viewable.index != null, 'Received a broken ViewToken');
|
||||
const info = this._subExtractor(viewable.index);
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
const keyExtractor = info.section.keyExtractor || this.props.keyExtractor;
|
||||
return {
|
||||
...viewable,
|
||||
index: info.index,
|
||||
key: keyExtractor(viewable.item, info.index),
|
||||
section: info.section,
|
||||
};
|
||||
};
|
||||
|
||||
_onViewableItemsChanged = (
|
||||
{viewableItems, changed}: {viewableItems: Array<ViewToken>, changed: Array<ViewToken>}
|
||||
) => {
|
||||
if (this.props.onViewableItemsChanged) {
|
||||
this.props.onViewableItemsChanged({
|
||||
viewableItems: viewableItems.map(this._convertViewable, this).filter(Boolean),
|
||||
changed: changed.map(this._convertViewable, this).filter(Boolean),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_isItemSticky = (item, index) => {
|
||||
const info = this._subExtractor(index);
|
||||
return info && info.index == null;
|
||||
};
|
||||
|
||||
_renderItem = ({item, index}: {item: Item, index: number}) => {
|
||||
const info = this._subExtractor(index);
|
||||
if (!info) {
|
||||
return null;
|
||||
} else if (info.index == null) {
|
||||
const {renderSectionHeader} = this.props;
|
||||
return renderSectionHeader ? renderSectionHeader({section: info.section}) : null;
|
||||
} else {
|
||||
const renderItem = info.section.renderItem ||
|
||||
this.props.renderItem;
|
||||
const SeparatorComponent = this._getSeparatorComponent(index, info);
|
||||
invariant(renderItem, 'no renderItem!');
|
||||
return (
|
||||
<View>
|
||||
{renderItem({item, index: info.index || 0})}
|
||||
{SeparatorComponent && <SeparatorComponent />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_getSeparatorComponent(index: number, info?: ?Object): ?ReactClass<*> {
|
||||
info = info || this._subExtractor(index);
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
const SeparatorComponent = info.section.SeparatorComponent || this.props.ItemSeparatorComponent;
|
||||
const {SectionSeparatorComponent} = this.props;
|
||||
const isLastItemInList = index === this.state.childProps.getItemCount() - 1;
|
||||
const isLastItemInSection = info.index === info.section.data.length - 1;
|
||||
if (SectionSeparatorComponent && isLastItemInSection && !isLastItemInList) {
|
||||
return SectionSeparatorComponent;
|
||||
}
|
||||
if (SeparatorComponent && !isLastItemInSection && !isLastItemInList) {
|
||||
return SeparatorComponent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_shouldItemUpdate = (prev, next) => {
|
||||
const {shouldItemUpdate} = this.props;
|
||||
if (!shouldItemUpdate || shouldItemUpdate(prev, next)) {
|
||||
return true;
|
||||
}
|
||||
return this._getSeparatorComponent(prev.index) !== this._getSeparatorComponent(next.index);
|
||||
}
|
||||
|
||||
_computeState(props: Props<SectionT>): State {
|
||||
const itemCount = props.sections.reduce((v, section) => v + section.data.length + 1, 0);
|
||||
return {
|
||||
childProps: {
|
||||
...props,
|
||||
FooterComponent: this.props.ListFooterComponent,
|
||||
HeaderComponent: this.props.ListHeaderComponent,
|
||||
renderItem: this._renderItem,
|
||||
SeparatorComponent: undefined, // Rendered with renderItem
|
||||
data: props.sections,
|
||||
getItemCount: () => itemCount,
|
||||
getItem,
|
||||
isItemSticky: this._isItemSticky,
|
||||
keyExtractor: this._keyExtractor,
|
||||
onViewableItemsChanged:
|
||||
props.onViewableItemsChanged ? this._onViewableItemsChanged : undefined,
|
||||
shouldItemUpdate: this._shouldItemUpdate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props: Props<SectionT>, context: Object) {
|
||||
super(props, context);
|
||||
warning(
|
||||
!props.stickySectionHeadersEnabled,
|
||||
'VirtualizedSectionList: Sticky headers only supported with legacyImplementation for now.'
|
||||
);
|
||||
this.state = this._computeState(props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props<SectionT>) {
|
||||
this.setState(this._computeState(nextProps));
|
||||
}
|
||||
|
||||
render() {
|
||||
return <VirtualizedList {...this.state.childProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
function getItem(sections: ?Array<Item>, index: number): ?Item {
|
||||
if (!sections) {
|
||||
return null;
|
||||
}
|
||||
let itemIdx = index - 1;
|
||||
for (let ii = 0; ii < sections.length; ii++) {
|
||||
if (itemIdx === -1) {
|
||||
return sections[ii]; // The section itself is the item
|
||||
} else if (itemIdx < sections[ii].data.length) {
|
||||
return sections[ii].data[itemIdx];
|
||||
} else {
|
||||
itemIdx -= (sections[ii].data.length + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = VirtualizedSectionList;
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const FlatList = require('FlatList');
|
||||
const React = require('react');
|
||||
|
||||
function renderMyListItem(info: {item: {title: string}, index: number}) {
|
||||
return <span />;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testEverythingIsFine() {
|
||||
const data = [{
|
||||
title: 'Title Text',
|
||||
key: 1,
|
||||
}];
|
||||
return <FlatList renderItem={renderMyListItem} data={data} />;
|
||||
},
|
||||
|
||||
testBadDataWithTypicalItem() {
|
||||
// $FlowExpectedError - bad title type 6, should be string
|
||||
const data = [{
|
||||
title: 6,
|
||||
key: 1,
|
||||
}];
|
||||
return <FlatList renderItem={renderMyListItem} data={data} />;
|
||||
},
|
||||
|
||||
testMissingFieldWithTypicalItem() {
|
||||
const data = [{
|
||||
key: 1,
|
||||
}];
|
||||
// $FlowExpectedError - missing title
|
||||
return <FlatList renderItem={renderMyListItem} data={data} />;
|
||||
},
|
||||
|
||||
testGoodDataWithBadCustomRenderItemFunction() {
|
||||
const data = [{
|
||||
widget: 6,
|
||||
key: 1,
|
||||
}];
|
||||
return (
|
||||
<FlatList
|
||||
renderItem={(info) =>
|
||||
// $FlowExpectedError - bad widgetCount type 6, should be Object
|
||||
<span>{info.item.widget.missingProp}</span>
|
||||
}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
testBadRenderItemFunction() {
|
||||
const data = [{
|
||||
title: 'foo',
|
||||
key: 1,
|
||||
}];
|
||||
return [
|
||||
// $FlowExpectedError - title should be inside `item`
|
||||
<FlatList renderItem={(info: {title: string}) => <span /> } data={data} />,
|
||||
// $FlowExpectedError - bad index type string, should be number
|
||||
<FlatList renderItem={(info: {item: any, index: string}) => <span /> } data={data} />,
|
||||
// $FlowExpectedError - bad title type number, should be string
|
||||
<FlatList renderItem={(info: {item: {title: number}}) => <span /> } data={data} />,
|
||||
// EverythingIsFine
|
||||
<FlatList renderItem={(info: {item: {title: string}}) => <span /> } data={data} />,
|
||||
];
|
||||
},
|
||||
|
||||
testOtherBadProps() {
|
||||
return [
|
||||
// $FlowExpectedError - bad numColumns type "lots"
|
||||
<FlatList renderItem={renderMyListItem} data={[]} numColumns="lots" />,
|
||||
// $FlowExpectedError - bad windowSize type "big"
|
||||
<FlatList renderItem={renderMyListItem} data={[]} windowSize="big" />,
|
||||
// $FlowExpectedError - missing `data` prop
|
||||
<FlatList renderItem={renderMyListItem} />,
|
||||
];
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
const SectionList = require('SectionList');
|
||||
|
||||
function renderMyListItem(info: {item: {title: string}, index: number}) {
|
||||
return <span />;
|
||||
}
|
||||
|
||||
const renderMyHeader = ({section}: {section: {fooNumber: number} & Object}) => <span />;
|
||||
|
||||
module.exports = {
|
||||
testGoodDataWithGoodItem() {
|
||||
const sections = [{
|
||||
key: 'a', data: [{
|
||||
title: 'foo',
|
||||
key: 1,
|
||||
}],
|
||||
}];
|
||||
return <SectionList renderItem={renderMyListItem} sections={sections} />;
|
||||
},
|
||||
|
||||
testBadRenderItemFunction() {
|
||||
const sections = [{
|
||||
key: 'a', data: [{
|
||||
title: 'foo',
|
||||
key: 1,
|
||||
}],
|
||||
}];
|
||||
return [
|
||||
// $FlowExpectedError - title should be inside `item`
|
||||
<SectionList renderItem={(info: {title: string}) => <span /> } sections={sections} />,
|
||||
// $FlowExpectedError - bad index type string, should be number
|
||||
<SectionList renderItem={(info: {index: string}) => <span /> } sections={sections} />,
|
||||
// EverythingIsFine
|
||||
<SectionList renderItem={(info: {item: {title: string}}) => <span /> } sections={sections} />,
|
||||
];
|
||||
},
|
||||
|
||||
testBadInheritedDefaultProp(): React.Element<*> {
|
||||
const sections = [];
|
||||
// $FlowExpectedError - bad windowSize type "big"
|
||||
return <SectionList renderItem={renderMyListItem} sections={sections} windowSize="big" />;
|
||||
},
|
||||
|
||||
testMissingData(): React.Element<*> {
|
||||
// $FlowExpectedError - missing `sections` prop
|
||||
return <SectionList renderItem={renderMyListItem} />;
|
||||
},
|
||||
|
||||
testBadSectionsShape(): React.Element<*> {
|
||||
const sections = [{
|
||||
key: 'a', items: [{
|
||||
title: 'foo',
|
||||
key: 1,
|
||||
}],
|
||||
}];
|
||||
// $FlowExpectedError - section missing `data` field
|
||||
return <SectionList renderItem={renderMyListItem} sections={sections} />;
|
||||
},
|
||||
|
||||
testBadSectionsMetadata(): React.Element<*> {
|
||||
// $FlowExpectedError - section has bad meta data `fooNumber` field of type string
|
||||
const sections = [{
|
||||
key: 'a', fooNumber: 'string', data: [{
|
||||
title: 'foo',
|
||||
key: 1,
|
||||
}],
|
||||
}];
|
||||
return (
|
||||
<SectionList
|
||||
renderSectionHeader={renderMyHeader}
|
||||
renderItem={renderMyListItem}
|
||||
sections={sections}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest.unmock('ViewabilityHelper');
|
||||
const ViewabilityHelper = require('ViewabilityHelper');
|
||||
|
||||
let rowFrames;
|
||||
let data;
|
||||
function getFrameMetrics(index: number) {
|
||||
const frame = rowFrames[data[index].key];
|
||||
return {length: frame.height, offset: frame.y};
|
||||
}
|
||||
function createViewToken(index: number, isViewable: boolean) {
|
||||
return {key: data[index].key, isViewable};
|
||||
}
|
||||
|
||||
describe('computeViewableItems', function() {
|
||||
it('returns all 4 entirely visible rows as viewable', function() {
|
||||
const helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 50});
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
b: {y: 50, height: 50},
|
||||
c: {y: 100, height: 50},
|
||||
d: {y: 150, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}];
|
||||
expect(helper.computeViewableItems(data.length, 0, 200, getFrameMetrics))
|
||||
.toEqual([0, 1, 2, 3]);
|
||||
});
|
||||
|
||||
it(
|
||||
'returns top 2 rows as viewable (1. entirely visible and 2. majority)',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 50});
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
b: {y: 50, height: 150},
|
||||
c: {y: 200, height: 50},
|
||||
d: {y: 250, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}];
|
||||
expect(helper.computeViewableItems(data.length, 0, 200, getFrameMetrics))
|
||||
.toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it(
|
||||
'returns only 2nd row as viewable (majority)',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 50});
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
b: {y: 50, height: 150},
|
||||
c: {y: 200, height: 50},
|
||||
d: {y: 250, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}];
|
||||
expect(helper.computeViewableItems(data.length, 25, 200, getFrameMetrics))
|
||||
.toEqual([1]);
|
||||
});
|
||||
|
||||
it(
|
||||
'handles empty input',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 50});
|
||||
rowFrames = {};
|
||||
data = [];
|
||||
expect(helper.computeViewableItems(data.length, 0, 200, getFrameMetrics))
|
||||
.toEqual([]);
|
||||
});
|
||||
|
||||
it(
|
||||
'handles different view area coverage percent thresholds',
|
||||
function() {
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
b: {y: 50, height: 150},
|
||||
c: {y: 200, height: 500},
|
||||
d: {y: 700, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}];
|
||||
|
||||
let helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 0});
|
||||
expect(helper.computeViewableItems(data.length, 0, 50, getFrameMetrics))
|
||||
.toEqual([0]);
|
||||
expect(helper.computeViewableItems(data.length, 1, 50, getFrameMetrics))
|
||||
.toEqual([0, 1]);
|
||||
expect(helper.computeViewableItems(data.length, 199, 50, getFrameMetrics))
|
||||
.toEqual([1, 2]);
|
||||
expect(helper.computeViewableItems(data.length, 250, 50, getFrameMetrics))
|
||||
.toEqual([2]);
|
||||
|
||||
helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 100});
|
||||
expect(helper.computeViewableItems(data.length, 0, 200, getFrameMetrics))
|
||||
.toEqual([0, 1]);
|
||||
expect(helper.computeViewableItems(data.length, 1, 200, getFrameMetrics))
|
||||
.toEqual([1]);
|
||||
expect(helper.computeViewableItems(data.length, 400, 200, getFrameMetrics))
|
||||
.toEqual([2]);
|
||||
expect(helper.computeViewableItems(data.length, 600, 200, getFrameMetrics))
|
||||
.toEqual([3]);
|
||||
|
||||
helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 10});
|
||||
expect(helper.computeViewableItems(data.length, 30, 200, getFrameMetrics))
|
||||
.toEqual([0, 1, 2]);
|
||||
expect(helper.computeViewableItems(data.length, 31, 200, getFrameMetrics))
|
||||
.toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it(
|
||||
'handles different item visible percent thresholds',
|
||||
function() {
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
b: {y: 50, height: 150},
|
||||
c: {y: 200, height: 50},
|
||||
d: {y: 250, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}];
|
||||
let helper = new ViewabilityHelper({itemVisiblePercentThreshold: 0});
|
||||
expect(helper.computeViewableItems(data.length, 0, 50, getFrameMetrics))
|
||||
.toEqual([0]);
|
||||
expect(helper.computeViewableItems(data.length, 1, 50, getFrameMetrics))
|
||||
.toEqual([0, 1]);
|
||||
|
||||
helper = new ViewabilityHelper({itemVisiblePercentThreshold: 100});
|
||||
expect(helper.computeViewableItems(data.length, 0, 250, getFrameMetrics))
|
||||
.toEqual([0, 1, 2]);
|
||||
expect(helper.computeViewableItems(data.length, 1, 250, getFrameMetrics))
|
||||
.toEqual([1, 2]);
|
||||
|
||||
helper = new ViewabilityHelper({itemVisiblePercentThreshold: 10});
|
||||
expect(helper.computeViewableItems(data.length, 184, 20, getFrameMetrics))
|
||||
.toEqual([1]);
|
||||
expect(helper.computeViewableItems(data.length, 185, 20, getFrameMetrics))
|
||||
.toEqual([1, 2]);
|
||||
expect(helper.computeViewableItems(data.length, 186, 20, getFrameMetrics))
|
||||
.toEqual([2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onUpdate', function() {
|
||||
it(
|
||||
'returns 1 visible row as viewable then scrolls away',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper();
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 50},
|
||||
};
|
||||
data = [{key: 'a'}];
|
||||
const onViewableItemsChanged = jest.fn();
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
0,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'a'}],
|
||||
viewableItems: [{isViewable: true, key: 'a'}],
|
||||
});
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
0,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(1); // nothing changed!
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
100,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(2);
|
||||
expect(onViewableItemsChanged.mock.calls[1][0]).toEqual({
|
||||
changed: [{isViewable: false, key: 'a'}],
|
||||
viewableItems: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'returns 1st visible row then 1st and 2nd then just 2nd',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper();
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 200},
|
||||
b: {y: 200, height: 200},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}];
|
||||
const onViewableItemsChanged = jest.fn();
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
0,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'a'}],
|
||||
viewableItems: [{isViewable: true, key: 'a'}],
|
||||
});
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
100,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(2);
|
||||
// Both visible with 100px overlap each
|
||||
expect(onViewableItemsChanged.mock.calls[1][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'b'}],
|
||||
viewableItems: [{isViewable: true, key: 'a'}, {isViewable: true, key: 'b'}],
|
||||
});
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
200,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(3);
|
||||
expect(onViewableItemsChanged.mock.calls[2][0]).toEqual({
|
||||
changed: [{isViewable: false, key: 'a'}],
|
||||
viewableItems: [{isViewable: true, key: 'b'}],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'minViewTime delays callback',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper({minViewTime: 350, viewAreaCoveragePercentThreshold: 0});
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 200},
|
||||
b: {y: 200, height: 200},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}];
|
||||
const onViewableItemsChanged = jest.fn();
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
0,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged).not.toBeCalled();
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'a'}],
|
||||
viewableItems: [{isViewable: true, key: 'a'}],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'minViewTime skips briefly visible items',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper({minViewTime: 350, viewAreaCoveragePercentThreshold: 0});
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 250},
|
||||
b: {y: 250, height: 200},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}];
|
||||
const onViewableItemsChanged = jest.fn();
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
0,
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
300, // scroll past item 'a'
|
||||
200,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'b'}],
|
||||
viewableItems: [{isViewable: true, key: 'b'}],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'waitForInteraction blocks callback until scroll',
|
||||
function() {
|
||||
const helper = new ViewabilityHelper({
|
||||
waitForInteraction: true,
|
||||
viewAreaCoveragePercentThreshold: 0,
|
||||
scrollInteractionFilter: {
|
||||
minimumOffset: 20,
|
||||
},
|
||||
});
|
||||
rowFrames = {
|
||||
a: {y: 0, height: 200},
|
||||
b: {y: 200, height: 200},
|
||||
};
|
||||
data = [{key: 'a'}, {key: 'b'}];
|
||||
const onViewableItemsChanged = jest.fn();
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
0,
|
||||
100,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged).not.toBeCalled();
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
10, // not far enough to meet minimumOffset
|
||||
100,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged).not.toBeCalled();
|
||||
helper.onUpdate(
|
||||
data.length,
|
||||
20,
|
||||
100,
|
||||
getFrameMetrics,
|
||||
createViewToken,
|
||||
onViewableItemsChanged,
|
||||
);
|
||||
expect(onViewableItemsChanged.mock.calls.length).toBe(1);
|
||||
expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({
|
||||
changed: [{isViewable: true, key: 'a'}],
|
||||
viewableItems: [{isViewable: true, key: 'a'}],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest.unmock('VirtualizeUtils');
|
||||
|
||||
const { elementsThatOverlapOffsets, newRangeCount } = require('VirtualizeUtils');
|
||||
|
||||
describe('newRangeCount', function() {
|
||||
it('handles subset', function() {
|
||||
expect(newRangeCount({first: 1, last: 4}, {first: 2, last: 3})).toBe(0);
|
||||
});
|
||||
it('handles forward disjoint set', function() {
|
||||
expect(newRangeCount({first: 1, last: 4}, {first: 6, last: 9})).toBe(4);
|
||||
});
|
||||
it('handles reverse disjoint set', function() {
|
||||
expect(newRangeCount({first: 6, last: 8}, {first: 1, last: 4})).toBe(4);
|
||||
});
|
||||
it('handles superset', function() {
|
||||
expect(newRangeCount({first: 1, last: 4}, {first: 0, last: 5})).toBe(2);
|
||||
});
|
||||
it('handles end extension', function() {
|
||||
expect(newRangeCount({first: 1, last: 4}, {first: 1, last: 8})).toBe(4);
|
||||
});
|
||||
it('handles front extension', function() {
|
||||
expect(newRangeCount({first: 1, last: 4}, {first: 0, last: 4})).toBe(1);
|
||||
});
|
||||
it('handles forward insersect', function() {
|
||||
expect(newRangeCount({first: 1, last: 4}, {first: 3, last: 6})).toBe(2);
|
||||
});
|
||||
it('handles reverse intersect', function() {
|
||||
expect(newRangeCount({first: 3, last: 6}, {first: 1, last: 4})).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('elementsThatOverlapOffsets', function() {
|
||||
it('handles fixed length', function() {
|
||||
const offsets = [0, 250, 350, 450];
|
||||
function getFrameMetrics(index: number) {
|
||||
return {
|
||||
length: 100,
|
||||
offset: (100 * index),
|
||||
};
|
||||
}
|
||||
expect(elementsThatOverlapOffsets(offsets, 100, getFrameMetrics)).toEqual([0, 2, 3, 4]);
|
||||
});
|
||||
it('handles variable length', function() {
|
||||
const offsets = [150, 250, 900];
|
||||
const frames = [
|
||||
{offset: 0, length: 50},
|
||||
{offset: 50, length: 200},
|
||||
{offset: 250, length: 600},
|
||||
{offset: 850, length: 100},
|
||||
{offset: 950, length: 150},
|
||||
];
|
||||
expect(elementsThatOverlapOffsets(offsets, frames.length, (ii) => frames[ii])).toEqual([1,1,3]);
|
||||
});
|
||||
it('handles out of bounds', function() {
|
||||
const offsets = [150, 900];
|
||||
const frames = [
|
||||
{offset: 0, length: 50},
|
||||
{offset: 50, length: 150},
|
||||
{offset: 250, length: 100},
|
||||
];
|
||||
expect(elementsThatOverlapOffsets(offsets, frames.length, (ii) => frames[ii])).toEqual([1]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user