Files
2022-07-03 17:37:19 -07:00

507 lines
13 KiB
JavaScript

/**
* 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,
TouchableHighlight,
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<Item> {
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 (
<TouchableHighlight
onHideUnderlay={this.props.onHideUnderlay}
onPress={this._onPress}
onShowUnderlay={this.props.onShowUnderlay}
style={horizontal ? styles.horizItem : styles.item}
tvParallaxProperties={{
pressMagnification: 1.1
}}
>
<View
style={[
styles.row,
horizontal && { width: HORIZ_WIDTH },
fixedHeight && { height: ITEM_HEIGHT }
]}
>
{!item.noImage && <Image source={imgSource} style={styles.thumb} />}
<Text
numberOfLines={horizontal || fixedHeight ? 3 : undefined}
style={styles.text}
>
{item.title} - {item.text}
</Text>
</View>
</TouchableHighlight>
);
}
}
class FooterComponent extends React.PureComponent<{}> {
render() {
return (
<View style={styles.headerFooterContainer}>
<SeparatorComponent />
<View style={styles.headerFooter}>
<Text>LIST FOOTER</Text>
</View>
</View>
);
}
}
class HeaderComponent extends React.PureComponent<{}> {
render() {
return (
<View style={styles.headerFooterContainer}>
<View style={styles.headerFooter}>
<Text>LIST HEADER</Text>
</View>
<SeparatorComponent />
</View>
);
}
}
class SeparatorComponent extends React.PureComponent<{}> {
render() {
return <View style={styles.separator} />;
}
}
class ItemSeparatorComponent extends React.PureComponent<{}> {
render() {
const style = this.props.highlighted
? [
styles.itemSeparator,
{ marginLeft: 0, backgroundColor: 'rgb(217, 217, 217)' }
]
: styles.itemSeparator;
return <View style={style} />;
}
}
class Spindicator extends React.PureComponent<{}> {
render() {
return (
<Animated.View
style={[
styles.spindicator,
{
transform: [
{
rotate: this.props.value.interpolate({
inputRange: [0, 5000],
outputRange: ['0deg', '360deg'],
extrapolate: 'extend'
})
}
]
}
]}
/>
);
}
}
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 (
<View style={styles.option}>
<Text>{key}:</Text>
<Switch
onValueChange={(value) => context.setState({ [key]: value })}
style={styles.smallSwitch}
value={context.state[key]}
/>
</View>
);
}
function PlainInput(props: Object) {
return (
<TextInput
autoCapitalize="none"
autoCorrect={false}
clearButtonMode="always"
style={styles.searchTextInput}
underlineColorAndroid="transparent"
{...props}
/>
);
}
class SingleColumnExample extends React.PureComponent {
static title = '<FlatList>';
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
.getNode()
.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.getNode().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 (
<View style={styles.container}>
<View style={styles.searchRow}>
<View style={styles.options}>
<PlainInput
onChangeText={this._onChangeFilterText}
placeholder="Search..."
value={this.state.filterText}
/>
<PlainInput
onChangeText={this._onChangeScrollToIndex}
placeholder="scrollToIndex..."
/>
</View>
<View style={styles.options}>
{renderSmallSwitchOption(this, 'virtualized')}
{renderSmallSwitchOption(this, 'horizontal')}
{renderSmallSwitchOption(this, 'fixedHeight')}
{renderSmallSwitchOption(this, 'logViewable')}
{renderSmallSwitchOption(this, 'inverted')}
{renderSmallSwitchOption(this, 'debug')}
<Spindicator value={this._scrollPos} />
</View>
</View>
<SeparatorComponent />
<AnimatedFlatList
ItemSeparatorComponent={ItemSeparatorComponent}
ListFooterComponent={FooterComponent}
ListHeaderComponent={<HeaderComponent />}
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}
/>
</View>
);
}
_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 (
<ItemComponent
fixedHeight={this.state.fixedHeight}
horizontal={this.state.horizontal}
item={item}
onHideUnderlay={separators.unhighlight}
onPress={this._pressItem}
onShowUnderlay={separators.highlight}
/>
);
};
// 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.getNode().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 (
<Example title="Lists">
<SingleColumnExample />
</Example>
);
}