diff --git a/examples/components/ListView/ListViewExample.js b/examples/components/ListView/ListViewExample.js
index 55fc6502..9ad9039a 100644
--- a/examples/components/ListView/ListViewExample.js
+++ b/examples/components/ListView/ListViewExample.js
@@ -1,4 +1,80 @@
import React from 'react';
-import { storiesOf, action } from '@kadira/storybook';
-import { ListView } from 'react-native'
+import { storiesOf } from '@kadira/storybook';
+import { ListView, StyleSheet, Text, View } from 'react-native';
+const generateData = (length) => Array.from({ length }).map((item, i) => i);
+const dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
+
+storiesOf('component: ListView', module)
+ .add('vertical', () => (
+
+ { console.log('ScrollView.onScroll', e); } }
+ // eslint-disable-next-line react/jsx-no-bind
+ renderRow={(row) => (
+ {row}
+ )}
+ scrollEventThrottle={1000} // 1 event per second
+ style={styles.scrollViewStyle}
+ />
+
+ ))
+ .add('incremental rendering - large pageSize', () => (
+
+ { console.log('ScrollView.onScroll', e); } }
+ pageSize={50}
+ // eslint-disable-next-line react/jsx-no-bind
+ renderRow={(row) => (
+ {row}
+ )}
+ scrollEventThrottle={1000} // 1 event per second
+ style={styles.scrollViewStyle}
+ />
+
+ ))
+ .add('incremental rendering - small pageSize', () => (
+
+ { console.log('ScrollView.onScroll', e); } }
+ pageSize={1}
+ // eslint-disable-next-line react/jsx-no-bind
+ renderRow={(row) => (
+ {row}
+ )}
+ scrollEventThrottle={1000} // 1 event per second
+ style={styles.scrollViewStyle}
+ />
+
+ ));
+
+const styles = StyleSheet.create({
+ box: {
+ flexGrow: 1,
+ justifyContent: 'center',
+ borderWidth: 1
+ },
+ scrollViewContainer: {
+ height: '200px',
+ width: 300
+ },
+ scrollViewStyle: {
+ borderWidth: '1px'
+ },
+ scrollViewContentContainerStyle: {
+ backgroundColor: '#eee',
+ padding: '10px'
+ }
+});
diff --git a/src/components/ListView/index.js b/src/components/ListView/index.js
index 4843c9a3..c3ce3631 100644
--- a/src/components/ListView/index.js
+++ b/src/components/ListView/index.js
@@ -2,18 +2,27 @@ import applyNativeMethods from '../../modules/applyNativeMethods';
import ListViewDataSource from './ListViewDataSource';
import ListViewPropTypes from './ListViewPropTypes';
import ScrollView from '../ScrollView';
-import View from '../View';
-import React, { Component } from 'react';
+import StaticRenderer from '../StaticRenderer';
+import React, { Component, isEmpty, merge } from 'react';
+import requestAnimationFrame from 'fbjs/lib/requestAnimationFrame';
+
+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;
class ListView extends Component {
static propTypes = ListViewPropTypes;
static defaultProps = {
- initialListSize: 10,
- pageSize: 1,
+ initialListSize: DEFAULT_INITIAL_ROWS,
+ pageSize: DEFAULT_PAGE_SIZE,
renderScrollComponent: (props) => ,
- scrollRenderAheadDistance: 1000,
- onEndReachedThreshold: 1000,
+ scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD,
+ onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD,
+ scrollEventThrottle: DEFAULT_SCROLL_CALLBACK_THROTTLE,
+ removeClippedSubviews: true,
stickyHeaderIndices: []
};
@@ -26,6 +35,34 @@ class ListView extends Component {
highlightedRow: {}
};
this.onRowHighlighted = (sectionId, rowId) => this._onRowHighlighted(sectionId, rowId);
+ this.scrollProperties = {};
+ }
+
+ componentWillMount() {
+ // this data should never trigger a render pass, so don't put in state
+ this.scrollProperties = {
+ visibleLength: null,
+ contentLength: null,
+ offset: 0
+ };
+ this._childFrames = [];
+ this._visibleRows = {};
+ this._prevRenderedRowsCount = 0;
+ this._sentEndForContentLength = null;
+ }
+
+ componentDidMount() {
+ // do this in animation frame until componentDidMount actually runs after
+ // the component is laid out
+ requestAnimationFrame(() => {
+ this._measureAndUpdateScrollProps();
+ });
+ }
+
+ componentDidUpdate() {
+ requestAnimationFrame(() => {
+ this._measureAndUpdateScrollProps();
+ });
}
getScrollResponder() {
@@ -40,58 +77,313 @@ class ListView extends Component {
return this._scrollViewRef && this._scrollViewRef.setNativeProps(props);
}
- _onRowHighlighted(sectionId, rowId) {
+ _onRowHighlighted = (sectionId, rowId) => {
this.setState({ highlightedRow: { sectionId, rowId } });
}
+ renderSectionHeaderFn = (data, sectionID) => {
+ return () => this.props.renderSectionHeader(data, sectionID);
+ }
+
+ renderRowFn = (data, sectionID, rowID) => {
+ return () => this.props.renderRow(data, sectionID, rowID, this._onRowHighlighted);
+ }
+
render() {
- const dataSource = this.props.dataSource;
- const header = this.props.renderHeader ? this.props.renderHeader() : undefined;
- const footer = this.props.renderFooter ? this.props.renderFooter() : undefined;
-
- // render sections and rows
const children = [];
- const sections = dataSource.rowIdentities;
- const renderRow = this.props.renderRow;
- const renderSectionHeader = this.props.renderSectionHeader;
- const renderSeparator = this.props.renderSeparator;
- for (let sectionIdx = 0, sectionCnt = sections.length; sectionIdx < sectionCnt; sectionIdx++) {
- const rows = sections[sectionIdx];
- const sectionId = dataSource.sectionIdentities[sectionIdx];
- // render optional section header
- if (renderSectionHeader) {
- const section = dataSource.getSectionHeaderData(sectionIdx);
- const key = `s_${sectionId}`;
- const child = {renderSectionHeader(section, sectionId)};
- children.push(child);
+ const dataSource = this.props.dataSource;
+ const allRowIDs = dataSource.rowIdentities;
+ let rowCount = 0;
+ const sectionHeaderIndices = [];
+
+ 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('fbjs/lib/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.');
+ }
}
- // render rows
- for (let rowIdx = 0, rowCnt = rows.length; rowIdx < rowCnt; rowIdx++) {
- const rowId = rows[rowIdx];
- const row = dataSource.getRowData(sectionIdx, rowIdx);
- const key = `r_${sectionId}_${rowId}`;
- const child = {renderRow(row, sectionId, rowId, this.onRowHighlighted)};
- children.push(child);
+ if (this.props.renderSectionHeader) {
+ const shouldUpdateHeader = rowCount >= this._prevRenderedRowsCount &&
+ dataSource.sectionHeaderShouldUpdate(sectionIdx);
+ children.push(
+
+ );
+ sectionHeaderIndices.push(totalIndex++);
+ }
- // render optional separator
- if (renderSeparator && ((rowIdx !== rows.length - 1) || (sectionIdx === sections.length - 1))) {
+ 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 =
+ ;
+ children.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 === rows[rowIdx + 1]);
- const separator = renderSeparator(sectionId, rowId, adjacentRowHighlighted);
- children.push(separator);
+ 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) {
+ children.push(separator);
+ totalIndex++;
+ }
}
+ if (++rowCount === this.state.curRenderedRowsCount) {
+ break;
+ }
+ }
+ if (rowCount >= this.state.curRenderedRowsCount) {
+ break;
}
}
- return React.cloneElement(this.props.renderScrollComponent(this.props), {
- ref: this._setScrollViewRef
+ const {
+ renderScrollComponent,
+ ...props
+ } = this.props;
+ Object.assign(props, {
+ onScroll: this._onScroll,
+ stickyHeaderIndices: this.props.stickyHeaderIndices.concat(sectionHeaderIndices),
+
+ // 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 React.cloneElement(renderScrollComponent(props), {
+ ref: this._setScrollViewRef,
+ onContentSizeChange: this._onContentSizeChange,
+ onLayout: this._onLayout
}, header, children, footer);
}
+ _measureAndUpdateScrollProps() {
+ const scrollComponent = this.getScrollResponder();
+ if (!scrollComponent || !scrollComponent.getInnerViewNode) {
+ return;
+ }
+
+ this._updateVisibleRows();
+ }
+
+ _onLayout = (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);
+ }
+
+ _updateVisibleRows(updatedFrames?: Array