React Native FlatList with columns, Last item width - react-native

I'm using a FlatList to show a list of items in two columns
<FlatList style={{margin:5}}
data={this.state.items}
numColumns={2}
keyExtractor={(item, index) => item.id }
renderItem={(item) => <Card image={item.item.gallery_image_url} text={item.item.name}/> }
/>
The card component is just a view with some styles:
<View style={{ flex: 1, margin: 5, backgroundColor: '#ddd', height: 130}} ></View>
It is working fine, but if the number of items is odd, the last row only contains one item and that item stretches to the full width of the screen.
How can I set the item to the same width of the others?

for your case use flex: 1/2
therefore: Your item should have flex of 1/(number of columns) if you have 3 columns your item should have flex:1/3

Theres a few things you can try here.
A) Setting a pre-defined width for the card ( Maybe equal to the height you've set? ). Then you can use alignItems in order to have the card positioned in the middle or on the left - Unsure as to which you wanted here.
B) If there are an even number of cards, you could add an empty View at the end in order to fill this space. I find this method pretty clunky but useful when trying to leave space for future elements.
C) Simply use alignItems: 'space-between, i like to use this to center items, but you would have to define the width, or use something like flex:0.5
I suggest researching more into flexbox to help you with this, as it is hard to tell the context of this situation. I'm assuming the above methods will help, but if not, here are some links for you to look at -
A Complete Guide to Flexbox (CSS Tricks)
Layout with Flexbox (React Native)
Video Tutorial (Youtube) Link Broken
Hope this helps. If you need any further clarification - just ask

This is the cleanest way to style a FlatList with columns and spaced evenly:
<FlatList style={{margin:5}}
numColumns={2} // set number of columns
columnWrapperStyle={style.row} // space them out evenly
data={this.state.items}
keyExtractor={(item, index) => item.id }
renderItem={(item) => <Card image={item.item.gallery_image_url} text={item.item.name}/> }
/>
const style = StyleSheet.create({
row: {
flex: 1,
justifyContent: "space-around"
}
});

You can try to get the current width of the device via Dimensions, do some math based on the number of columns you want to render, minus off the margins and set that as the minWidth and maxWidth.
For example:
const {height, width} = Dimensions.get('window');
const itemWidth = (width - 15) / 2;
<View style={{ flex: 1, margin: 5, backgroundColor: '#ddd', minWidth: {this.itemWidth}, maxWidth: {this.itemWidth}, height: 130}} ></View>

The reason for it is your Card have style flex: 1, so it will try to expand all the space remain.
You can fix it by add maxWidth: '50%' to your Card style
<View style={{ flex: 1, margin: 5, backgroundColor: '#ddd', height: 130, maxWidth: '50%'}} ></View>

#Emilius Mfuruki suggestion is good, but if you have text with varying length it doesn't work perfectly. Then use this width inside your item view:
const {height, width} = Dimensions.get('window');
const itemWidth = (width - (MarginFromTheSide * 2 + MarginInBetween * (n-1))) / n;
In FlatList use:
columnWrapperStyle={{
flex: 1,
justifyContent: 'space-evenly',
}}
Works perfectly.

The simplest solution is do the math.
Imagine we have 2 View for each Row and we want to give 10 margin to every side it will look something like that:
As you see in the image above each View have 2 margins in horizontal. (inside of red rectangle)
So we have to subtract the product of margin, number of column and 2 from the width.
import { Dimensions } from 'react-native';
const {width} = Dimensions.get("window")
const column = 2
const margin = 10
const SIZE = (width - (margin * column * 2)) / column
<View style={{ margin: 10, width: SIZE }} ></View>

I tried some of the solutions above but I still had some problems with the margins on the last item (2 columns list).
My solution was simply wrapping the item into a parent container, leaving the original container with flex: 1 and the parent container of the item with flex: 0.5 so it would take the margin correctly.
itemContainer: {
flex: 0.5,
},
itemSubContainer: {
flex: 1,
marginHorizontal: margin,
},

A simple way with flex
<FlatList style={{margin:5}}
data={this.state.items}
numColumns={2}
keyExtractor={(item, index) => item.id }
renderItem={({item, index}) => {
const lastItem = index === this.state.items.length - 1;
return (
<View style={{flex: lastItem ? 1 / 2 : 1 }}>
<Card image={item.gallery_image_url} text={item.name}/>
</View>
)}}
/>

