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;