diff --git a/Examples/UIExplorer/js/FlatListExample.js b/Examples/UIExplorer/js/FlatListExample.js
index a85504d28..0a2b97215 100644
--- a/Examples/UIExplorer/js/FlatListExample.js
+++ b/Examples/UIExplorer/js/FlatListExample.js
@@ -94,6 +94,7 @@ class FlatListExample extends React.PureComponent {
{renderSmallSwitchOption(this, 'debug')}
+
+ {item.title} - {item.text}
+
+
+ );
+ }
+}
+
class FooterComponent extends React.PureComponent {
render() {
return (
@@ -245,10 +262,19 @@ const styles = StyleSheet.create({
transform: [{scale: 0.5}],
},
}),
+ stacked: {
+ alignItems: 'center',
+ backgroundColor: '#F6F6F6',
+ padding: 10,
+ },
thumb: {
width: 64,
height: 64,
},
+ stackedText: {
+ padding: 4,
+ fontSize: 18,
+ },
text: {
flex: 1,
},
@@ -260,6 +286,7 @@ module.exports = {
ItemComponent,
PlainInput,
SeparatorComponent,
+ StackedItemComponent,
genItemData,
getItemLayout,
pressItem,
diff --git a/Examples/UIExplorer/js/SectionListExample.js b/Examples/UIExplorer/js/SectionListExample.js
new file mode 100644
index 000000000..a4d1ba2e3
--- /dev/null
+++ b/Examples/UIExplorer/js/SectionListExample.js
@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @flow
+ */
+'use strict';
+
+const React = require('react');
+const ReactNative = require('react-native');
+const {
+ StyleSheet,
+ Text,
+ View,
+} = ReactNative;
+
+const SectionList = require('SectionList');
+const UIExplorerPage = require('./UIExplorerPage');
+
+const infoLog = require('infoLog');
+
+const {
+ FooterComponent,
+ ItemComponent,
+ PlainInput,
+ SeparatorComponent,
+ StackedItemComponent,
+ genItemData,
+ pressItem,
+ renderSmallSwitchOption,
+} = require('./ListExampleShared');
+
+const SectionHeaderComponent = ({section}) =>
+
+ SECTION HEADER: {section.key}
+
+ ;
+
+class SectionListExample extends React.PureComponent {
+ static title = '';
+ static description = 'Performant, scrollable list of data.';
+
+ state = {
+ data: genItemData(1000),
+ filterText: '',
+ logViewable: false,
+ virtualized: true,
+ };
+ render() {
+ const filterRegex = new RegExp(String(this.state.filterText), 'i');
+ const filter = (item) => (filterRegex.test(item.text) || filterRegex.test(item.title));
+ const filteredData = this.state.data.filter(filter);
+ return (
+
+
+ {
+ this.setState(() => ({filterText}));
+ }}
+ placeholder="Search..."
+ value={this.state.filterText}
+ />
+
+ {renderSmallSwitchOption(this, 'virtualized')}
+ {renderSmallSwitchOption(this, 'logViewable')}
+
+
+
+ alert('onRefresh: nothing to refresh :P')}
+ onViewableItemsChanged={this._onViewableItemsChanged}
+ refreshing={false}
+ shouldItemUpdate={(prev, next) => prev.item !== next.item}
+ sections={[
+ {ItemComponent: StackedItemComponent, key: 's1', data: [
+ {title: 'Item In Header Section', text: 's1', key: '0'}
+ ]},
+ {key: 's2', data: filteredData},
+ ]}
+ viewablePercentThreshold={100}
+ />
+
+ );
+ }
+ _renderItemComponent = ({item}) => ;
+ // This is called when items change viewability by scrolling into our out of the viewable area.
+ _onViewableItemsChanged = (info: {
+ changed: Array<{
+ key: string, isViewable: boolean, item: {columns: Array<*>}, index: ?number, section?: any
+ }>},
+ ) => {
+ // Impressions can be logged here
+ if (this.state.logViewable) {
+ infoLog('onViewableItemsChanged: ', info.changed.map((v: Object) => (
+ {...v, item: '...', section: v.section.key}
+ )));
+ }
+ };
+ _pressItem = (index: number) => {
+ pressItem(this, index);
+ };
+}
+
+const styles = StyleSheet.create({
+ headerText: {
+ padding: 4,
+ },
+ optionSection: {
+ flexDirection: 'row',
+ },
+ searchRow: {
+ paddingHorizontal: 10,
+ },
+});
+
+module.exports = SectionListExample;
diff --git a/Libraries/Experimental/SectionList.js b/Libraries/Experimental/SectionList.js
new file mode 100644
index 000000000..1865b7fac
--- /dev/null
+++ b/Libraries/Experimental/SectionList.js
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * Facebook, Inc. ("Facebook") owns all right, title and interest, including
+ * all intellectual property and other proprietary rights, in and to the React
+ * Native CustomComponents software (the "Software"). Subject to your
+ * compliance with these terms, you are hereby granted a non-exclusive,
+ * worldwide, royalty-free copyright license to (1) use and copy the Software;
+ * and (2) reproduce and distribute the Software as part of your own software
+ * ("Your Software"). Facebook reserves all rights not expressly granted to
+ * you in this license agreement.
+ *
+ * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
+ * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
+ * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @providesModule SectionList
+ * @flow
+ */
+'use strict';
+
+const MetroListView = require('MetroListView');
+const React = require('React');
+const VirtualizedSectionList = require('VirtualizedSectionList');
+
+import type {Viewable} from 'ViewabilityHelper';
+
+type Item = any;
+type SectionItem = any;
+
+type SectionBase = {
+ // Must be provided directly on each section.
+ data: ?Array,
+ key: string,
+
+ // Optional props will override list-wide props just for this section.
+ ItemComponent?: ?ReactClass<{item: SectionItem, index: number}>,
+ SeparatorComponent?: ?ReactClass<*>,
+ keyExtractor?: (item: SectionItem) => string,
+
+ // TODO: support more optional/override props
+ // FooterComponent?: ?ReactClass<*>,
+ // HeaderComponent?: ?ReactClass<*>,
+ // onViewableItemsChanged?: ({viewableItems: Array, changed: Array}) => void,
+
+ // TODO: support recursive sections
+ // SectionHeaderComponent?: ?ReactClass<{section: SectionBase}>,
+ // sections?: ?Array;
+};
+
+type RequiredProps = {
+ sections: Array,
+};
+
+type OptionalProps = {
+ /**
+ * Rendered after the last item in the last section.
+ */
+ FooterComponent?: ?ReactClass<*>,
+ /**
+ * Default renderer for every item in every section.
+ */
+ ItemComponent?: ?ReactClass<{item: Item, index: number}>,
+ /**
+ * Rendered at the top of each section. In the future, a sticky option will be added.
+ */
+ SectionHeaderComponent?: ?ReactClass<{section: SectionT}>,
+ /**
+ * Rendered at the bottom of every Item except the very last one in the last section.
+ */
+ SeparatorComponent?: ?ReactClass<*>,
+ /**
+ * Warning: Virtualization can drastically improve memory consumption for long lists, but trashes
+ * the state of items when they scroll out of the render window, so make sure all relavent data is
+ * stored outside of the recursive `ItemComponent` instance tree.
+ */
+ enableVirtualization?: ?boolean,
+ keyExtractor?: (item: Item) => string,
+ onEndReached?: ({distanceFromEnd: number}) => void,
+ /**
+ * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
+ * sure to also set the `refreshing` prop correctly.
+ */
+ onRefresh?: ?Function,
+ /**
+ * Called when the viewability of rows changes, as defined by the
+ * `viewablePercentThreshold` prop.
+ */
+ onViewableItemsChanged?: ({viewableItems: Array, changed: Array}) => void,
+ /**
+ * Set this true while waiting for new data from a refresh.
+ */
+ refreshing?: boolean,
+};
+
+type Props = RequiredProps & OptionalProps;
+
+/**
+ * A performant interface for rendering sectioned lists, supporting the most handy features:
+ *
+ * - Fully cross-platform.
+ * - Viewability callbacks.
+ * - Footer support.
+ * - Separator support.
+ * - Heterogeneous data and item support.
+ * - Pull to Refresh.
+ *
+ * If you don't need section support and want a simpler interface, use FlatList.
+ */
+class SectionList extends React.Component, void> {
+ props: Props;
+
+ render() {
+ if (this.props.legacyImplementation) {
+ return ;
+ } else {
+ return ;
+ }
+ }
+}
+
+module.exports = SectionList;
diff --git a/Libraries/Experimental/VirtualizedSectionList.js b/Libraries/Experimental/VirtualizedSectionList.js
new file mode 100644
index 000000000..fa4140597
--- /dev/null
+++ b/Libraries/Experimental/VirtualizedSectionList.js
@@ -0,0 +1,250 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * Facebook, Inc. ("Facebook") owns all right, title and interest, including
+ * all intellectual property and other proprietary rights, in and to the React
+ * Native CustomComponents software (the "Software"). Subject to your
+ * compliance with these terms, you are hereby granted a non-exclusive,
+ * worldwide, royalty-free copyright license to (1) use and copy the Software;
+ * and (2) reproduce and distribute the Software as part of your own software
+ * ("Your Software"). Facebook reserves all rights not expressly granted to
+ * you in this license agreement.
+ *
+ * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
+ * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
+ * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @providesModule VirtualizedSectionList
+ * @flow
+ */
+'use strict';
+
+const React = require('React');
+const View = require('View');
+const VirtualizedList = require('VirtualizedList');
+
+const invariant = require('invariant');
+const warning = require('warning');
+
+import type {Viewable} from 'ViewabilityHelper';
+
+type Item = any;
+type SectionItem = any;
+
+type Section = {
+ // Must be provided directly on each section.
+ data: ?Array,
+ key: string,
+ /**
+ * Stick whatever you want here, e.g. meta data for rendering section headers.
+ */
+ extra?: any,
+
+ // Optional props will override list-wide props just for this section.
+ ItemComponent?: ?ReactClass<{item: SectionItem, index: number}>,
+ SeparatorComponent?: ?ReactClass<*>,
+ keyExtractor?: (item: SectionItem) => string,
+
+ // TODO: support more optional/override props
+ // FooterComponent?: ?ReactClass<*>,
+ // HeaderComponent?: ?ReactClass<*>,
+ // onViewableItemsChanged?: ({viewableItems: Array, changed: Array}) => void,
+
+ // TODO: support recursive sections
+ // SectionHeaderComponent?: ?ReactClass<{section: Section}>,
+ // sections?: ?Array;
+}
+
+type RequiredProps = {
+ sections: Array,
+};
+type OptionalProps = {
+ ItemComponent?: ?ReactClass<{item: Item, index: number}>,
+ SectionHeaderComponent?: ?ReactClass<{section: Section}>,
+ SeparatorComponent?: ?ReactClass<*>,
+ /**
+ * Warning: Virtualization can drastically improve memory consumption for long lists, but trashes
+ * the state of items when they scroll out of the render window, so make sure all relavent data is
+ * stored outside of the recursive `ItemComponent` instance tree.
+ */
+ enableVirtualization?: ?boolean,
+ horizontal?: ?boolean,
+ keyExtractor?: (item: Item, index: number) => string,
+ onEndReached?: ({distanceFromEnd: number}) => void,
+ /**
+ * Called when the viewability of rows changes, as defined by the
+ * `viewablePercentThreshold` prop. Called for all items from all sections.
+ */
+ onViewableItemsChanged?: ({viewableItems: Array, changed: Array}) => void,
+};
+type Props = RequiredProps & OptionalProps;
+
+/**
+ * Right now this just flattens everything into one list and uses VirtualizedList under the
+ * hood. The only operation that might not scale well is concatting the data arrays of all the
+ * sections when new props are received, which should be plenty fast for up to ~10,000 items.
+ */
+class VirtualizedSectionList extends React.PureComponent {
+ props: Props;
+
+ state: {
+ childProps: Object,
+ };
+
+ static defaultProps: OptionalProps = {
+ keyExtractor: (item: Item, index: number) => item.key || String(index),
+ };
+
+ _keyExtractor = (item: Item, index: number) => {
+ const info = this._subExtractor(item, index);
+ return info && info.key;
+ };
+
+ _subExtractor(
+ item,
+ index: number,
+ ): ?{
+ section: Section,
+ key: string, // Key of the section or combined key for section + item
+ index: ?number, // Relative index within the section
+ } {
+ let itemIndex = index;
+ const defaultKeyExtractor = this.props.keyExtractor;
+ for (let ii = 0; ii < this.props.sections.length; ii++) {
+ const section = this.props.sections[ii];
+ const keyExtractor = section.keyExtractor || defaultKeyExtractor;
+ const key = keyExtractor(section, ii);
+ itemIndex -= 1; // The section itself is an item
+ if (itemIndex >= section.data.length) {
+ itemIndex -= section.data.length;
+ } else if (itemIndex === -1) {
+ return {section, key, index: null};
+ } else {
+ return {
+ section,
+ key: key + ':' + keyExtractor(section.data[itemIndex], itemIndex),
+ index: itemIndex,
+ };
+ }
+ }
+ }
+
+ _convertViewable = (viewable: Viewable): ?Viewable => {
+ invariant(viewable.index != null, 'Received a broken Viewable');
+ const info = this._subExtractor(viewable.item, viewable.index);
+ if (!info) {
+ return null;
+ }
+ const keyExtractor = info.section.keyExtractor || this.props.keyExtractor;
+ return {
+ ...viewable,
+ index: info.index,
+ key: keyExtractor(viewable.item, info.index),
+ section: info.section,
+ };
+ };
+
+ _onViewableItemsChanged = (
+ {viewableItems, changed}: {viewableItems: Array, changed: Array}
+ ) => {
+ if (this.props.onViewableItemsChanged) {
+ this.props.onViewableItemsChanged({
+ viewableItems: viewableItems.map(this._convertViewable, this).filter(Boolean),
+ changed: changed.map(this._convertViewable, this).filter(Boolean),
+ });
+ }
+ }
+
+ _isItemSticky = (item, index) => {
+ const info = this._subExtractor(item, index);
+ return info && info.index == null;
+ };
+
+ _renderItem = ({item, index}: {item: Item, index: number}) => {
+ const info = this._subExtractor(item, index);
+ if (!info) {
+ return null;
+ } else if (info.index == null) {
+ return ;
+ } else {
+ const ItemComponent = info.section.ItemComponent || this.props.ItemComponent;
+ const SeparatorComponent = info.section.SeparatorComponent || this.props.SeparatorComponent;
+ return (
+
+
+ {SeparatorComponent && index < this.state.childProps.getItemCount() - 1
+ ?
+ : null
+ }
+
+ );
+ }
+ };
+
+ _computeState(props: Props) {
+ const itemCount = props.sections.reduce((v, section) => v + section.data.length + 1, 0);
+ return {
+ childProps: {
+ ...props,
+ ItemComponent: this._renderItem,
+ SeparatorComponent: undefined, // Rendered with ItemComponent
+ data: props.sections,
+ getItemCount: () => itemCount,
+ getItem,
+ isItemSticky: this._isItemSticky,
+ keyExtractor: this._keyExtractor,
+ onViewableItemsChanged:
+ props.onViewableItemsChanged ? this._onViewableItemsChanged : undefined,
+ },
+ };
+ }
+
+ constructor(props: Props, context: Object) {
+ super(props, context);
+ warning(
+ !props.stickySectionHeadersEnabled,
+ 'VirtualizedSectionList: Sticky headers only supported with legacyImplementation for now.'
+ );
+ this.state = this._computeState(props);
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ this.setState(this._computeState(nextProps));
+ }
+
+ render() {
+ return ;
+ }
+}
+
+function getItem(sections: ?Array- , index: number): ?Item {
+ if (!sections) {
+ return null;
+ }
+ let itemIdx = index - 1;
+ for (let ii = 0; ii < sections.length; ii++) {
+ if (itemIdx === -1) {
+ return sections[ii]; // The section itself is the item
+ } else if (itemIdx < sections[ii].data.length) {
+ return sections[ii].data[itemIdx];
+ } else {
+ itemIdx -= (sections[ii].data.length + 1);
+ }
+ }
+ return null;
+}
+
+module.exports = VirtualizedSectionList;