You can use ListFooterComponent={this.renderFooter}

None of the above answers have worked perfectly for me so I post my own answer:
works with padding and margins
the last element will always have the correct size
<FlatList
data={data}
numColumns={2}
renderItem={({item, index}) => {
const lastItem = index === data.length - 1;
return (
<View style={{flex: 1, padding: 8, maxWidth: lastItem ? '50%' : '100%' }}>
...
</View>
)}}
/>
Note: change maxWidth according to number of columns
Result:

just use flex:0.5 and width:'50%'

Create an array with odd number of images in it, like:
const images = [
require('./../...jpg'),
require('./../...jpg'),
require('./../...jpg'),
require('./../...jpg'),
require('./../...jpg'),
];
And then, use the code given below,
const App = () => {
const _renderItem = ({ item, index }) => (
<Image
source={item}
style={{
width: '50%',
height: 200,
}}
resizeMode="cover"
/>
);
return (
<View style={{flex: 1, marginHorizontal: 10,}}>
<FlatList
columnWrapperStyle={{ justifyContent: 'space-between' }}
keyExtractor={(_, index)=> index.toString()}
data={images}
numColumns={2}
renderItem={_renderItem}
/>
</View>
)
};
export default App;
Working Example

Related

Select and deselect multiple Items in flatlist performance issue

Displaying more than 200+ images in the flatlist and make the user to select or deselect by clicking an image. So, when the user is clicks a image, Need to show the check-circle icon on the image. Similarly, when the user is deselect the selected image, need to remove the icon. I got two arrays images[] and selectedImagesId[]. Whenever the user selects an image, the image id will be pushed into selectedImagesId array. By the way, Iam showing the icon if the rendered image id avaliable in the selectedImagesId. The problem here is, it takes so long to display or remove the icon.
<FlatList
data={props.images}
extraData={selectedImagesId}
initialNumToRender={10}
refreshing={true}
removeClippedSubviews={true}
maxToRenderPerBatch={1}
windowSize={7}
showsVerticalScrollIndicator={false}
numColumns={3}
keyExtractor={(item) => item.id.toString()}
renderItem={(itemData) => <RenderData itemData={itemData} />}
>
const RenderData = ({ itemData }) => (
<View>
<TouchableOpacity
activeOpacity={0.8}
style={{
width: width / 3.5,
height: height / 7,
padding: 2,
}}
onPress={() => {
console.log('pressed');
if (props.selectedImages.includes(itemData.item.uri)) {
const index = props.selectedImages.indexOf(itemData.item.uri);
if (index > -1) {
props.removeImageandId(index, itemData.item.id);
}
} else {
props.selectImageandId(itemData.item.uri, itemData.item.id);
}
}}>
<Image
style={{ width: '100%', height: '100%' }}
source={{
uri: itemData.item.uri,
}}
/>
{selectedImagesId.includes(itemData.item.id) && (
<AntDesign
name='checkcircle'
size={24}
color='white'
style={{ position: 'absolute', bottom: 5, right: 15 }}
/>
)}
</TouchableOpacity>
<Text style={{ backgroundColor: 'red', fontSize: 25 }}>
{render.current++}
</Text>
</View>
);
You can Refer This article
It will guide you on how to render Only particular selected components that change.
not the Whole flatList components.
so, that based Only selected particular component you can remove the icon. not whole flatList Re-render.

Component only reachable by scrolling regardless of window size

