/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @noflow */ 'use strict'; import * as React from 'react'; import { Animated, FlatList, Image, StyleSheet, Switch, Pressable, Text, TextInput, View } from 'react-native'; import Example from '../../shared/example'; type Item = { title: string, text: string, key: string, pressed: boolean, noImage?: ?boolean }; const AnimatedFlatList = Animated.createAnimatedComponent(FlatList); const HEADER = { height: 30, width: 100 }; const SEPARATOR_HEIGHT = StyleSheet.hairlineWidth; const HORIZ_WIDTH = 200; const ITEM_HEIGHT = 72; const VIEWABILITY_CONFIG = { minimumViewTime: 3000, viewAreaCoveragePercentThreshold: 100, waitForInteraction: true }; const THUMB_URLS = [ './lists/like.png', './lists/dislike.png', './lists/call.png', './lists/fist.png', './lists/bandaged.png', './lists/flowers.png', './lists/heart.png', './lists/liking.png', './lists/party.png', './lists/poke.png', './lists/superlike.png', './lists/victory.png' ]; const LOREM_IPSUM = 'Lorem ipsum dolor sit amet, ius ad pertinax oportere accommodare, an vix ' + 'civibus corrumpit referrentur. Te nam case ludus inciderint, te mea facilisi adipiscing. Sea id ' + 'integre luptatum. In tota sale consequuntur nec. Erat ocurreret mei ei. Eu paulo sapientem ' + 'vulputate est, vel an accusam intellegam interesset. Nam eu stet pericula reprimique, ea vim illud ' + 'modus, putant invidunt reprehendunt ne qui.'; function genItemData(count: number, start: number = 0): Array { const dataBlob = []; for (let ii = start; ii < count + start; ii++) { const itemHash = Math.abs(hashCode('Item ' + ii)); dataBlob.push({ title: 'Item ' + ii, text: LOREM_IPSUM.substr(0, (itemHash % 301) + 20), key: String(ii), pressed: false }); } return dataBlob; } class ItemComponent extends React.PureComponent<{ fixedHeight?: ?boolean, horizontal?: ?boolean, item: Item, onPress: (key: string) => void, onShowUnderlay?: () => void, onHideUnderlay?: () => void }> { _onPress = () => { this.props.onPress(this.props.item.key); }; render() { const { fixedHeight, horizontal, item } = this.props; const itemHash = Math.abs(hashCode(item.title)); const imgSource = THUMB_URLS[itemHash % THUMB_URLS.length]; return ( {!item.noImage && } {item.title} - {item.text} ); } } class FooterComponent extends React.PureComponent<{}> { render() { return ( LIST FOOTER ); } } class HeaderComponent extends React.PureComponent<{}> { render() { return ( LIST HEADER ); } } class SeparatorComponent extends React.PureComponent<{}> { render() { return ; } } class ItemSeparatorComponent extends React.PureComponent<{}> { render() { const style = this.props.highlighted ? [ styles.itemSeparator, { marginLeft: 0, backgroundColor: 'rgb(217, 217, 217)' } ] : styles.itemSeparator; return ; } } class Spindicator extends React.PureComponent<{}> { render() { return ( ); } } function hashCode(str: string): number { let hash = 15; for (let ii = str.length - 1; ii >= 0; ii--) { hash = (hash << 5) - hash + str.charCodeAt(ii); } return hash; } function getItemLayout(data: any, index: number, horizontal?: boolean) { const [length, separator, header] = horizontal ? [HORIZ_WIDTH, 0, HEADER.width] : [ITEM_HEIGHT, SEPARATOR_HEIGHT, HEADER.height]; return { length, offset: (length + separator) * index + header, index }; } function pressItem(context: Object, key: string) { const index = Number(key); const pressed = !context.state.data[index].pressed; context.setState((state) => { const newData = [...state.data]; newData[index] = { ...state.data[index], pressed, title: 'Item ' + key + (pressed ? ' (pressed)' : '') }; return { data: newData }; }); } function renderSmallSwitchOption(context: Object, key: string) { return ( {key}: context.setState({ [key]: value })} style={styles.smallSwitch} value={context.state[key]} /> ); } function PlainInput(props: Object) { return ( ); } class SingleColumnExample extends React.PureComponent { static title = ''; static description = 'Performant, scrollable list of data.'; state = { data: genItemData(100), debug: false, horizontal: false, inverted: false, filterText: '', fixedHeight: true, logViewable: false, virtualized: true }; _onChangeFilterText = (filterText) => { this.setState({ filterText }); }; _onChangeScrollToIndex = (text) => { this._listRef.scrollToIndex({ viewPosition: 0.5, index: Number(text) }); }; _scrollPos = new Animated.Value(0); _scrollSinkX = Animated.event( [{ nativeEvent: { contentOffset: { x: this._scrollPos } } }], { useNativeDriver: true } ); _scrollSinkY = Animated.event( [{ nativeEvent: { contentOffset: { y: this._scrollPos } } }], { useNativeDriver: true } ); componentDidUpdate() { this._listRef.recordInteraction(); // e.g. flipping logViewable switch } 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 ( {renderSmallSwitchOption(this, 'virtualized')} {renderSmallSwitchOption(this, 'horizontal')} {renderSmallSwitchOption(this, 'fixedHeight')} {renderSmallSwitchOption(this, 'logViewable')} {renderSmallSwitchOption(this, 'inverted')} {renderSmallSwitchOption(this, 'debug')} } contentContainerStyle={styles.list} data={filteredData} debug={this.state.debug} disableVirtualization={!this.state.virtualized} getItemLayout={ this.state.fixedHeight ? this._getItemLayout : undefined } horizontal={this.state.horizontal} inverted={this.state.inverted} key={ (this.state.horizontal ? 'h' : 'v') + (this.state.fixedHeight ? 'f' : 'd') } keyboardDismissMode="on-drag" keyboardShouldPersistTaps="always" legacyImplementation={false} numColumns={1} onEndReached={this._onEndReached} onRefresh={this._onRefresh} onScroll={ this.state.horizontal ? this._scrollSinkX : this._scrollSinkY } onViewableItemsChanged={this._onViewableItemsChanged} ref={this._captureRef} refreshing={false} renderItem={this._renderItemComponent} viewabilityConfig={VIEWABILITY_CONFIG} /> ); } _captureRef = (ref) => { this._listRef = ref; }; _getItemLayout = (data: any, index: number) => { return getItemLayout(data, index, this.state.horizontal); }; _onEndReached = () => { if (this.state.data.length >= 1000) { return; } this.setState((state) => ({ data: state.data.concat(genItemData(100, state.data.length)) })); }; _onRefresh = () => console.log('onRefresh: nothing to refresh :P'); _renderItemComponent = ({ item, separators }) => { return ( ); }; // This is called when items change viewability by scrolling into or out of // the viewable area. _onViewableItemsChanged = (info: { changed: Array<{ key: string, isViewable: boolean, item: any, index: ?number, section?: any }> }) => { // Impressions can be logged here if (this.state.logViewable) { console.log( 'onViewableItemsChanged: ', info.changed.map((v) => ({ ...v, item: '...' })) ); } }; _pressItem = (key: string) => { this._listRef.recordInteraction(); pressItem(this, key); }; _listRef: AnimatedFlatList; } const styles = StyleSheet.create({ container: { backgroundColor: 'rgb(239, 239, 244)', flex: 1 }, list: { backgroundColor: 'white' }, options: { flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center' }, searchRow: { padding: 10 }, headerFooter: { ...HEADER, alignSelf: 'center', alignItems: 'center', justifyContent: 'center' }, headerFooterContainer: { backgroundColor: 'rgb(239, 239, 244)' }, horizItem: { alignSelf: 'flex-start' // Necessary for touch highlight }, item: { flex: 1 }, itemSeparator: { height: SEPARATOR_HEIGHT, backgroundColor: 'rgb(200, 199, 204)', marginLeft: 60 }, option: { flexDirection: 'row', padding: 8, paddingLeft: 0, alignItems: 'center' }, row: { flexDirection: 'row', padding: 10, backgroundColor: 'white' }, searchTextInput: { backgroundColor: 'white', borderColor: '#cccccc', borderRadius: 3, borderWidth: 1, paddingLeft: 8, paddingVertical: 0, height: 26, fontSize: 14, flexGrow: 1 }, separator: { height: SEPARATOR_HEIGHT, backgroundColor: 'rgb(200, 199, 204)' }, smallSwitch: { top: 1, margin: -6, transform: 'scale(0.7)' }, stacked: { alignItems: 'center', backgroundColor: 'white', padding: 10 }, thumb: { width: 50, height: 50, left: -5 }, spindicator: { marginLeft: 'auto', marginTop: 8, width: 2, height: 16, backgroundColor: 'darkgray' }, stackedText: { padding: 4, fontSize: 18 }, text: { flex: 1 } }); export default function ListsPage() { return ( ); }