2015-01-30 01:10:49 +00:00
|
|
|
/**
|
2015-03-26 16:32:19 +00:00
|
|
|
* Copyright (c) 2015, Facebook, Inc. All rights reserved.
|
2015-03-23 22:07:33 +00:00
|
|
|
*
|
2015-07-24 19:58:12 +00:00
|
|
|
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
|
2015-03-26 16:32:19 +00:00
|
|
|
* all intellectual property and other proprietary rights, in and to the React
|
2015-07-24 19:58:12 +00:00
|
|
|
* Native CustomComponents software (the "Software"). Subject to your
|
2015-03-26 16:32:19 +00:00
|
|
|
* 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
|
2015-07-24 19:58:12 +00:00
|
|
|
* ("Your Software"). Facebook reserves all rights not expressly granted to
|
2015-03-26 16:32:19 +00:00
|
|
|
* 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.
|
2015-01-30 01:10:49 +00:00
|
|
|
*
|
|
|
|
* @providesModule ListView
|
|
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var ListViewDataSource = require('ListViewDataSource');
|
|
|
|
var React = require('React');
|
2015-07-20 19:32:14 +00:00
|
|
|
var RCTScrollViewManager = require('NativeModules').ScrollViewManager;
|
2015-01-30 01:10:49 +00:00
|
|
|
var ScrollView = require('ScrollView');
|
|
|
|
var ScrollResponder = require('ScrollResponder');
|
|
|
|
var StaticRenderer = require('StaticRenderer');
|
2015-03-25 02:34:12 +00:00
|
|
|
var TimerMixin = require('react-timer-mixin');
|
2015-01-30 01:10:49 +00:00
|
|
|
|
2015-07-01 16:02:38 +00:00
|
|
|
var isEmpty = require('isEmpty');
|
2015-01-30 01:10:49 +00:00
|
|
|
var logError = require('logError');
|
|
|
|
var merge = require('merge');
|
|
|
|
|
|
|
|
var PropTypes = React.PropTypes;
|
|
|
|
|
2015-03-09 23:18:15 +00:00
|
|
|
var DEFAULT_PAGE_SIZE = 1;
|
|
|
|
var DEFAULT_INITIAL_ROWS = 10;
|
|
|
|
var DEFAULT_SCROLL_RENDER_AHEAD = 1000;
|
|
|
|
var DEFAULT_END_REACHED_THRESHOLD = 1000;
|
|
|
|
var DEFAULT_SCROLL_CALLBACK_THROTTLE = 50;
|
|
|
|
var SCROLLVIEW_REF = 'listviewscroll';
|
|
|
|
|
|
|
|
|
2015-01-30 01:10:49 +00:00
|
|
|
/**
|
|
|
|
* ListView - A core component designed for efficient display of vertically
|
|
|
|
* scrolling lists of changing data. The minimal API is to create a
|
2015-03-09 23:18:15 +00:00
|
|
|
* `ListView.DataSource`, populate it with a simple array of data blobs, and
|
2015-01-30 01:10:49 +00:00
|
|
|
* instantiate a `ListView` component with that data source and a `renderRow`
|
|
|
|
* callback which takes a blob from the data array and returns a renderable
|
2015-03-09 23:18:15 +00:00
|
|
|
* component.
|
|
|
|
*
|
|
|
|
* Minimal example:
|
2015-01-30 01:10:49 +00:00
|
|
|
*
|
2015-03-09 23:18:15 +00:00
|
|
|
* ```
|
|
|
|
* getInitialState: function() {
|
2015-03-26 23:22:16 +00:00
|
|
|
* var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
|
2015-03-09 23:18:15 +00:00
|
|
|
* return {
|
|
|
|
* dataSource: ds.cloneWithRows(['row 1', 'row 2']),
|
|
|
|
* };
|
|
|
|
* },
|
2015-01-30 01:10:49 +00:00
|
|
|
*
|
2015-03-09 23:18:15 +00:00
|
|
|
* render: function() {
|
|
|
|
* return (
|
|
|
|
* <ListView
|
|
|
|
* dataSource={this.state.dataSource}
|
|
|
|
* renderRow={(rowData) => <Text>{rowData}</Text>}
|
|
|
|
* />
|
|
|
|
* );
|
|
|
|
* },
|
|
|
|
* ```
|
2015-01-30 01:10:49 +00:00
|
|
|
*
|
|
|
|
* 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:
|
|
|
|
*
|
2015-05-14 16:45:38 +00:00
|
|
|
* * Only re-render changed rows - the rowHasChanged function provided to the
|
2015-01-30 01:10:49 +00:00
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
var ListView = React.createClass({
|
|
|
|
mixins: [ScrollResponder.Mixin, TimerMixin],
|
|
|
|
|
2015-03-03 16:38:50 +00:00
|
|
|
statics: {
|
|
|
|
DataSource: ListViewDataSource,
|
|
|
|
},
|
|
|
|
|
2015-01-30 01:10:49 +00:00
|
|
|
/**
|
|
|
|
* You must provide a renderRow function. If you omit any of the other render
|
|
|
|
* functions, ListView will simply skip rendering them.
|
|
|
|
*
|
2015-05-26 22:16:42 +00:00
|
|
|
* - renderRow(rowData, sectionID, rowID, highlightRow);
|
2015-01-30 01:10:49 +00:00
|
|
|
* - renderSectionHeader(sectionData, sectionID);
|
|
|
|
*/
|
2015-03-05 05:06:49 +00:00
|
|
|
propTypes: {
|
|
|
|
...ScrollView.propTypes,
|
|
|
|
|
|
|
|
dataSource: PropTypes.instanceOf(ListViewDataSource).isRequired,
|
|
|
|
/**
|
2015-05-26 22:16:42 +00:00
|
|
|
* (sectionID, rowID, adjacentRowHighlighted) => renderable
|
2015-12-03 18:53:40 +00:00
|
|
|
*
|
2015-05-26 22:16:42 +00:00
|
|
|
* 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: PropTypes.func,
|
|
|
|
/**
|
|
|
|
* (rowData, sectionID, rowID, highlightRow) => renderable
|
2015-12-03 18:53:40 +00:00
|
|
|
*
|
2015-03-05 05:06:49 +00:00
|
|
|
* 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
|
2015-05-26 22:16:42 +00:00
|
|
|
* provide custom extractors. ListView can be notified when a row is
|
|
|
|
* being highlighted by calling highlightRow function. The separators above and
|
|
|
|
* below will be hidden when a row is highlighted. The highlighted state of
|
|
|
|
* a row can be reset by calling highlightRow(null).
|
2015-03-05 05:06:49 +00:00
|
|
|
*/
|
|
|
|
renderRow: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
|
|
* How many rows to render on initial component mount. Use this to make
|
2015-12-15 17:08:39 +00:00
|
|
|
* it so that the first screen worth of data appears at one time instead of
|
2015-03-05 05:06:49 +00:00
|
|
|
* over the course of multiple frames.
|
|
|
|
*/
|
|
|
|
initialListSize: PropTypes.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: PropTypes.func,
|
|
|
|
/**
|
|
|
|
* Threshold in pixels for onEndReached.
|
|
|
|
*/
|
|
|
|
onEndReachedThreshold: PropTypes.number,
|
|
|
|
/**
|
|
|
|
* Number of rows to render per event loop.
|
|
|
|
*/
|
|
|
|
pageSize: PropTypes.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.
|
|
|
|
*/
|
|
|
|
renderFooter: PropTypes.func,
|
|
|
|
renderHeader: PropTypes.func,
|
|
|
|
/**
|
|
|
|
* (sectionData, sectionID) => renderable
|
|
|
|
*
|
|
|
|
* If provided, a sticky header is rendered for this section. 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.
|
|
|
|
*/
|
|
|
|
renderSectionHeader: PropTypes.func,
|
2015-07-01 16:02:38 +00:00
|
|
|
/**
|
|
|
|
* (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: React.PropTypes.func.isRequired,
|
2015-03-05 05:06:49 +00:00
|
|
|
/**
|
|
|
|
* How early to start rendering rows before they come on screen, in
|
|
|
|
* pixels.
|
|
|
|
*/
|
|
|
|
scrollRenderAheadDistance: React.PropTypes.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: React.PropTypes.func,
|
|
|
|
/**
|
2015-12-28 15:55:39 +00:00
|
|
|
* A performance optimization for improving scroll perf of
|
2015-03-05 05:06:49 +00:00
|
|
|
* large lists, used in conjunction with overflow: 'hidden' on the row
|
2015-12-28 15:55:39 +00:00
|
|
|
* containers. This is enabled by default.
|
2015-03-05 05:06:49 +00:00
|
|
|
*/
|
|
|
|
removeClippedSubviews: React.PropTypes.bool,
|
2016-01-06 17:16:40 +00:00
|
|
|
/**
|
|
|
|
* 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}`.
|
|
|
|
* @platform ios
|
|
|
|
*/
|
|
|
|
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number),
|
2015-03-05 05:06:49 +00:00
|
|
|
},
|
2015-01-30 01:10:49 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Exports some data, e.g. for perf investigations or analytics.
|
|
|
|
*/
|
|
|
|
getMetrics: function() {
|
|
|
|
return {
|
2015-07-15 17:05:13 +00:00
|
|
|
contentLength: this.scrollProperties.contentLength,
|
2015-01-30 01:10:49 +00:00
|
|
|
totalRows: this.props.dataSource.getRowCount(),
|
|
|
|
renderedRows: this.state.curRenderedRowsCount,
|
|
|
|
visibleRows: Object.keys(this._visibleRows).length,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2016-02-03 17:16:15 +00:00
|
|
|
* Provides a handle to the underlying scroll responder.
|
2015-01-30 01:10:49 +00:00
|
|
|
*/
|
|
|
|
getScrollResponder: function() {
|
2016-02-03 17:16:15 +00:00
|
|
|
return this.refs[SCROLLVIEW_REF].getScrollResponder();
|
2015-01-30 01:10:49 +00:00
|
|
|
},
|
|
|
|
|
2016-02-03 17:16:15 +00:00
|
|
|
scrollTo: function(...args) {
|
|
|
|
this.refs[SCROLLVIEW_REF].scrollTo(...args);
|
2016-01-06 18:24:48 +00:00
|
|
|
},
|
|
|
|
|
2015-01-30 01:10:49 +00:00
|
|
|
setNativeProps: function(props) {
|
|
|
|
this.refs[SCROLLVIEW_REF].setNativeProps(props);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* React life cycle hooks.
|
|
|
|
*/
|
|
|
|
|
|
|
|
getDefaultProps: function() {
|
|
|
|
return {
|
|
|
|
initialListSize: DEFAULT_INITIAL_ROWS,
|
|
|
|
pageSize: DEFAULT_PAGE_SIZE,
|
2015-07-01 16:02:38 +00:00
|
|
|
renderScrollComponent: props => <ScrollView {...props} />,
|
2015-01-30 01:10:49 +00:00
|
|
|
scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD,
|
|
|
|
onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD,
|
2016-01-06 17:16:40 +00:00
|
|
|
stickyHeaderIndices: [],
|
2015-01-30 01:10:49 +00:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
getInitialState: function() {
|
|
|
|
return {
|
|
|
|
curRenderedRowsCount: this.props.initialListSize,
|
2015-05-26 22:16:42 +00:00
|
|
|
highlightedRow: {},
|
2015-01-30 01:10:49 +00:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2015-07-24 00:57:42 +00:00
|
|
|
getInnerViewNode: function() {
|
|
|
|
return this.refs[SCROLLVIEW_REF].getInnerViewNode();
|
|
|
|
},
|
|
|
|
|
2015-01-30 01:10:49 +00:00
|
|
|
componentWillMount: function() {
|
|
|
|
// this data should never trigger a render pass, so don't put in state
|
|
|
|
this.scrollProperties = {
|
2015-07-15 17:05:13 +00:00
|
|
|
visibleLength: null,
|
|
|
|
contentLength: null,
|
|
|
|
offset: 0
|
2015-01-30 01:10:49 +00:00
|
|
|
};
|
|
|
|
this._childFrames = [];
|
|
|
|
this._visibleRows = {};
|
2015-12-20 01:12:56 +00:00
|
|
|
this._prevRenderedRowsCount = 0;
|
2015-12-21 16:40:52 +00:00
|
|
|
this._sentEndForContentLength = null;
|
2015-01-30 01:10:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
componentDidMount: function() {
|
|
|
|
// do this in animation frame until componentDidMount actually runs after
|
|
|
|
// the component is laid out
|
|
|
|
this.requestAnimationFrame(() => {
|
|
|
|
this._measureAndUpdateScrollProps();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
componentWillReceiveProps: function(nextProps) {
|
|
|
|
if (this.props.dataSource !== nextProps.dataSource) {
|
2015-06-15 20:29:02 +00:00
|
|
|
this.setState((state, props) => {
|
2015-12-20 01:12:56 +00:00
|
|
|
this._prevRenderedRowsCount = 0;
|
2015-06-15 20:29:02 +00:00
|
|
|
return {
|
2016-01-07 03:15:16 +00:00
|
|
|
curRenderedRowsCount: Math.min(
|
|
|
|
state.curRenderedRowsCount + props.pageSize,
|
|
|
|
props.dataSource.getRowCount()
|
|
|
|
),
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (this.props.initialListSize !== nextProps.initialListSize) {
|
|
|
|
this.setState((state, props) => {
|
|
|
|
return {
|
|
|
|
curRenderedRowsCount: Math.max(
|
|
|
|
state.curRenderedRowsCount,
|
|
|
|
props.initialListSize
|
|
|
|
),
|
2015-06-15 20:29:02 +00:00
|
|
|
};
|
|
|
|
});
|
2015-01-30 01:10:49 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-06-22 20:37:25 +00:00
|
|
|
componentDidUpdate: function() {
|
2015-06-26 17:25:54 +00:00
|
|
|
this.requestAnimationFrame(() => {
|
|
|
|
this._measureAndUpdateScrollProps();
|
|
|
|
});
|
2015-06-22 20:37:25 +00:00
|
|
|
},
|
|
|
|
|
2015-05-26 22:16:42 +00:00
|
|
|
onRowHighlighted: function(sectionID, rowID) {
|
|
|
|
this.setState({highlightedRow: {sectionID, rowID}});
|
|
|
|
},
|
|
|
|
|
2015-01-30 01:10:49 +00:00
|
|
|
render: function() {
|
|
|
|
var bodyComponents = [];
|
|
|
|
|
|
|
|
var dataSource = this.props.dataSource;
|
|
|
|
var allRowIDs = dataSource.rowIdentities;
|
|
|
|
var rowCount = 0;
|
|
|
|
var sectionHeaderIndices = [];
|
|
|
|
|
|
|
|
var header = this.props.renderHeader && this.props.renderHeader();
|
|
|
|
var footer = this.props.renderFooter && this.props.renderFooter();
|
|
|
|
var totalIndex = header ? 1 : 0;
|
|
|
|
|
|
|
|
for (var sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) {
|
|
|
|
var sectionID = dataSource.sectionIdentities[sectionIdx];
|
|
|
|
var rowIDs = allRowIDs[sectionIdx];
|
|
|
|
if (rowIDs.length === 0) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.props.renderSectionHeader) {
|
2015-12-20 01:12:56 +00:00
|
|
|
var shouldUpdateHeader = rowCount >= this._prevRenderedRowsCount &&
|
2015-01-30 01:10:49 +00:00
|
|
|
dataSource.sectionHeaderShouldUpdate(sectionIdx);
|
|
|
|
bodyComponents.push(
|
|
|
|
<StaticRenderer
|
|
|
|
key={'s_' + sectionID}
|
|
|
|
shouldUpdate={!!shouldUpdateHeader}
|
|
|
|
render={this.props.renderSectionHeader.bind(
|
|
|
|
null,
|
|
|
|
dataSource.getSectionHeaderData(sectionIdx),
|
|
|
|
sectionID
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
sectionHeaderIndices.push(totalIndex++);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var rowIdx = 0; rowIdx < rowIDs.length; rowIdx++) {
|
|
|
|
var rowID = rowIDs[rowIdx];
|
Ensure row key uniqueness for listview with sections
Summary: I encounter issues when using ListView with multiple Sections.
```
...
var ds = new ListView.DataSource({
rowHasChanged : (row1, row2) => row1 !== row2,
sectionHeaderHasChanged : (s1, s2) => s1 !== s2
});
...
getInitialState: function() {
var sectionIDs = [0,1,2]
var rowIDs = [
[0,1,2],
[0,1,2],
[0,1,2]
]
var dataBlob = [
[blob0,blob1,blob2],
[blob3,blob4,blob5],
[blob6,blob7,blob8],
]
return {
dataSource : ds.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
};
```
the code above would throw error because of duplicate key, as ''sectionID0 + rowID0 === sectionID1 + rowID1'. And I have to do:
```
...
sectionIDs.map((id)=>{
return id+'';
});
rowIDs.map((sec)=>{
return sec,map((id)=>{
return id+'';
});
});
...
```
ListView with sections does not seem to be documented yet, so I am not sure if this is the intended behaviour or am I missing anything. Co
Closes https://github.com/facebook/react-native/pull/4082
Reviewed By: svcscm
Differential Revision: D2652028
Pulled By: mkonicek
fb-gh-sync-id: a9933bac1a1ae2d6cbc73a11ef6aefde6fcdb35f
2015-11-13 16:12:43 +00:00
|
|
|
var comboID = sectionID + '_' + rowID;
|
2015-12-20 01:12:56 +00:00
|
|
|
var shouldUpdateRow = rowCount >= this._prevRenderedRowsCount &&
|
2015-01-30 01:10:49 +00:00
|
|
|
dataSource.rowShouldUpdate(sectionIdx, rowIdx);
|
|
|
|
var row =
|
|
|
|
<StaticRenderer
|
|
|
|
key={'r_' + comboID}
|
|
|
|
shouldUpdate={!!shouldUpdateRow}
|
|
|
|
render={this.props.renderRow.bind(
|
|
|
|
null,
|
|
|
|
dataSource.getRowData(sectionIdx, rowIdx),
|
|
|
|
sectionID,
|
2015-05-26 22:16:42 +00:00
|
|
|
rowID,
|
|
|
|
this.onRowHighlighted
|
2015-01-30 01:10:49 +00:00
|
|
|
)}
|
|
|
|
/>;
|
|
|
|
bodyComponents.push(row);
|
|
|
|
totalIndex++;
|
2015-05-26 22:16:42 +00:00
|
|
|
|
|
|
|
if (this.props.renderSeparator &&
|
2015-06-01 20:27:21 +00:00
|
|
|
(rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1)) {
|
2015-05-26 22:16:42 +00:00
|
|
|
var adjacentRowHighlighted =
|
|
|
|
this.state.highlightedRow.sectionID === sectionID && (
|
|
|
|
this.state.highlightedRow.rowID === rowID ||
|
|
|
|
this.state.highlightedRow.rowID === rowIDs[rowIdx + 1]
|
|
|
|
);
|
|
|
|
var separator = this.props.renderSeparator(
|
|
|
|
sectionID,
|
|
|
|
rowID,
|
|
|
|
adjacentRowHighlighted
|
|
|
|
);
|
|
|
|
bodyComponents.push(separator);
|
|
|
|
totalIndex++;
|
|
|
|
}
|
2015-01-30 01:10:49 +00:00
|
|
|
if (++rowCount === this.state.curRenderedRowsCount) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (rowCount >= this.state.curRenderedRowsCount) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-01 16:02:38 +00:00
|
|
|
var {
|
|
|
|
renderScrollComponent,
|
|
|
|
...props,
|
|
|
|
} = this.props;
|
2015-03-31 03:12:32 +00:00
|
|
|
if (!props.scrollEventThrottle) {
|
|
|
|
props.scrollEventThrottle = DEFAULT_SCROLL_CALLBACK_THROTTLE;
|
2015-01-30 01:10:49 +00:00
|
|
|
}
|
2015-10-13 15:04:34 +00:00
|
|
|
if (props.removeClippedSubviews === undefined) {
|
|
|
|
props.removeClippedSubviews = true;
|
|
|
|
}
|
2015-07-01 16:02:38 +00:00
|
|
|
Object.assign(props, {
|
|
|
|
onScroll: this._onScroll,
|
2016-01-06 17:16:40 +00:00
|
|
|
stickyHeaderIndices: this.props.stickyHeaderIndices.concat(sectionHeaderIndices),
|
2015-07-27 18:11:59 +00:00
|
|
|
|
|
|
|
// 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,
|
2015-07-01 16:02:38 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// TODO(ide): Use function refs so we can compose with the scroll
|
|
|
|
// component's original ref instead of clobbering it
|
|
|
|
return React.cloneElement(renderScrollComponent(props), {
|
|
|
|
ref: SCROLLVIEW_REF,
|
2015-11-20 15:36:23 +00:00
|
|
|
onContentSizeChange: this._onContentSizeChange,
|
|
|
|
onLayout: this._onLayout,
|
2015-07-24 00:50:16 +00:00
|
|
|
}, header, bodyComponents, footer);
|
2015-01-30 01:10:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Private methods
|
|
|
|
*/
|
|
|
|
|
|
|
|
_measureAndUpdateScrollProps: function() {
|
2015-07-13 19:26:57 +00:00
|
|
|
var scrollComponent = this.getScrollResponder();
|
2015-07-13 19:27:02 +00:00
|
|
|
if (!scrollComponent || !scrollComponent.getInnerViewNode) {
|
|
|
|
return;
|
|
|
|
}
|
2015-07-20 19:32:14 +00:00
|
|
|
|
|
|
|
// RCTScrollViewManager.calculateChildFrames is not available on
|
|
|
|
// every platform
|
|
|
|
RCTScrollViewManager && RCTScrollViewManager.calculateChildFrames &&
|
|
|
|
RCTScrollViewManager.calculateChildFrames(
|
|
|
|
React.findNodeHandle(scrollComponent),
|
2015-12-20 01:12:56 +00:00
|
|
|
this._updateVisibleRows,
|
2015-07-20 19:32:14 +00:00
|
|
|
);
|
2015-01-30 01:10:49 +00:00
|
|
|
},
|
|
|
|
|
2015-11-20 15:36:23 +00:00
|
|
|
_onContentSizeChange: function(width, height) {
|
2015-12-30 12:52:34 +00:00
|
|
|
var contentLength = !this.props.horizontal ? height : width;
|
|
|
|
if (contentLength !== this.scrollProperties.contentLength) {
|
|
|
|
this.scrollProperties.contentLength = contentLength;
|
|
|
|
this._updateVisibleRows();
|
|
|
|
this._renderMoreRowsIfNeeded();
|
2015-12-10 20:42:22 +00:00
|
|
|
}
|
2015-12-30 12:52:34 +00:00
|
|
|
this.props.onContentSizeChange && this.props.onContentSizeChange(width, height);
|
2015-11-20 15:36:23 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_onLayout: function(event) {
|
|
|
|
var {width, height} = event.nativeEvent.layout;
|
2015-12-30 12:52:34 +00:00
|
|
|
var visibleLength = !this.props.horizontal ? height : width;
|
|
|
|
if (visibleLength !== this.scrollProperties.visibleLength) {
|
|
|
|
this.scrollProperties.visibleLength = visibleLength;
|
|
|
|
this._updateVisibleRows();
|
|
|
|
this._renderMoreRowsIfNeeded();
|
2016-02-01 22:52:33 +00:00
|
|
|
}
|
2015-12-30 12:52:34 +00:00
|
|
|
this.props.onLayout && this.props.onLayout(event);
|
2015-01-30 01:10:49 +00:00
|
|
|
},
|
|
|
|
|
2015-10-05 20:42:25 +00:00
|
|
|
_maybeCallOnEndReached: function(event) {
|
|
|
|
if (this.props.onEndReached &&
|
|
|
|
this.scrollProperties.contentLength !== this._sentEndForContentLength &&
|
|
|
|
this._getDistanceFromEnd(this.scrollProperties) < this.props.onEndReachedThreshold &&
|
|
|
|
this.state.curRenderedRowsCount === this.props.dataSource.getRowCount()) {
|
|
|
|
this._sentEndForContentLength = this.scrollProperties.contentLength;
|
|
|
|
this.props.onEndReached(event);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2015-01-30 01:10:49 +00:00
|
|
|
_renderMoreRowsIfNeeded: function() {
|
2015-07-15 17:05:13 +00:00
|
|
|
if (this.scrollProperties.contentLength === null ||
|
|
|
|
this.scrollProperties.visibleLength === null ||
|
2015-01-30 01:10:49 +00:00
|
|
|
this.state.curRenderedRowsCount === this.props.dataSource.getRowCount()) {
|
2015-10-05 20:42:25 +00:00
|
|
|
this._maybeCallOnEndReached();
|
2015-01-30 01:10:49 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-02-01 22:52:33 +00:00
|
|
|
|
2015-01-30 01:10:49 +00:00
|
|
|
var distanceFromEnd = this._getDistanceFromEnd(this.scrollProperties);
|
|
|
|
if (distanceFromEnd < this.props.scrollRenderAheadDistance) {
|
|
|
|
this._pageInNewRows();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_pageInNewRows: function() {
|
2015-06-15 20:29:02 +00:00
|
|
|
this.setState((state, props) => {
|
|
|
|
var rowsToRender = Math.min(
|
|
|
|
state.curRenderedRowsCount + props.pageSize,
|
|
|
|
props.dataSource.getRowCount()
|
|
|
|
);
|
2015-12-20 01:12:56 +00:00
|
|
|
this._prevRenderedRowsCount = state.curRenderedRowsCount;
|
2015-06-15 20:29:02 +00:00
|
|
|
return {
|
2015-01-30 01:10:49 +00:00
|
|
|
curRenderedRowsCount: rowsToRender
|
2015-06-15 20:29:02 +00:00
|
|
|
};
|
|
|
|
}, () => {
|
|
|
|
this._measureAndUpdateScrollProps();
|
2015-12-20 01:12:56 +00:00
|
|
|
this._prevRenderedRowsCount = this.state.curRenderedRowsCount;
|
2015-06-15 20:29:02 +00:00
|
|
|
});
|
2015-01-30 01:10:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_getDistanceFromEnd: function(scrollProperties) {
|
2015-11-03 18:11:51 +00:00
|
|
|
var maxLength = Math.max(
|
|
|
|
scrollProperties.contentLength,
|
|
|
|
scrollProperties.visibleLength
|
|
|
|
);
|
|
|
|
return maxLength - scrollProperties.visibleLength - scrollProperties.offset;
|
2015-01-30 01:10:49 +00:00
|
|
|
},
|
|
|
|
|
2015-07-07 14:38:24 +00:00
|
|
|
_updateVisibleRows: function(updatedFrames) {
|
2015-01-30 01:10:49 +00:00
|
|
|
if (!this.props.onChangeVisibleRows) {
|
|
|
|
return; // No need to compute visible rows if there is no callback
|
|
|
|
}
|
|
|
|
if (updatedFrames) {
|
2015-06-01 20:27:21 +00:00
|
|
|
updatedFrames.forEach((newFrame) => {
|
|
|
|
this._childFrames[newFrame.index] = merge(newFrame);
|
2015-01-30 01:10:49 +00:00
|
|
|
});
|
|
|
|
}
|
2015-07-15 17:05:13 +00:00
|
|
|
var isVertical = !this.props.horizontal;
|
2015-01-30 01:10:49 +00:00
|
|
|
var dataSource = this.props.dataSource;
|
2015-07-15 17:05:13 +00:00
|
|
|
var visibleMin = this.scrollProperties.offset;
|
|
|
|
var visibleMax = visibleMin + this.scrollProperties.visibleLength;
|
2015-01-30 01:10:49 +00:00
|
|
|
var allRowIDs = dataSource.rowIdentities;
|
|
|
|
|
|
|
|
var header = this.props.renderHeader && this.props.renderHeader();
|
|
|
|
var totalIndex = header ? 1 : 0;
|
|
|
|
var visibilityChanged = false;
|
2015-03-12 19:51:44 +00:00
|
|
|
var changedRows = {};
|
|
|
|
for (var sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) {
|
2015-01-30 01:10:49 +00:00
|
|
|
var rowIDs = allRowIDs[sectionIdx];
|
|
|
|
if (rowIDs.length === 0) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
var sectionID = dataSource.sectionIdentities[sectionIdx];
|
|
|
|
if (this.props.renderSectionHeader) {
|
|
|
|
totalIndex++;
|
|
|
|
}
|
|
|
|
var visibleSection = this._visibleRows[sectionID];
|
|
|
|
if (!visibleSection) {
|
|
|
|
visibleSection = {};
|
|
|
|
}
|
|
|
|
for (var rowIdx = 0; rowIdx < rowIDs.length; rowIdx++) {
|
|
|
|
var rowID = rowIDs[rowIdx];
|
|
|
|
var frame = this._childFrames[totalIndex];
|
|
|
|
totalIndex++;
|
|
|
|
if (!frame) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
var rowVisible = visibleSection[rowID];
|
2015-07-15 17:05:13 +00:00
|
|
|
var min = isVertical ? frame.y : frame.x;
|
|
|
|
var max = min + (isVertical ? frame.height : frame.width);
|
|
|
|
if (min > visibleMax || max < visibleMin) {
|
2015-01-30 01:10:49 +00:00
|
|
|
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) {
|
2015-07-15 17:05:13 +00:00
|
|
|
var 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'
|
|
|
|
];
|
2015-07-07 14:38:24 +00:00
|
|
|
this._updateVisibleRows(e.nativeEvent.updatedChildFrames);
|
2015-10-05 20:42:25 +00:00
|
|
|
if (!this._maybeCallOnEndReached(e)) {
|
2015-01-30 01:10:49 +00:00
|
|
|
this._renderMoreRowsIfNeeded();
|
|
|
|
}
|
|
|
|
|
2015-11-04 19:25:47 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-01-30 01:10:49 +00:00
|
|
|
this.props.onScroll && this.props.onScroll(e);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
module.exports = ListView;
|