I'm trying to make a layout so that the later parts of the view are only reachable by scrolling.
Currently I'm using Dimensions to generate Views with the correct height. Is there a better way of doing so? My current solution doesn't seem too correct.
export default function MyApp() {
const height = Dimensions.get('window').height;
return (
<View style={styles.container}>
<ScrollView>
<View style={{backgroundColor: 'green', height:height}}/>
<View style={{backgroundColor: 'red', height:40}}/>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container:{
backgroundColor: 'white',
flex: 1
}
});
You can use VirtualizedList component, for example as
<VirtualizedList
data={['body']}
renderItem={({ item }) => (
<View style={styles.screen}>
{/* Put more content for body */}
</View>
)}
keyExtractor={(item, index) => index.toString()}
getItemCount={() => {
return 1;
}}
getItem={(data, index) => {
return data[index];
}}>
</VirtualizedList>
Your solution work, but not good and it have downside, when you change your phone orientation to landscape there will be bug. I dont like using Dimensions in my code unless there is no other way or use Dimensions addEventListener to listen window size and update component whenever window size change. I will suggest you a better way.
First, create a component called LayoutSizeAwareView, after this view rendered, we will catch it size from onLayout props and use them to render it children.
const LayoutSizeAwareView = (props) => {
const [size, setSize] = React.useState({width: 0, height: 0});
return (
<View
...props,
onLayout={(e) => {
setSize({
width: e.nativeEvent.layout.width,
height: e.nativeEvent.layout.height,
})
props.onLayout(e)
}}
>
{props.children(size)}
</View>
)
}
And then, in your case, use it like this
export default function MyApp() {
return (
<LayoutSizeAwareView style={styles.container}>
{({width, height}) => {
return (
<ScrollView>
<View style={{backgroundColor: 'green', height: height}}/>
<View style={{backgroundColor: 'red', height: 40}}/>
</ScrollView>
)
}}
</View>
);
}
const styles = StyleSheet.create({
container:{
backgroundColor: 'white',
flex: 1
}
});
This way your code look even cooler, there will be some typo in my code since I dont have IDE here, but you might get the idea.

React Native - Horizontal Flatlist with column wrapper doesn't take flex properly as required?

