mirror of
https://github.com/status-im/react-native.git
synced 2025-01-10 09:35:48 +00:00
db061ea8c7
Summary: A common UI pattern for list empty states is some text/images centered inside the visible part of the list. This is pretty hard to do currently because we wrap ListEmptyComponent with an extra view with no way to style it so we cannot just use `flex: 1` to make it fill the available space. - Added an example of ListEmptyComponent in the FlatList example in RNTester Before (no way to make ListEmptyComponent fill the space): <img width="377" alt="screen shot 2018-03-05 at 5 24 15 pm" src="https://user-images.githubusercontent.com/2677334/37003152-129db3ac-209a-11e8-9600-110f10d57144.png"> After: <img width="377" alt="screen shot 2018-03-05 at 5 09 20 pm" src="https://user-images.githubusercontent.com/2677334/37002809-e6971178-2098-11e8-8cf7-74bfb2f6a992.png"> - Tested some edge cases like returning null from the ListEmptyComponent - Tested in an app that uses FlatList + ListEmptyComponent [GENERAL] [MINOR] [VirtualizedList] - Don't wrap ListEmptyComponent in an extra view Closes https://github.com/facebook/react-native/pull/18206 Differential Revision: D7266274 Pulled By: sahrens fbshipit-source-id: 4636d2418474a4c86ac63e5e18a9afc391a518c5
347 lines
8.3 KiB
JavaScript
347 lines
8.3 KiB
JavaScript
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
* @providesModule ListExampleShared
|
|
*/
|
|
'use strict';
|
|
|
|
const React = require('react');
|
|
const ReactNative = require('react-native');
|
|
const {
|
|
Animated,
|
|
Image,
|
|
Platform,
|
|
TouchableHighlight,
|
|
StyleSheet,
|
|
Switch,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} = ReactNative;
|
|
|
|
type Item = {title: string, text: string, key: string, pressed: boolean, noImage?: ?boolean};
|
|
|
|
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;
|
|
}
|
|
|
|
const HORIZ_WIDTH = 200;
|
|
const ITEM_HEIGHT = 72;
|
|
|
|
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
|
|
onPress={this._onPress}
|
|
onShowUnderlay={this.props.onShowUnderlay}
|
|
onHideUnderlay={this.props.onHideUnderlay}
|
|
tvParallaxProperties={{
|
|
pressMagnification: 1.1,
|
|
}}
|
|
style={horizontal ? styles.horizItem : styles.item}>
|
|
<View style={[
|
|
styles.row, horizontal && {width: HORIZ_WIDTH}, fixedHeight && {height: ITEM_HEIGHT}]}>
|
|
{!item.noImage && <Image style={styles.thumb} source={imgSource} />}
|
|
<Text
|
|
style={styles.text}
|
|
numberOfLines={(horizontal || fixedHeight) ? 3 : undefined}>
|
|
{item.title} - {item.text}
|
|
</Text>
|
|
</View>
|
|
</TouchableHighlight>
|
|
);
|
|
}
|
|
}
|
|
|
|
const renderStackedItem = ({item}: {item: Item}) => {
|
|
const itemHash = Math.abs(hashCode(item.title));
|
|
const imgSource = THUMB_URLS[itemHash % THUMB_URLS.length];
|
|
return (
|
|
<View style={styles.stacked}>
|
|
<Text style={styles.stackedText}>{item.title} - {item.text}</Text>
|
|
<Image style={styles.thumb} source={imgSource} />
|
|
</View>
|
|
);
|
|
};
|
|
|
|
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 ListEmptyComponent extends React.PureComponent<{}> {
|
|
render() {
|
|
return (
|
|
<View style={styles.listEmpty}>
|
|
<Text>The list is empty :o</Text>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
class SeparatorComponent extends React.PureComponent<{}> {
|
|
render() {
|
|
return <View style={styles.separator} />;
|
|
}
|
|
}
|
|
|
|
class ItemSeparatorComponent extends React.PureComponent<$FlowFixMeProps> {
|
|
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<$FlowFixMeProps> {
|
|
render() {
|
|
return (
|
|
<Animated.View style={[styles.spindicator, {
|
|
transform: [
|
|
{rotate: this.props.value.interpolate({
|
|
inputRange: [0, 5000],
|
|
outputRange: ['0deg', '360deg'],
|
|
extrapolate: 'extend',
|
|
})}
|
|
]
|
|
}]} />
|
|
);
|
|
}
|
|
}
|
|
|
|
const THUMB_URLS = [
|
|
require('./Thumbnails/like.png'),
|
|
require('./Thumbnails/dislike.png'),
|
|
require('./Thumbnails/call.png'),
|
|
require('./Thumbnails/fist.png'),
|
|
require('./Thumbnails/bandaged.png'),
|
|
require('./Thumbnails/flowers.png'),
|
|
require('./Thumbnails/heart.png'),
|
|
require('./Thumbnails/liking.png'),
|
|
require('./Thumbnails/party.png'),
|
|
require('./Thumbnails/poke.png'),
|
|
require('./Thumbnails/superlike.png'),
|
|
require('./Thumbnails/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.';
|
|
|
|
/* eslint no-bitwise: 0 */
|
|
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;
|
|
}
|
|
|
|
const HEADER = {height: 30, width: 100};
|
|
const SEPARATOR_HEIGHT = StyleSheet.hairlineWidth;
|
|
|
|
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) {
|
|
if (Platform.isTVOS) {
|
|
return null;
|
|
}
|
|
return (
|
|
<View style={styles.option}>
|
|
<Text>{key}:</Text>
|
|
<Switch
|
|
style={styles.smallSwitch}
|
|
value={context.state[key]}
|
|
onValueChange={(value) => context.setState({[key]: value})}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function PlainInput(props: Object) {
|
|
return (
|
|
<TextInput
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
clearButtonMode="always"
|
|
underlineColorAndroid="transparent"
|
|
style={styles.searchTextInput}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
headerFooter: {
|
|
...HEADER,
|
|
alignSelf: 'center',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
headerFooterContainer: {
|
|
backgroundColor: 'rgb(239, 239, 244)',
|
|
},
|
|
listEmpty: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flexGrow: 1,
|
|
},
|
|
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,
|
|
paddingRight: 0,
|
|
},
|
|
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: Platform.select({
|
|
android: {
|
|
top: 1,
|
|
margin: -6,
|
|
transform: [{scale: 0.7}],
|
|
},
|
|
ios: {
|
|
top: 4,
|
|
margin: -10,
|
|
transform: [{scale: 0.5}],
|
|
},
|
|
}),
|
|
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,
|
|
},
|
|
});
|
|
|
|
module.exports = {
|
|
FooterComponent,
|
|
HeaderComponent,
|
|
ListEmptyComponent,
|
|
ItemComponent,
|
|
ItemSeparatorComponent,
|
|
PlainInput,
|
|
SeparatorComponent,
|
|
Spindicator,
|
|
genItemData,
|
|
getItemLayout,
|
|
pressItem,
|
|
renderSmallSwitchOption,
|
|
renderStackedItem,
|
|
};
|