mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-05-10 01:15:00 +08:00
Remove ListView and SwipeableListView from React Native
Summary: This diff removes ListView and SwipeableListView from React Native: * Removes the code and all examples * Removes the exports on `react-native-implementation` but leaves an error message in dev mode only * Uses `deprecated-react-native-listview` for `ListView` and `deprecated-react-native-swipeable-listview` for `SwipeableListView` Both ListView and SwipeableListView are now fully removed from React Native in open source and we will continue to use the deprecated packages internally. Reviewed By: TheSavior Differential Revision: D14181708 fbshipit-source-id: 5030c33791f998567de058fee934449c16fa1d54
This commit is contained in:
committed by
Facebook Github Bot
parent
4ba304dda5
commit
14d9b2d68d
@@ -46,12 +46,10 @@ type State = {|
|
||||
* A container component that renders multiple SwipeableRow's in a FlatList
|
||||
* implementation. This is designed to be a drop-in replacement for the
|
||||
* standard React Native `FlatList`, so use it as if it were a FlatList, but
|
||||
* with extra props, i.e.
|
||||
*
|
||||
* <SwipeableListView renderRow={..} renderQuickActions={..} {..FlatList props} />
|
||||
* with extra props.
|
||||
*
|
||||
* SwipeableRow can be used independently of this component, but the main
|
||||
* benefit of using this component is
|
||||
* benefits of using this component are:
|
||||
*
|
||||
* - It ensures that at most 1 row is swiped open (auto closes others)
|
||||
* - It can bounce the 1st row of the list so users know it's swipeable
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ListView = require('ListView');
|
||||
const React = require('React');
|
||||
const SwipeableListViewDataSource = require('SwipeableListViewDataSource');
|
||||
const SwipeableRow = require('SwipeableRow');
|
||||
|
||||
type ListViewProps = React.ElementConfig<typeof ListView>;
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
...ListViewProps,
|
||||
|
||||
/**
|
||||
* To alert the user that swiping is possible, the first row can bounce
|
||||
* on component mount.
|
||||
*/
|
||||
bounceFirstRowOnMount: boolean,
|
||||
/**
|
||||
* Use `SwipeableListView.getNewDataSource()` to get a data source to use,
|
||||
* then use it just like you would a normal ListView data source
|
||||
*/
|
||||
dataSource: SwipeableListViewDataSource,
|
||||
/**
|
||||
* Maximum distance to open to after a swipe
|
||||
*/
|
||||
maxSwipeDistance:
|
||||
| number
|
||||
| ((rowData: Object, sectionID: string, rowID: string) => number),
|
||||
onScroll?: ?Function,
|
||||
/**
|
||||
* Callback method to render the swipeable view
|
||||
*/
|
||||
renderRow: (
|
||||
rowData: Object,
|
||||
sectionID: string,
|
||||
rowID: string,
|
||||
) => React.Element<any>,
|
||||
/**
|
||||
* Callback method to render the view that will be unveiled on swipe
|
||||
*/
|
||||
renderQuickActions: (
|
||||
rowData: Object,
|
||||
sectionID: string,
|
||||
rowID: string,
|
||||
) => ?React.Element<any>,
|
||||
|}>;
|
||||
|
||||
type State = {|
|
||||
dataSource: Object,
|
||||
|};
|
||||
|
||||
/**
|
||||
* A container component that renders multiple SwipeableRow's in a ListView
|
||||
* implementation. This is designed to be a drop-in replacement for the
|
||||
* standard React Native `ListView`, so use it as if it were a ListView, but
|
||||
* with extra props, i.e.
|
||||
*
|
||||
* let ds = SwipeableListView.getNewDataSource();
|
||||
* ds.cloneWithRowsAndSections(dataBlob, ?sectionIDs, ?rowIDs);
|
||||
* // ..
|
||||
* <SwipeableListView renderRow={..} renderQuickActions={..} {..ListView props} />
|
||||
*
|
||||
* SwipeableRow can be used independently of this component, but the main
|
||||
* benefit of using this component is
|
||||
*
|
||||
* - It ensures that at most 1 row is swiped open (auto closes others)
|
||||
* - It can bounce the 1st row of the list so users know it's swipeable
|
||||
* - More to come
|
||||
*/
|
||||
class SwipeableListView extends React.Component<Props, State> {
|
||||
props: Props;
|
||||
state: State;
|
||||
|
||||
_listViewRef: ?React.Element<any> = null;
|
||||
_shouldBounceFirstRowOnMount: boolean = false;
|
||||
|
||||
static getNewDataSource(): Object {
|
||||
return new SwipeableListViewDataSource({
|
||||
getRowData: (data, sectionID, rowID) => data[sectionID][rowID],
|
||||
getSectionHeaderData: (data, sectionID) => data[sectionID],
|
||||
rowHasChanged: (row1, row2) => row1 !== row2,
|
||||
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
|
||||
});
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
bounceFirstRowOnMount: false,
|
||||
renderQuickActions: () => null,
|
||||
};
|
||||
|
||||
constructor(props: Props, context: any): void {
|
||||
super(props, context);
|
||||
|
||||
this._shouldBounceFirstRowOnMount = this.props.bounceFirstRowOnMount;
|
||||
this.state = {
|
||||
dataSource: this.props.dataSource,
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps: Props): void {
|
||||
if (
|
||||
this.state.dataSource.getDataSource() !==
|
||||
nextProps.dataSource.getDataSource()
|
||||
) {
|
||||
this.setState({
|
||||
dataSource: nextProps.dataSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
return (
|
||||
// $FlowFixMe Found when typing ListView
|
||||
<ListView
|
||||
{...this.props}
|
||||
ref={ref => {
|
||||
// $FlowFixMe Found when typing ListView
|
||||
this._listViewRef = ref;
|
||||
}}
|
||||
dataSource={this.state.dataSource.getDataSource()}
|
||||
onScroll={this._onScroll}
|
||||
renderRow={this._renderRow}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_onScroll = (e): void => {
|
||||
// Close any opens rows on ListView scroll
|
||||
if (this.props.dataSource.getOpenRowID()) {
|
||||
this.setState({
|
||||
dataSource: this.state.dataSource.setOpenRowID(null),
|
||||
});
|
||||
}
|
||||
this.props.onScroll && this.props.onScroll(e);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a work-around to lock vertical `ListView` scrolling on iOS and
|
||||
* mimic Android behaviour. Locking vertical scrolling when horizontal
|
||||
* scrolling is active allows us to significantly improve framerates
|
||||
* (from high 20s to almost consistently 60 fps)
|
||||
*/
|
||||
_setListViewScrollable(value: boolean): void {
|
||||
if (
|
||||
this._listViewRef &&
|
||||
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an
|
||||
* error found when Flow v0.68 was deployed. To see the error delete this
|
||||
* comment and run Flow. */
|
||||
typeof this._listViewRef.setNativeProps === 'function'
|
||||
) {
|
||||
this._listViewRef.setNativeProps({
|
||||
scrollEnabled: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Passing through ListView's getScrollResponder() function
|
||||
getScrollResponder(): ?Object {
|
||||
if (
|
||||
this._listViewRef &&
|
||||
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an
|
||||
* error found when Flow v0.68 was deployed. To see the error delete this
|
||||
* comment and run Flow. */
|
||||
typeof this._listViewRef.getScrollResponder === 'function'
|
||||
) {
|
||||
return this._listViewRef.getScrollResponder();
|
||||
}
|
||||
}
|
||||
|
||||
// This enables rows having variable width slideoutView.
|
||||
_getMaxSwipeDistance(
|
||||
rowData: Object,
|
||||
sectionID: string,
|
||||
rowID: string,
|
||||
): number {
|
||||
if (typeof this.props.maxSwipeDistance === 'function') {
|
||||
return this.props.maxSwipeDistance(rowData, sectionID, rowID);
|
||||
}
|
||||
|
||||
return this.props.maxSwipeDistance;
|
||||
}
|
||||
|
||||
_renderRow = (
|
||||
rowData: Object,
|
||||
sectionID: string,
|
||||
rowID: string,
|
||||
): React.Element<any> => {
|
||||
const slideoutView = this.props.renderQuickActions(
|
||||
rowData,
|
||||
sectionID,
|
||||
rowID,
|
||||
);
|
||||
|
||||
// If renderQuickActions is unspecified or returns falsey, don't allow swipe
|
||||
if (!slideoutView) {
|
||||
return this.props.renderRow(rowData, sectionID, rowID);
|
||||
}
|
||||
|
||||
let shouldBounceOnMount = false;
|
||||
if (this._shouldBounceFirstRowOnMount) {
|
||||
this._shouldBounceFirstRowOnMount = false;
|
||||
shouldBounceOnMount = rowID === this.props.dataSource.getFirstRowID();
|
||||
}
|
||||
|
||||
return (
|
||||
<SwipeableRow
|
||||
slideoutView={slideoutView}
|
||||
isOpen={rowData.id === this.props.dataSource.getOpenRowID()}
|
||||
maxSwipeDistance={this._getMaxSwipeDistance(rowData, sectionID, rowID)}
|
||||
key={rowID}
|
||||
onOpen={() => this._onOpen(rowData.id)}
|
||||
onClose={() => this._onClose(rowData.id)}
|
||||
onSwipeEnd={() => this._setListViewScrollable(true)}
|
||||
onSwipeStart={() => this._setListViewScrollable(false)}
|
||||
shouldBounceOnMount={shouldBounceOnMount}>
|
||||
{this.props.renderRow(rowData, sectionID, rowID)}
|
||||
</SwipeableRow>
|
||||
);
|
||||
};
|
||||
|
||||
_onOpen(rowID: string): void {
|
||||
this.setState({
|
||||
dataSource: this.state.dataSource.setOpenRowID(rowID),
|
||||
});
|
||||
}
|
||||
|
||||
_onClose(rowID: string): void {
|
||||
this.setState({
|
||||
dataSource: this.state.dataSource.setOpenRowID(null),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SwipeableListView;
|
||||
@@ -1,116 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ListViewDataSource = require('ListViewDataSource');
|
||||
|
||||
/**
|
||||
* Data source wrapper around ListViewDataSource to allow for tracking of
|
||||
* which row is swiped open and close opened row(s) when another row is swiped
|
||||
* open.
|
||||
*
|
||||
* See https://github.com/facebook/react-native/pull/5602 for why
|
||||
* ListViewDataSource is not subclassed.
|
||||
*/
|
||||
class SwipeableListViewDataSource {
|
||||
_previousOpenRowID: string;
|
||||
_openRowID: string;
|
||||
|
||||
_dataBlob: any;
|
||||
_dataSource: ListViewDataSource;
|
||||
|
||||
rowIdentities: Array<Array<string>>;
|
||||
sectionIdentities: Array<string>;
|
||||
|
||||
constructor(params: Object) {
|
||||
this._dataSource = new ListViewDataSource({
|
||||
getRowData: params.getRowData,
|
||||
getSectionHeaderData: params.getSectionHeaderData,
|
||||
rowHasChanged: (row1, row2) => {
|
||||
/**
|
||||
* Row needs to be re-rendered if its swiped open/close status is
|
||||
* changed, or its data blob changed.
|
||||
*/
|
||||
return (
|
||||
(row1.id !== this._previousOpenRowID &&
|
||||
row2.id === this._openRowID) ||
|
||||
(row1.id === this._previousOpenRowID &&
|
||||
row2.id !== this._openRowID) ||
|
||||
params.rowHasChanged(row1, row2)
|
||||
);
|
||||
},
|
||||
sectionHeaderHasChanged: params.sectionHeaderHasChanged,
|
||||
});
|
||||
}
|
||||
|
||||
cloneWithRowsAndSections(
|
||||
dataBlob: any,
|
||||
sectionIdentities: ?Array<string>,
|
||||
rowIdentities: ?Array<Array<string>>,
|
||||
): SwipeableListViewDataSource {
|
||||
this._dataSource = this._dataSource.cloneWithRowsAndSections(
|
||||
dataBlob,
|
||||
sectionIdentities,
|
||||
rowIdentities,
|
||||
);
|
||||
|
||||
this._dataBlob = dataBlob;
|
||||
this.rowIdentities = this._dataSource.rowIdentities;
|
||||
this.sectionIdentities = this._dataSource.sectionIdentities;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// For the actual ListView to use
|
||||
getDataSource(): ListViewDataSource {
|
||||
return this._dataSource;
|
||||
}
|
||||
|
||||
getOpenRowID(): ?string {
|
||||
return this._openRowID;
|
||||
}
|
||||
|
||||
getFirstRowID(): ?string {
|
||||
/**
|
||||
* If rowIdentities is specified, find the first data row from there since
|
||||
* we don't want to attempt to bounce section headers. If unspecified, find
|
||||
* the first data row from _dataBlob.
|
||||
*/
|
||||
if (this.rowIdentities) {
|
||||
return this.rowIdentities[0] && this.rowIdentities[0][0];
|
||||
}
|
||||
return Object.keys(this._dataBlob)[0];
|
||||
}
|
||||
|
||||
getLastRowID(): ?string {
|
||||
if (this.rowIdentities && this.rowIdentities.length) {
|
||||
const lastSection = this.rowIdentities[this.rowIdentities.length - 1];
|
||||
if (lastSection && lastSection.length) {
|
||||
return lastSection[lastSection.length - 1];
|
||||
}
|
||||
}
|
||||
return Object.keys(this._dataBlob)[this._dataBlob.length - 1];
|
||||
}
|
||||
|
||||
setOpenRowID(rowID: string): SwipeableListViewDataSource {
|
||||
this._previousOpenRowID = this._openRowID;
|
||||
this._openRowID = rowID;
|
||||
|
||||
this._dataSource = this._dataSource.cloneWithRowsAndSections(
|
||||
this._dataBlob,
|
||||
this.sectionIdentities,
|
||||
this.rowIdentities,
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SwipeableListViewDataSource;
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
const React = require('React');
|
||||
const ListViewDataSource = require('ListViewDataSource');
|
||||
|
||||
// This class is purely a facsimile of ListView so that we can
|
||||
// properly type it with Flow before migrating ListView off of
|
||||
// createReactClass. If there are things missing here that are in
|
||||
// ListView, that is unintentional.
|
||||
class InternalListViewType<Props> extends React.Component<Props> {
|
||||
static DataSource = ListViewDataSource;
|
||||
setNativeProps(props: Object) {}
|
||||
flashScrollIndicators() {}
|
||||
getScrollResponder(): any {}
|
||||
getScrollableNode(): any {}
|
||||
getMetrics(): Object {}
|
||||
scrollTo(...args: Array<mixed>) {}
|
||||
scrollToEnd(options?: ?{animated?: ?boolean}) {}
|
||||
}
|
||||
|
||||
module.exports = InternalListViewType;
|
||||
@@ -1,766 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const InternalListViewType = require('InternalListViewType');
|
||||
const ListViewDataSource = require('ListViewDataSource');
|
||||
const Platform = require('Platform');
|
||||
const React = require('React');
|
||||
const ReactNative = require('ReactNative');
|
||||
const RCTScrollViewManager = require('NativeModules').ScrollViewManager;
|
||||
const ScrollView = require('ScrollView');
|
||||
const ScrollResponder = require('ScrollResponder');
|
||||
const StaticRenderer = require('StaticRenderer');
|
||||
const View = require('View');
|
||||
const cloneReferencedElement = require('react-clone-referenced-element');
|
||||
const createReactClass = require('create-react-class');
|
||||
const isEmpty = require('isEmpty');
|
||||
const merge = require('merge');
|
||||
|
||||
import type {Props as ScrollViewProps} from 'ScrollView';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 1;
|
||||
const DEFAULT_INITIAL_ROWS = 10;
|
||||
const DEFAULT_SCROLL_RENDER_AHEAD = 1000;
|
||||
const DEFAULT_END_REACHED_THRESHOLD = 1000;
|
||||
const DEFAULT_SCROLL_CALLBACK_THROTTLE = 50;
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
...ScrollViewProps,
|
||||
|
||||
/**
|
||||
* An instance of [ListView.DataSource](docs/listviewdatasource.html) to use
|
||||
*/
|
||||
dataSource: ListViewDataSource,
|
||||
/**
|
||||
* (sectionID, rowID, adjacentRowHighlighted) => renderable
|
||||
*
|
||||
* If provided, a renderable component to be rendered as the separator
|
||||
* below each row but not the last row if there is a section header below.
|
||||
* Take a sectionID and rowID of the row above and whether its adjacent row
|
||||
* is highlighted.
|
||||
*/
|
||||
renderSeparator?: ?Function,
|
||||
/**
|
||||
* (rowData, sectionID, rowID, highlightRow) => renderable
|
||||
*
|
||||
* Takes a data entry from the data source and its ids and should return
|
||||
* a renderable component to be rendered as the row. By default the data
|
||||
* is exactly what was put into the data source, but it's also possible to
|
||||
* provide custom extractors. ListView can be notified when a row is
|
||||
* being highlighted by calling `highlightRow(sectionID, rowID)`. This
|
||||
* sets a boolean value of adjacentRowHighlighted in renderSeparator, allowing you
|
||||
* to control the separators above and below the highlighted row. The highlighted
|
||||
* state of a row can be reset by calling highlightRow(null).
|
||||
*/
|
||||
renderRow: Function,
|
||||
/**
|
||||
* How many rows to render on initial component mount. Use this to make
|
||||
* it so that the first screen worth of data appears at one time instead of
|
||||
* over the course of multiple frames.
|
||||
*/
|
||||
initialListSize?: ?number,
|
||||
/**
|
||||
* Called when all rows have been rendered and the list has been scrolled
|
||||
* to within onEndReachedThreshold of the bottom. The native scroll
|
||||
* event is provided.
|
||||
*/
|
||||
onEndReached?: ?Function,
|
||||
/**
|
||||
* Threshold in pixels (virtual, not physical) for calling onEndReached.
|
||||
*/
|
||||
onEndReachedThreshold?: ?number,
|
||||
/**
|
||||
* Number of rows to render per event loop. Note: if your 'rows' are actually
|
||||
* cells, i.e. they don't span the full width of your view (as in the
|
||||
* ListViewGridLayoutExample), you should set the pageSize to be a multiple
|
||||
* of the number of cells per row, otherwise you're likely to see gaps at
|
||||
* the edge of the ListView as new pages are loaded.
|
||||
*/
|
||||
pageSize?: ?number,
|
||||
/**
|
||||
* () => renderable
|
||||
*
|
||||
* The header and footer are always rendered (if these props are provided)
|
||||
* on every render pass. If they are expensive to re-render, wrap them
|
||||
* in StaticContainer or other mechanism as appropriate. Footer is always
|
||||
* at the bottom of the list, and header at the top, on every render pass.
|
||||
* In a horizontal ListView, the header is rendered on the left and the
|
||||
* footer on the right.
|
||||
*/
|
||||
renderFooter?: ?Function,
|
||||
renderHeader?: ?Function,
|
||||
/**
|
||||
* (sectionData, sectionID) => renderable
|
||||
*
|
||||
* If provided, a header is rendered for this section.
|
||||
*/
|
||||
renderSectionHeader?: ?Function,
|
||||
/**
|
||||
* (props) => renderable
|
||||
*
|
||||
* A function that returns the scrollable component in which the list rows
|
||||
* are rendered. Defaults to returning a ScrollView with the given props.
|
||||
*/
|
||||
renderScrollComponent?: ?Function,
|
||||
/**
|
||||
* How early to start rendering rows before they come on screen, in
|
||||
* pixels.
|
||||
*/
|
||||
scrollRenderAheadDistance?: ?number,
|
||||
/**
|
||||
* (visibleRows, changedRows) => void
|
||||
*
|
||||
* Called when the set of visible rows changes. `visibleRows` maps
|
||||
* { sectionID: { rowID: true }} for all the visible rows, and
|
||||
* `changedRows` maps { sectionID: { rowID: true | false }} for the rows
|
||||
* that have changed their visibility, with true indicating visible, and
|
||||
* false indicating the view has moved out of view.
|
||||
*/
|
||||
onChangeVisibleRows?: ?Function,
|
||||
/**
|
||||
* A performance optimization for improving scroll perf of
|
||||
* large lists, used in conjunction with overflow: 'hidden' on the row
|
||||
* containers. This is enabled by default.
|
||||
*/
|
||||
removeClippedSubviews?: ?boolean,
|
||||
/**
|
||||
* Makes the sections headers sticky. The sticky behavior means that it
|
||||
* will scroll with the content at the top of the section until it reaches
|
||||
* the top of the screen, at which point it will stick to the top until it
|
||||
* is pushed off the screen by the next section header. This property is
|
||||
* not supported in conjunction with `horizontal={true}`. Only enabled by
|
||||
* default on iOS because of typical platform standards.
|
||||
*/
|
||||
stickySectionHeadersEnabled?: ?boolean,
|
||||
/**
|
||||
* An array of child indices determining which children get docked to the
|
||||
* top of the screen when scrolling. For example, passing
|
||||
* `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the
|
||||
* top of the scroll view. This property is not supported in conjunction
|
||||
* with `horizontal={true}`.
|
||||
*/
|
||||
stickyHeaderIndices?: ?$ReadOnlyArray<number>,
|
||||
/**
|
||||
* Flag indicating whether empty section headers should be rendered. In the future release
|
||||
* empty section headers will be rendered by default, and the flag will be deprecated.
|
||||
* If empty sections are not desired to be rendered their indices should be excluded from sectionID object.
|
||||
*/
|
||||
enableEmptySections?: ?boolean,
|
||||
|}>;
|
||||
|
||||
/**
|
||||
* DEPRECATED - use one of the new list components, such as [`FlatList`](docs/flatlist.html)
|
||||
* or [`SectionList`](docs/sectionlist.html) for bounded memory use, fewer bugs,
|
||||
* better performance, an easier to use API, and more features. Check out this
|
||||
* [blog post](https://facebook.github.io/react-native/blog/2017/03/13/better-list-views.html)
|
||||
* for more details.
|
||||
*
|
||||
* ListView - A core component designed for efficient display of vertically
|
||||
* scrolling lists of changing data. The minimal API is to create a
|
||||
* [`ListView.DataSource`](docs/listviewdatasource.html), populate it with a simple
|
||||
* array of data blobs, and instantiate a `ListView` component with that data
|
||||
* source and a `renderRow` callback which takes a blob from the data array and
|
||||
* returns a renderable component.
|
||||
*
|
||||
* Minimal example:
|
||||
*
|
||||
* ```
|
||||
* class MyComponent extends Component {
|
||||
* constructor() {
|
||||
* super();
|
||||
* const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
|
||||
* this.state = {
|
||||
* dataSource: ds.cloneWithRows(['row 1', 'row 2']),
|
||||
* };
|
||||
* }
|
||||
*
|
||||
* render() {
|
||||
* return (
|
||||
* <ListView
|
||||
* dataSource={this.state.dataSource}
|
||||
* renderRow={(rowData) => <Text>{rowData}</Text>}
|
||||
* />
|
||||
* );
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ListView also supports more advanced features, including sections with sticky
|
||||
* section headers, header and footer support, callbacks on reaching the end of
|
||||
* the available data (`onEndReached`) and on the set of rows that are visible
|
||||
* in the device viewport change (`onChangeVisibleRows`), and several
|
||||
* performance optimizations.
|
||||
*
|
||||
* There are a few performance operations designed to make ListView scroll
|
||||
* smoothly while dynamically loading potentially very large (or conceptually
|
||||
* infinite) data sets:
|
||||
*
|
||||
* * Only re-render changed rows - the rowHasChanged function provided to the
|
||||
* data source tells the ListView if it needs to re-render a row because the
|
||||
* source data has changed - see ListViewDataSource for more details.
|
||||
*
|
||||
* * Rate-limited row rendering - By default, only one row is rendered per
|
||||
* event-loop (customizable with the `pageSize` prop). This breaks up the
|
||||
* work into smaller chunks to reduce the chance of dropping frames while
|
||||
* rendering rows.
|
||||
*/
|
||||
|
||||
const ListView = createReactClass({
|
||||
displayName: 'ListView',
|
||||
_rafIds: ([]: Array<AnimationFrameID>),
|
||||
_childFrames: ([]: Array<Object>),
|
||||
_sentEndForContentLength: (null: ?number),
|
||||
_scrollComponent: (null: ?React.ElementRef<typeof ScrollView>),
|
||||
_prevRenderedRowsCount: 0,
|
||||
_visibleRows: ({}: Object),
|
||||
scrollProperties: ({}: Object),
|
||||
|
||||
mixins: [ScrollResponder.Mixin],
|
||||
|
||||
statics: {
|
||||
DataSource: ListViewDataSource,
|
||||
},
|
||||
|
||||
/**
|
||||
* Exports some data, e.g. for perf investigations or analytics.
|
||||
*/
|
||||
getMetrics: function() {
|
||||
return {
|
||||
contentLength: this.scrollProperties.contentLength,
|
||||
totalRows: this.props.enableEmptySections
|
||||
? this.props.dataSource.getRowAndSectionCount()
|
||||
: this.props.dataSource.getRowCount(),
|
||||
renderedRows: this.state.curRenderedRowsCount,
|
||||
visibleRows: Object.keys(this._visibleRows).length,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Provides a handle to the underlying scroll responder.
|
||||
* Note that `this._scrollComponent` might not be a `ScrollView`, so we
|
||||
* need to check that it responds to `getScrollResponder` before calling it.
|
||||
*/
|
||||
getScrollResponder: function() {
|
||||
if (this._scrollComponent && this._scrollComponent.getScrollResponder) {
|
||||
return this._scrollComponent.getScrollResponder();
|
||||
}
|
||||
},
|
||||
|
||||
getScrollableNode: function() {
|
||||
if (this._scrollComponent && this._scrollComponent.getScrollableNode) {
|
||||
return this._scrollComponent.getScrollableNode();
|
||||
} else {
|
||||
return ReactNative.findNodeHandle(this._scrollComponent);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Scrolls to a given x, y offset, either immediately or with a smooth animation.
|
||||
*
|
||||
* See `ScrollView#scrollTo`.
|
||||
*/
|
||||
scrollTo: function(...args: any) {
|
||||
if (this._scrollComponent && this._scrollComponent.scrollTo) {
|
||||
this._scrollComponent.scrollTo(...args);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* If this is a vertical ListView scrolls to the bottom.
|
||||
* If this is a horizontal ListView scrolls to the right.
|
||||
*
|
||||
* Use `scrollToEnd({animated: true})` for smooth animated scrolling,
|
||||
* `scrollToEnd({animated: false})` for immediate scrolling.
|
||||
* If no options are passed, `animated` defaults to true.
|
||||
*
|
||||
* See `ScrollView#scrollToEnd`.
|
||||
*/
|
||||
scrollToEnd: function(options?: ?{animated?: boolean}) {
|
||||
if (this._scrollComponent) {
|
||||
if (this._scrollComponent.scrollToEnd) {
|
||||
this._scrollComponent.scrollToEnd(options);
|
||||
} else {
|
||||
console.warn(
|
||||
'The scroll component used by the ListView does not support ' +
|
||||
'scrollToEnd. Check the renderScrollComponent prop of your ListView.',
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays the scroll indicators momentarily.
|
||||
*
|
||||
* @platform ios
|
||||
*/
|
||||
flashScrollIndicators: function() {
|
||||
if (this._scrollComponent && this._scrollComponent.flashScrollIndicators) {
|
||||
this._scrollComponent.flashScrollIndicators();
|
||||
}
|
||||
},
|
||||
|
||||
setNativeProps: function(props: Object) {
|
||||
if (this._scrollComponent) {
|
||||
this._scrollComponent.setNativeProps(props);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* React life cycle hooks.
|
||||
*/
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
initialListSize: DEFAULT_INITIAL_ROWS,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
renderScrollComponent: props => <ScrollView {...props} />,
|
||||
scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD,
|
||||
onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD,
|
||||
stickySectionHeadersEnabled: Platform.OS === 'ios',
|
||||
stickyHeaderIndices: [],
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
curRenderedRowsCount: this.props.initialListSize,
|
||||
highlightedRow: ({}: Object),
|
||||
};
|
||||
},
|
||||
|
||||
getInnerViewNode: function() {
|
||||
return this._scrollComponent && this._scrollComponent.getInnerViewNode();
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
// this data should never trigger a render pass, so don't put in state
|
||||
this.scrollProperties = {
|
||||
visibleLength: null,
|
||||
contentLength: null,
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
this._rafIds = [];
|
||||
this._childFrames = [];
|
||||
this._visibleRows = {};
|
||||
this._prevRenderedRowsCount = 0;
|
||||
this._sentEndForContentLength = null;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._rafIds.forEach(cancelAnimationFrame);
|
||||
this._rafIds = [];
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// do this in animation frame until componentDidMount actually runs after
|
||||
// the component is laid out
|
||||
this._requestAnimationFrame(() => {
|
||||
this._measureAndUpdateScrollProps();
|
||||
});
|
||||
},
|
||||
|
||||
UNSAFE_componentWillReceiveProps: function(nextProps: Object) {
|
||||
if (
|
||||
this.props.dataSource !== nextProps.dataSource ||
|
||||
this.props.initialListSize !== nextProps.initialListSize
|
||||
) {
|
||||
this.setState(
|
||||
(state, props) => {
|
||||
this._prevRenderedRowsCount = 0;
|
||||
return {
|
||||
curRenderedRowsCount: Math.min(
|
||||
Math.max(state.curRenderedRowsCount, props.initialListSize),
|
||||
props.enableEmptySections
|
||||
? props.dataSource.getRowAndSectionCount()
|
||||
: props.dataSource.getRowCount(),
|
||||
),
|
||||
};
|
||||
},
|
||||
() => this._renderMoreRowsIfNeeded(),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
this._requestAnimationFrame(() => {
|
||||
this._measureAndUpdateScrollProps();
|
||||
});
|
||||
},
|
||||
|
||||
_onRowHighlighted: function(sectionID: string, rowID: string) {
|
||||
this.setState({highlightedRow: {sectionID, rowID}});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const bodyComponents = [];
|
||||
|
||||
const dataSource = this.props.dataSource;
|
||||
const allRowIDs = dataSource.rowIdentities;
|
||||
let rowCount = 0;
|
||||
const stickySectionHeaderIndices = [];
|
||||
|
||||
const {renderSectionHeader} = this.props;
|
||||
|
||||
const header = this.props.renderHeader && this.props.renderHeader();
|
||||
const footer = this.props.renderFooter && this.props.renderFooter();
|
||||
let totalIndex = header ? 1 : 0;
|
||||
|
||||
for (let sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) {
|
||||
const sectionID = dataSource.sectionIdentities[sectionIdx];
|
||||
const rowIDs = allRowIDs[sectionIdx];
|
||||
if (rowIDs.length === 0) {
|
||||
if (this.props.enableEmptySections === undefined) {
|
||||
const warning = require('fbjs/lib/warning');
|
||||
warning(
|
||||
false,
|
||||
'In next release empty section headers will be rendered.' +
|
||||
" In this release you can use 'enableEmptySections' flag to render empty section headers.",
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
const invariant = require('invariant');
|
||||
invariant(
|
||||
this.props.enableEmptySections,
|
||||
"In next release 'enableEmptySections' flag will be deprecated, empty section headers will always be rendered." +
|
||||
' If empty section headers are not desirable their indices should be excluded from sectionIDs object.' +
|
||||
" In this release 'enableEmptySections' may only have value 'true' to allow empty section headers rendering.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (renderSectionHeader) {
|
||||
const element = renderSectionHeader(
|
||||
dataSource.getSectionHeaderData(sectionIdx),
|
||||
sectionID,
|
||||
);
|
||||
if (element) {
|
||||
bodyComponents.push(
|
||||
React.cloneElement(element, {key: 's_' + sectionID}),
|
||||
);
|
||||
if (this.props.stickySectionHeadersEnabled) {
|
||||
stickySectionHeaderIndices.push(totalIndex);
|
||||
}
|
||||
totalIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
for (let rowIdx = 0; rowIdx < rowIDs.length; rowIdx++) {
|
||||
const rowID = rowIDs[rowIdx];
|
||||
const comboID = sectionID + '_' + rowID;
|
||||
const shouldUpdateRow =
|
||||
rowCount >= this._prevRenderedRowsCount &&
|
||||
dataSource.rowShouldUpdate(sectionIdx, rowIdx);
|
||||
const row = (
|
||||
<StaticRenderer
|
||||
key={'r_' + comboID}
|
||||
shouldUpdate={!!shouldUpdateRow}
|
||||
render={this.props.renderRow.bind(
|
||||
null,
|
||||
dataSource.getRowData(sectionIdx, rowIdx),
|
||||
sectionID,
|
||||
rowID,
|
||||
this._onRowHighlighted,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
bodyComponents.push(row);
|
||||
totalIndex++;
|
||||
|
||||
if (
|
||||
this.props.renderSeparator &&
|
||||
(rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1)
|
||||
) {
|
||||
const adjacentRowHighlighted =
|
||||
this.state.highlightedRow.sectionID === sectionID &&
|
||||
(this.state.highlightedRow.rowID === rowID ||
|
||||
this.state.highlightedRow.rowID === rowIDs[rowIdx + 1]);
|
||||
const separator = this.props.renderSeparator(
|
||||
sectionID,
|
||||
rowID,
|
||||
adjacentRowHighlighted,
|
||||
);
|
||||
if (separator) {
|
||||
bodyComponents.push(<View key={'s_' + comboID}>{separator}</View>);
|
||||
totalIndex++;
|
||||
}
|
||||
}
|
||||
if (++rowCount === this.state.curRenderedRowsCount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rowCount >= this.state.curRenderedRowsCount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const {renderScrollComponent, ...props} = this.props;
|
||||
if (!props.scrollEventThrottle) {
|
||||
props.scrollEventThrottle = DEFAULT_SCROLL_CALLBACK_THROTTLE;
|
||||
}
|
||||
if (props.removeClippedSubviews === undefined) {
|
||||
props.removeClippedSubviews = true;
|
||||
}
|
||||
Object.assign(props, {
|
||||
onScroll: this._onScroll,
|
||||
stickyHeaderIndices: this.props.stickyHeaderIndices.concat(
|
||||
stickySectionHeaderIndices,
|
||||
),
|
||||
|
||||
// Do not pass these events downstream to ScrollView since they will be
|
||||
// registered in ListView's own ScrollResponder.Mixin
|
||||
onKeyboardWillShow: undefined,
|
||||
onKeyboardWillHide: undefined,
|
||||
onKeyboardDidShow: undefined,
|
||||
onKeyboardDidHide: undefined,
|
||||
});
|
||||
|
||||
return cloneReferencedElement(
|
||||
renderScrollComponent(props),
|
||||
{
|
||||
ref: this._setScrollComponentRef,
|
||||
onContentSizeChange: this._onContentSizeChange,
|
||||
onLayout: this._onLayout,
|
||||
DEPRECATED_sendUpdatedChildFrames:
|
||||
typeof props.onChangeVisibleRows !== undefined,
|
||||
},
|
||||
header,
|
||||
bodyComponents,
|
||||
footer,
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Private methods
|
||||
*/
|
||||
|
||||
_requestAnimationFrame: function(fn: () => void): void {
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
this._rafIds = this._rafIds.filter(id => id !== rafId);
|
||||
fn();
|
||||
});
|
||||
this._rafIds.push(rafId);
|
||||
},
|
||||
|
||||
_measureAndUpdateScrollProps: function() {
|
||||
const scrollComponent = this.getScrollResponder();
|
||||
if (!scrollComponent || !scrollComponent.getInnerViewNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// RCTScrollViewManager.calculateChildFrames is not available on
|
||||
// every platform
|
||||
RCTScrollViewManager &&
|
||||
RCTScrollViewManager.calculateChildFrames &&
|
||||
RCTScrollViewManager.calculateChildFrames(
|
||||
ReactNative.findNodeHandle(scrollComponent),
|
||||
this._updateVisibleRows,
|
||||
);
|
||||
},
|
||||
|
||||
_setScrollComponentRef: function(scrollComponent) {
|
||||
this._scrollComponent = scrollComponent;
|
||||
},
|
||||
|
||||
_onContentSizeChange: function(width: number, height: number) {
|
||||
const contentLength = !this.props.horizontal ? height : width;
|
||||
if (contentLength !== this.scrollProperties.contentLength) {
|
||||
this.scrollProperties.contentLength = contentLength;
|
||||
this._updateVisibleRows();
|
||||
this._renderMoreRowsIfNeeded();
|
||||
}
|
||||
this.props.onContentSizeChange &&
|
||||
this.props.onContentSizeChange(width, height);
|
||||
},
|
||||
|
||||
_onLayout: function(event: Object) {
|
||||
const {width, height} = event.nativeEvent.layout;
|
||||
const visibleLength = !this.props.horizontal ? height : width;
|
||||
if (visibleLength !== this.scrollProperties.visibleLength) {
|
||||
this.scrollProperties.visibleLength = visibleLength;
|
||||
this._updateVisibleRows();
|
||||
this._renderMoreRowsIfNeeded();
|
||||
}
|
||||
this.props.onLayout && this.props.onLayout(event);
|
||||
},
|
||||
|
||||
_maybeCallOnEndReached: function(event?: Object) {
|
||||
if (
|
||||
this.props.onEndReached &&
|
||||
this.scrollProperties.contentLength !== this._sentEndForContentLength &&
|
||||
this._getDistanceFromEnd(this.scrollProperties) <
|
||||
this.props.onEndReachedThreshold &&
|
||||
this.state.curRenderedRowsCount ===
|
||||
(this.props.enableEmptySections
|
||||
? this.props.dataSource.getRowAndSectionCount()
|
||||
: this.props.dataSource.getRowCount())
|
||||
) {
|
||||
this._sentEndForContentLength = this.scrollProperties.contentLength;
|
||||
this.props.onEndReached(event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_renderMoreRowsIfNeeded: function() {
|
||||
if (
|
||||
this.scrollProperties.contentLength === null ||
|
||||
this.scrollProperties.visibleLength === null ||
|
||||
this.state.curRenderedRowsCount ===
|
||||
(this.props.enableEmptySections
|
||||
? this.props.dataSource.getRowAndSectionCount()
|
||||
: this.props.dataSource.getRowCount())
|
||||
) {
|
||||
this._maybeCallOnEndReached();
|
||||
return;
|
||||
}
|
||||
|
||||
const distanceFromEnd = this._getDistanceFromEnd(this.scrollProperties);
|
||||
if (distanceFromEnd < this.props.scrollRenderAheadDistance) {
|
||||
this._pageInNewRows();
|
||||
}
|
||||
},
|
||||
|
||||
_pageInNewRows: function() {
|
||||
this.setState(
|
||||
(state, props) => {
|
||||
const rowsToRender = Math.min(
|
||||
state.curRenderedRowsCount + props.pageSize,
|
||||
props.enableEmptySections
|
||||
? props.dataSource.getRowAndSectionCount()
|
||||
: props.dataSource.getRowCount(),
|
||||
);
|
||||
this._prevRenderedRowsCount = state.curRenderedRowsCount;
|
||||
return {
|
||||
curRenderedRowsCount: rowsToRender,
|
||||
};
|
||||
},
|
||||
() => {
|
||||
this._measureAndUpdateScrollProps();
|
||||
this._prevRenderedRowsCount = this.state.curRenderedRowsCount;
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
_getDistanceFromEnd: function(scrollProperties: Object) {
|
||||
return (
|
||||
scrollProperties.contentLength -
|
||||
scrollProperties.visibleLength -
|
||||
scrollProperties.offset
|
||||
);
|
||||
},
|
||||
|
||||
_updateVisibleRows: function(updatedFrames?: Array<Object>) {
|
||||
if (!this.props.onChangeVisibleRows) {
|
||||
return; // No need to compute visible rows if there is no callback
|
||||
}
|
||||
if (updatedFrames) {
|
||||
updatedFrames.forEach(newFrame => {
|
||||
this._childFrames[newFrame.index] = merge(newFrame);
|
||||
});
|
||||
}
|
||||
const isVertical = !this.props.horizontal;
|
||||
const dataSource = this.props.dataSource;
|
||||
const visibleMin = this.scrollProperties.offset;
|
||||
const visibleMax = visibleMin + this.scrollProperties.visibleLength;
|
||||
const allRowIDs = dataSource.rowIdentities;
|
||||
|
||||
const header = this.props.renderHeader && this.props.renderHeader();
|
||||
let totalIndex = header ? 1 : 0;
|
||||
let visibilityChanged = false;
|
||||
const changedRows = {};
|
||||
for (let sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) {
|
||||
const rowIDs = allRowIDs[sectionIdx];
|
||||
if (rowIDs.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const sectionID = dataSource.sectionIdentities[sectionIdx];
|
||||
if (this.props.renderSectionHeader) {
|
||||
totalIndex++;
|
||||
}
|
||||
let visibleSection = this._visibleRows[sectionID];
|
||||
if (!visibleSection) {
|
||||
visibleSection = {};
|
||||
}
|
||||
for (let rowIdx = 0; rowIdx < rowIDs.length; rowIdx++) {
|
||||
const rowID = rowIDs[rowIdx];
|
||||
const frame = this._childFrames[totalIndex];
|
||||
totalIndex++;
|
||||
if (
|
||||
this.props.renderSeparator &&
|
||||
(rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1)
|
||||
) {
|
||||
totalIndex++;
|
||||
}
|
||||
if (!frame) {
|
||||
break;
|
||||
}
|
||||
const rowVisible = visibleSection[rowID];
|
||||
const min = isVertical ? frame.y : frame.x;
|
||||
const max = min + (isVertical ? frame.height : frame.width);
|
||||
if ((!min && !max) || min === max) {
|
||||
break;
|
||||
}
|
||||
if (min > visibleMax || max < visibleMin) {
|
||||
if (rowVisible) {
|
||||
visibilityChanged = true;
|
||||
delete visibleSection[rowID];
|
||||
if (!changedRows[sectionID]) {
|
||||
changedRows[sectionID] = {};
|
||||
}
|
||||
changedRows[sectionID][rowID] = false;
|
||||
}
|
||||
} else if (!rowVisible) {
|
||||
visibilityChanged = true;
|
||||
visibleSection[rowID] = true;
|
||||
if (!changedRows[sectionID]) {
|
||||
changedRows[sectionID] = {};
|
||||
}
|
||||
changedRows[sectionID][rowID] = true;
|
||||
}
|
||||
}
|
||||
if (!isEmpty(visibleSection)) {
|
||||
this._visibleRows[sectionID] = visibleSection;
|
||||
} else if (this._visibleRows[sectionID]) {
|
||||
delete this._visibleRows[sectionID];
|
||||
}
|
||||
}
|
||||
visibilityChanged &&
|
||||
this.props.onChangeVisibleRows(this._visibleRows, changedRows);
|
||||
},
|
||||
|
||||
_onScroll: function(e: Object) {
|
||||
const isVertical = !this.props.horizontal;
|
||||
this.scrollProperties.visibleLength =
|
||||
e.nativeEvent.layoutMeasurement[isVertical ? 'height' : 'width'];
|
||||
this.scrollProperties.contentLength =
|
||||
e.nativeEvent.contentSize[isVertical ? 'height' : 'width'];
|
||||
this.scrollProperties.offset =
|
||||
e.nativeEvent.contentOffset[isVertical ? 'y' : 'x'];
|
||||
this._updateVisibleRows(e.nativeEvent.updatedChildFrames);
|
||||
if (!this._maybeCallOnEndReached(e)) {
|
||||
this._renderMoreRowsIfNeeded();
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.onEndReached &&
|
||||
this._getDistanceFromEnd(this.scrollProperties) >
|
||||
this.props.onEndReachedThreshold
|
||||
) {
|
||||
// Scrolled out of the end zone, so it should be able to trigger again.
|
||||
this._sentEndForContentLength = null;
|
||||
}
|
||||
|
||||
this.props.onScroll && this.props.onScroll(e);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = ((ListView: any): Class<InternalListViewType<Props>>);
|
||||
@@ -1,428 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const invariant = require('invariant');
|
||||
const isEmpty = require('isEmpty');
|
||||
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
|
||||
* found when Flow v0.54 was deployed. To see the error delete this comment and
|
||||
* run Flow. */
|
||||
const warning = require('fbjs/lib/warning');
|
||||
|
||||
function defaultGetRowData(
|
||||
dataBlob: any,
|
||||
sectionID: number | string,
|
||||
rowID: number | string,
|
||||
): any {
|
||||
return dataBlob[sectionID][rowID];
|
||||
}
|
||||
|
||||
function defaultGetSectionHeaderData(
|
||||
dataBlob: any,
|
||||
sectionID: number | string,
|
||||
): any {
|
||||
return dataBlob[sectionID];
|
||||
}
|
||||
|
||||
type differType = (data1: any, data2: any) => boolean;
|
||||
|
||||
type ParamType = {
|
||||
rowHasChanged: differType,
|
||||
getRowData?: ?typeof defaultGetRowData,
|
||||
sectionHeaderHasChanged?: ?differType,
|
||||
getSectionHeaderData?: ?typeof defaultGetSectionHeaderData,
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides efficient data processing and access to the
|
||||
* `ListView` component. A `ListViewDataSource` is created with functions for
|
||||
* extracting data from the input blob, and comparing elements (with default
|
||||
* implementations for convenience). The input blob can be as simple as an
|
||||
* array of strings, or an object with rows nested inside section objects.
|
||||
*
|
||||
* To update the data in the datasource, use `cloneWithRows` (or
|
||||
* `cloneWithRowsAndSections` if you care about sections). The data in the
|
||||
* data source is immutable, so you can't modify it directly. The clone methods
|
||||
* suck in the new data and compute a diff for each row so ListView knows
|
||||
* whether to re-render it or not.
|
||||
*
|
||||
* In this example, a component receives data in chunks, handled by
|
||||
* `_onDataArrived`, which concats the new data onto the old data and updates the
|
||||
* data source. We use `concat` to create a new array - mutating `this._data`,
|
||||
* e.g. with `this._data.push(newRowData)`, would be an error. `_rowHasChanged`
|
||||
* understands the shape of the row data and knows how to efficiently compare
|
||||
* it.
|
||||
*
|
||||
* ```
|
||||
* getInitialState: function() {
|
||||
* const ds = new ListView.DataSource({rowHasChanged: this._rowHasChanged});
|
||||
* return {ds};
|
||||
* },
|
||||
* _onDataArrived(newData) {
|
||||
* this._data = this._data.concat(newData);
|
||||
* this.setState({
|
||||
* ds: this.state.ds.cloneWithRows(this._data)
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
class ListViewDataSource {
|
||||
/**
|
||||
* You can provide custom extraction and `hasChanged` functions for section
|
||||
* headers and rows. If absent, data will be extracted with the
|
||||
* `defaultGetRowData` and `defaultGetSectionHeaderData` functions.
|
||||
*
|
||||
* The default extractor expects data of one of the following forms:
|
||||
*
|
||||
* { sectionID_1: { rowID_1: <rowData1>, ... }, ... }
|
||||
*
|
||||
* or
|
||||
*
|
||||
* { sectionID_1: [ <rowData1>, <rowData2>, ... ], ... }
|
||||
*
|
||||
* or
|
||||
*
|
||||
* [ [ <rowData1>, <rowData2>, ... ], ... ]
|
||||
*
|
||||
* The constructor takes in a params argument that can contain any of the
|
||||
* following:
|
||||
*
|
||||
* - getRowData(dataBlob, sectionID, rowID);
|
||||
* - getSectionHeaderData(dataBlob, sectionID);
|
||||
* - rowHasChanged(prevRowData, nextRowData);
|
||||
* - sectionHeaderHasChanged(prevSectionData, nextSectionData);
|
||||
*/
|
||||
constructor(params: ParamType) {
|
||||
invariant(
|
||||
params && typeof params.rowHasChanged === 'function',
|
||||
'Must provide a rowHasChanged function.',
|
||||
);
|
||||
this._rowHasChanged = params.rowHasChanged;
|
||||
this._getRowData = params.getRowData || defaultGetRowData;
|
||||
this._sectionHeaderHasChanged = params.sectionHeaderHasChanged;
|
||||
this._getSectionHeaderData =
|
||||
params.getSectionHeaderData || defaultGetSectionHeaderData;
|
||||
|
||||
this._dataBlob = null;
|
||||
this._dirtyRows = [];
|
||||
this._dirtySections = [];
|
||||
this._cachedRowCount = 0;
|
||||
|
||||
// These two private variables are accessed by outsiders because ListView
|
||||
// uses them to iterate over the data in this class.
|
||||
this.rowIdentities = [];
|
||||
this.sectionIdentities = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones this `ListViewDataSource` with the specified `dataBlob` and
|
||||
* `rowIdentities`. The `dataBlob` is just an arbitrary blob of data. At
|
||||
* construction an extractor to get the interesting information was defined
|
||||
* (or the default was used).
|
||||
*
|
||||
* The `rowIdentities` is a 2D array of identifiers for rows.
|
||||
* ie. [['a1', 'a2'], ['b1', 'b2', 'b3'], ...]. If not provided, it's
|
||||
* assumed that the keys of the section data are the row identities.
|
||||
*
|
||||
* Note: This function does NOT clone the data in this data source. It simply
|
||||
* passes the functions defined at construction to a new data source with
|
||||
* the data specified. If you wish to maintain the existing data you must
|
||||
* handle merging of old and new data separately and then pass that into
|
||||
* this function as the `dataBlob`.
|
||||
*/
|
||||
cloneWithRows(
|
||||
dataBlob: $ReadOnlyArray<any> | {+[key: string]: any},
|
||||
rowIdentities: ?$ReadOnlyArray<string>,
|
||||
): ListViewDataSource {
|
||||
const rowIds = rowIdentities ? [[...rowIdentities]] : null;
|
||||
if (!this._sectionHeaderHasChanged) {
|
||||
this._sectionHeaderHasChanged = () => false;
|
||||
}
|
||||
return this.cloneWithRowsAndSections({s1: dataBlob}, ['s1'], rowIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* This performs the same function as the `cloneWithRows` function but here
|
||||
* you also specify what your `sectionIdentities` are. If you don't care
|
||||
* about sections you should safely be able to use `cloneWithRows`.
|
||||
*
|
||||
* `sectionIdentities` is an array of identifiers for sections.
|
||||
* ie. ['s1', 's2', ...]. The identifiers should correspond to the keys or array indexes
|
||||
* of the data you wish to include. If not provided, it's assumed that the
|
||||
* keys of dataBlob are the section identities.
|
||||
*
|
||||
* Note: this returns a new object!
|
||||
*
|
||||
* ```
|
||||
* const dataSource = ds.cloneWithRowsAndSections({
|
||||
* addresses: ['row 1', 'row 2'],
|
||||
* phone_numbers: ['data 1', 'data 2'],
|
||||
* }, ['phone_numbers']);
|
||||
* ```
|
||||
*/
|
||||
cloneWithRowsAndSections(
|
||||
dataBlob: any,
|
||||
sectionIdentities: ?Array<string>,
|
||||
rowIdentities: ?Array<Array<string>>,
|
||||
): ListViewDataSource {
|
||||
invariant(
|
||||
typeof this._sectionHeaderHasChanged === 'function',
|
||||
'Must provide a sectionHeaderHasChanged function with section data.',
|
||||
);
|
||||
invariant(
|
||||
!sectionIdentities ||
|
||||
!rowIdentities ||
|
||||
sectionIdentities.length === rowIdentities.length,
|
||||
'row and section ids lengths must be the same',
|
||||
);
|
||||
|
||||
const newSource = new ListViewDataSource({
|
||||
getRowData: this._getRowData,
|
||||
getSectionHeaderData: this._getSectionHeaderData,
|
||||
rowHasChanged: this._rowHasChanged,
|
||||
sectionHeaderHasChanged: this._sectionHeaderHasChanged,
|
||||
});
|
||||
newSource._dataBlob = dataBlob;
|
||||
if (sectionIdentities) {
|
||||
newSource.sectionIdentities = sectionIdentities;
|
||||
} else {
|
||||
newSource.sectionIdentities = Object.keys(dataBlob);
|
||||
}
|
||||
if (rowIdentities) {
|
||||
newSource.rowIdentities = rowIdentities;
|
||||
} else {
|
||||
newSource.rowIdentities = [];
|
||||
newSource.sectionIdentities.forEach(sectionID => {
|
||||
newSource.rowIdentities.push(Object.keys(dataBlob[sectionID]));
|
||||
});
|
||||
}
|
||||
newSource._cachedRowCount = countRows(newSource.rowIdentities);
|
||||
|
||||
newSource._calculateDirtyArrays(
|
||||
this._dataBlob,
|
||||
this.sectionIdentities,
|
||||
this.rowIdentities,
|
||||
);
|
||||
|
||||
return newSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of rows in the data source.
|
||||
*
|
||||
* If you are specifying the rowIdentities or sectionIdentities, then `getRowCount` will return the number of rows in the filtered data source.
|
||||
*/
|
||||
getRowCount(): number {
|
||||
return this._cachedRowCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of rows in the data source (see `getRowCount` for how this is calculated) plus the number of sections in the data.
|
||||
*
|
||||
* If you are specifying the rowIdentities or sectionIdentities, then `getRowAndSectionCount` will return the number of rows & sections in the filtered data source.
|
||||
*/
|
||||
getRowAndSectionCount(): number {
|
||||
return this._cachedRowCount + this.sectionIdentities.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the row is dirtied and needs to be rerendered
|
||||
*/
|
||||
rowShouldUpdate(sectionIndex: number, rowIndex: number): boolean {
|
||||
const needsUpdate = this._dirtyRows[sectionIndex][rowIndex];
|
||||
warning(
|
||||
needsUpdate !== undefined,
|
||||
'missing dirtyBit for section, row: ' + sectionIndex + ', ' + rowIndex,
|
||||
);
|
||||
return needsUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data required to render the row.
|
||||
*/
|
||||
getRowData(sectionIndex: number, rowIndex: number): any {
|
||||
const sectionID = this.sectionIdentities[sectionIndex];
|
||||
const rowID = this.rowIdentities[sectionIndex][rowIndex];
|
||||
warning(
|
||||
sectionID !== undefined && rowID !== undefined,
|
||||
'rendering invalid section, row: ' + sectionIndex + ', ' + rowIndex,
|
||||
);
|
||||
return this._getRowData(this._dataBlob, sectionID, rowID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the rowID at index provided if the dataSource arrays were flattened,
|
||||
* or null of out of range indexes.
|
||||
*/
|
||||
getRowIDForFlatIndex(index: number): ?string {
|
||||
let accessIndex = index;
|
||||
for (let ii = 0; ii < this.sectionIdentities.length; ii++) {
|
||||
if (accessIndex >= this.rowIdentities[ii].length) {
|
||||
accessIndex -= this.rowIdentities[ii].length;
|
||||
} else {
|
||||
return this.rowIdentities[ii][accessIndex];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sectionID at index provided if the dataSource arrays were flattened,
|
||||
* or null for out of range indexes.
|
||||
*/
|
||||
getSectionIDForFlatIndex(index: number): ?string {
|
||||
let accessIndex = index;
|
||||
for (let ii = 0; ii < this.sectionIdentities.length; ii++) {
|
||||
if (accessIndex >= this.rowIdentities[ii].length) {
|
||||
accessIndex -= this.rowIdentities[ii].length;
|
||||
} else {
|
||||
return this.sectionIdentities[ii];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the number of rows in each section
|
||||
*/
|
||||
getSectionLengths(): Array<number> {
|
||||
const results = [];
|
||||
for (let ii = 0; ii < this.sectionIdentities.length; ii++) {
|
||||
results.push(this.rowIdentities[ii].length);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the section header is dirtied and needs to be rerendered
|
||||
*/
|
||||
sectionHeaderShouldUpdate(sectionIndex: number): boolean {
|
||||
const needsUpdate = this._dirtySections[sectionIndex];
|
||||
warning(
|
||||
needsUpdate !== undefined,
|
||||
'missing dirtyBit for section: ' + sectionIndex,
|
||||
);
|
||||
return needsUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data required to render the section header
|
||||
*/
|
||||
getSectionHeaderData(sectionIndex: number): any {
|
||||
if (!this._getSectionHeaderData) {
|
||||
return null;
|
||||
}
|
||||
const sectionID = this.sectionIdentities[sectionIndex];
|
||||
warning(
|
||||
sectionID !== undefined,
|
||||
'renderSection called on invalid section: ' + sectionIndex,
|
||||
);
|
||||
return this._getSectionHeaderData(this._dataBlob, sectionID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private members and methods.
|
||||
*/
|
||||
|
||||
_getRowData: typeof defaultGetRowData;
|
||||
_getSectionHeaderData: typeof defaultGetSectionHeaderData;
|
||||
_rowHasChanged: differType;
|
||||
_sectionHeaderHasChanged: ?differType;
|
||||
|
||||
_dataBlob: any;
|
||||
_dirtyRows: Array<Array<boolean>>;
|
||||
_dirtySections: Array<boolean>;
|
||||
_cachedRowCount: number;
|
||||
|
||||
// These two 'protected' variables are accessed by ListView to iterate over
|
||||
// the data in this class.
|
||||
rowIdentities: Array<Array<string>>;
|
||||
sectionIdentities: Array<string>;
|
||||
|
||||
_calculateDirtyArrays(
|
||||
prevDataBlob: any,
|
||||
prevSectionIDs: Array<string>,
|
||||
prevRowIDs: Array<Array<string>>,
|
||||
): void {
|
||||
// construct a hashmap of the existing (old) id arrays
|
||||
const prevSectionsHash = keyedDictionaryFromArray(prevSectionIDs);
|
||||
const prevRowsHash = {};
|
||||
for (let ii = 0; ii < prevRowIDs.length; ii++) {
|
||||
const sectionID = prevSectionIDs[ii];
|
||||
warning(
|
||||
!prevRowsHash[sectionID],
|
||||
'SectionID appears more than once: ' + sectionID,
|
||||
);
|
||||
prevRowsHash[sectionID] = keyedDictionaryFromArray(prevRowIDs[ii]);
|
||||
}
|
||||
|
||||
// compare the 2 identity array and get the dirtied rows
|
||||
this._dirtySections = [];
|
||||
this._dirtyRows = [];
|
||||
|
||||
let dirty;
|
||||
for (let sIndex = 0; sIndex < this.sectionIdentities.length; sIndex++) {
|
||||
const sectionID = this.sectionIdentities[sIndex];
|
||||
// dirty if the sectionHeader is new or _sectionHasChanged is true
|
||||
dirty = !prevSectionsHash[sectionID];
|
||||
const sectionHeaderHasChanged = this._sectionHeaderHasChanged;
|
||||
if (!dirty && sectionHeaderHasChanged) {
|
||||
dirty = sectionHeaderHasChanged(
|
||||
this._getSectionHeaderData(prevDataBlob, sectionID),
|
||||
this._getSectionHeaderData(this._dataBlob, sectionID),
|
||||
);
|
||||
}
|
||||
this._dirtySections.push(!!dirty);
|
||||
|
||||
this._dirtyRows[sIndex] = [];
|
||||
for (
|
||||
let rIndex = 0;
|
||||
rIndex < this.rowIdentities[sIndex].length;
|
||||
rIndex++
|
||||
) {
|
||||
const rowID = this.rowIdentities[sIndex][rIndex];
|
||||
// dirty if the section is new, row is new or _rowHasChanged is true
|
||||
dirty =
|
||||
!prevSectionsHash[sectionID] ||
|
||||
!prevRowsHash[sectionID][rowID] ||
|
||||
this._rowHasChanged(
|
||||
this._getRowData(prevDataBlob, sectionID, rowID),
|
||||
this._getRowData(this._dataBlob, sectionID, rowID),
|
||||
);
|
||||
this._dirtyRows[sIndex].push(!!dirty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function countRows(allRowIDs) {
|
||||
let totalRows = 0;
|
||||
for (let sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) {
|
||||
const rowIDs = allRowIDs[sectionIdx];
|
||||
totalRows += rowIDs.length;
|
||||
}
|
||||
return totalRows;
|
||||
}
|
||||
|
||||
function keyedDictionaryFromArray(arr) {
|
||||
if (isEmpty(arr)) {
|
||||
return {};
|
||||
}
|
||||
const result = {};
|
||||
for (let ii = 0; ii < arr.length; ii++) {
|
||||
const key = arr[ii];
|
||||
warning(!result[key], 'Value appears more than once in array: ' + key);
|
||||
result[key] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = ListViewDataSource;
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const ListViewDataSource = require('ListViewDataSource');
|
||||
const React = require('React');
|
||||
const ScrollView = require('ScrollView');
|
||||
const StaticRenderer = require('StaticRenderer');
|
||||
|
||||
class ListViewMock extends React.Component<$FlowFixMeProps> {
|
||||
static latestRef: ?ListViewMock;
|
||||
static defaultProps = {
|
||||
/* $FlowFixMe(>=0.59.0 site=react_native_fb) This comment suppresses an
|
||||
* error caught by Flow 0.59 which was not caught before. Most likely, this
|
||||
* error is because an exported function parameter is missing an
|
||||
* annotation. Without an annotation, these parameters are uncovered by
|
||||
* Flow. */
|
||||
renderScrollComponent: props => <ScrollView {...props} />,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
ListViewMock.latestRef = this;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {dataSource, renderFooter, renderHeader} = this.props;
|
||||
let rows = [
|
||||
renderHeader && (
|
||||
<StaticRenderer
|
||||
key="renderHeader"
|
||||
shouldUpdate={true}
|
||||
render={renderHeader}
|
||||
/>
|
||||
),
|
||||
];
|
||||
|
||||
const dataSourceRows = dataSource.rowIdentities.map(
|
||||
(rowIdentity, rowIdentityIndex) => {
|
||||
const sectionID = dataSource.sectionIdentities[rowIdentityIndex];
|
||||
return rowIdentity.map((row, rowIndex) => (
|
||||
<StaticRenderer
|
||||
key={'section_' + sectionID + '_row_' + rowIndex}
|
||||
shouldUpdate={true}
|
||||
render={this.props.renderRow.bind(
|
||||
null,
|
||||
dataSource.getRowData(rowIdentityIndex, rowIndex),
|
||||
sectionID,
|
||||
row,
|
||||
)}
|
||||
/>
|
||||
));
|
||||
},
|
||||
);
|
||||
|
||||
rows = [...rows, ...dataSourceRows];
|
||||
renderFooter &&
|
||||
rows.push(
|
||||
<StaticRenderer
|
||||
key="renderFooter"
|
||||
shouldUpdate={true}
|
||||
render={renderFooter}
|
||||
/>,
|
||||
);
|
||||
|
||||
return this.props.renderScrollComponent({...this.props, children: rows});
|
||||
}
|
||||
static DataSource = ListViewDataSource;
|
||||
}
|
||||
|
||||
module.exports = ListViewMock;
|
||||
@@ -71,14 +71,6 @@ module.exports = {
|
||||
get KeyboardAvoidingView() {
|
||||
return require('KeyboardAvoidingView');
|
||||
},
|
||||
get ListView() {
|
||||
warnOnce(
|
||||
'listview-deprecation',
|
||||
'ListView is deprecated and will be removed in a future release. ' +
|
||||
'See https://fb.me/nolistview for more information',
|
||||
);
|
||||
return require('ListView');
|
||||
},
|
||||
get MaskedViewIOS() {
|
||||
warnOnce(
|
||||
'maskedviewios-moved',
|
||||
@@ -136,14 +128,6 @@ module.exports = {
|
||||
get SwipeableFlatList() {
|
||||
return require('SwipeableFlatList');
|
||||
},
|
||||
get SwipeableListView() {
|
||||
warnOnce(
|
||||
'swipablelistview-deprecation',
|
||||
'ListView and SwipeableListView are deprecated and will be removed in a future release. ' +
|
||||
'See https://fb.me/nolistview for more information',
|
||||
);
|
||||
return require('SwipeableListView');
|
||||
},
|
||||
get Text() {
|
||||
return require('Text');
|
||||
},
|
||||
@@ -374,3 +358,31 @@ module.exports = {
|
||||
return require('DeprecatedViewPropTypes');
|
||||
},
|
||||
};
|
||||
|
||||
if (__DEV__) {
|
||||
// $FlowFixMe This is intentional: Flow will error when attempting to access ListView.
|
||||
Object.defineProperty(module.exports, 'ListView', {
|
||||
configurable: true,
|
||||
get() {
|
||||
invariant(
|
||||
false,
|
||||
'ListView has been removed from React Native. ' +
|
||||
'See https://fb.me/nolistview for more information or use ' +
|
||||
'`deprecated-react-native-listview`.',
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// $FlowFixMe This is intentional: Flow will error when attempting to access SwipeableListView.
|
||||
Object.defineProperty(module.exports, 'SwipeableListView', {
|
||||
configurable: true,
|
||||
get() {
|
||||
invariant(
|
||||
false,
|
||||
'SwipeableListView has been removed from React Native. ' +
|
||||
'See https://fb.me/nolistview for more information or use ' +
|
||||
'`deprecated-react-native-swipeable-listview`.',
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user