I have a dynamic flatlist with values that should be rendered in the horizontal list with 3 columns. But, I am not able to make it display as I wanted.
I am trying the code as follow,
let data = ["item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8"];
const { width } = Dimensions.get('window');
const itemWidth = (width - 12) / 3;
<FlatList
columnWrapperStyle={{ justifyContent: 'space-around', alignItems: 'flex-start' }}
numColumns={3}
data={data}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator
keyExtractor={(item, index) => `${index}${item}`}
renderItem={(item) => {
return (
<View style={{ flex: 1, backgroundColor: '#ddd', minWidth: itemWidth, maxWidth: itemWidth }}>
<Text >
Hello World
</Text>
</View>
);
}}
/>
But, the output is not as I wanted.
Let say, if the list is the length of multiple of 3 like 6,9,12 it works as I wanted.
But, if the list has length 8 first two rows are rendered okay. But, the third row is giving full space to the remaining two items. Which I don't want.
Currently i am geeting output as follow
But the output should be like,
Can anyone suggest a workaround?
Thanks.
See i can provide you one logical solution, and i'm assuming you want to plot the text inside the Flatlist. For this kind of scenarios, you would need to append an empty div for the respective empty slots.
If you want the numOfColumns to be 3,
let data = ["item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8"];
First check the remainder and num of rows to be added by
let numOfRowsToBeAdded = 3-(data.length%3);
// here im ommitting if we get 0 as remainder as its not required
if(numOfRowsToBeAdded){
for(let i=0;i<numOfRowsToBeAdded;i++){
data.push(null);
}
}
Hope you got the logic. feel free for doubts. ill help
Since you calculate itemWidth manually, you can use justify: flex-end for your columnWrapperStyle and then add a marginRight: 6 to every first and second item in every row:
renderItem={(item, index) => {
const isThirdColumn = (index + 1) % 3 === 0;
const marginRight = isThirdColumn ? 0 : 6;
return (
<View style={{ marginRight, ...otherStyles }}>

React Native: Correct scrolling in horizontal FlatList with Item Separator

ReactNative: v0.52.0
Platform: iOS
My FlatList code:
<FlatList
horizontal
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
legacyImplementation={false}
data={this.props.photos}
renderItem={item => this.renderPhoto(item)}
keyExtractor={photo => photo.id}
ItemSeparatorComponent={this.itemSeparatorComponent}
/>
Item separator code:
itemSeparatorComponent = () => {
return <View style = {
{
height: '100%',
width: 5,
backgroundColor: 'red',
}
}
/>
}
And finally FlatList item component:
renderPhoto = ({ item, index }) => {
return (
<View style = {{ width: SCREEN_WIDTH, height: 'auto' }}>
<FastImage
style = { styles.photo }
resizeMode = { FastImage.resizeMode.contain }
source = {{ uri: item.source.uri }}
/>
</View>
)
}
But when scrolling, the FlatList makes an offset to the separator but not to the left edge of item:
And with each new element the FlatList adds the width of the all previous separators to offset:
How to make the FlatList component consider the width of the separator component in horizontal scrolling and make proper offset?
I had the same use-case. For anyone looking for a solution, here it is.
Step 1) Don't use ItemSeparatorComponent prop. Instead, render it inline in your renderItem component.
Step 2) (Key-point). Specify the width and height in the style prop of the FlatList. The width, in your case, should be SCREEN_WIDTH + 5.
Then Flatlist will automatically move the entire screen (photo + separator) away when pagination is enabled. So now your code should be like so:-
<FlatList
horizontal
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
legacyImplementation={false}
data={this.props.photos}
renderItem={item => this.renderPhoto(item)}
keyExtractor={photo => photo.id}
style={{width: SCREEN_WIDTH + 5, height:'100%'}}
/>
Render photo code:-
renderPhoto = ({ item, index }) => {
return (
<View style = {{ width: SCREEN_WIDTH + 5, height: 'auto',
flexDirection:'row'}}>
<FastImage
style = { styles.photo }
resizeMode = { FastImage.resizeMode.contain }
source = {{ uri: item.source.uri }}
/>
{this. itemSeparatorComponent()}
</View>
)}
Item separator code:
itemSeparatorComponent = () => {
return <View style = {
{
height: '100%',
width: 5,
backgroundColor: 'red',
}
}
/>
}
If you still can't figure it out, then look at this component:
https://github.com/zachgibson/react-native-parallax-swiper
Try to go into the implementation, you will see that this guy has provided width and height to the Animated.ScrollView.
https://github.com/zachgibson/react-native-parallax-swiper/blob/master/src/ParallaxSwiper.js
Line number: 93 - 97
The top-level view you're returning in the renderPhoto function has a width of SCREEN_WIDTH, yet the ItemSeparatorComponent, which renders in between each item, is taking up a width of 5 as per your style definition. Consequently, for each additional item you scroll to, that initial offset will become 5 more pixels on the left.
To fix this, you can either remove the ItemSeparatorComponent completely, (as you already have pagingEnabled set to true), or set the width of the top-level view returned in renderPhoto equal to SCREEN_WIDTH - 2.5. That way you'll see half of the item separator on the right edge of one photo and the other half on the left edge of the next photo.
Actually, one other possible solution could be to remove the item separator, set the renderPhoto View's width to SCREEN_WIDTH + 5, and then include these additional properties inside the style: {paddingRight: 5, borderRightWidth: 5, borderRightColor: 'red'}. That way the red separator won't be visible until scrolling left and right, because of the pagingEnabled property.

React-Native fixed Footer within ScrollView?

I would like to create Views with dynamic content that should have some kind of footer that should normally (if there is enough space) look like they are sticked to the bottom.
Before I introduced ScrollViews my code looked like this:
<View style={{flex: 1, justifyContent: 'space-between'}}>
<View>{content}</View>
<View>{stickyFooter}</View>
</View>
After introducing the ScrollView I had to remove flex: 1 because otherwise the ScrollView is not scrollable at all. After that justifyContent became useless because the height of the flex-container is not set to 100%. Therefore both Views just appear next to each other.
To make clear what my view should look like in general:
The button should not be sticked to the bottom, but it should appear at the bottom (if possible).
This is an old question now, but for the benefit of those who end up here from a google search like I did, I paste the solution that worked for me below.
Markup
render = () => {
return (
<View>
<ScrollView style={styles.container}>
...
</ScrollView>
<TouchableOpacity style={styles.floatingActionButton}>
<RkButton
rkType='stretch'>
<RkText>Do stuff</RkText>
</RkButton>
</TouchableOpacity>
</View>
)
}
Styles
const styles = StyleSheet.create(theme => ({
floatingActionButton: {
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
bottom: 10,
left: 10,
right: 10,
borderRadius: 10,
}
}));