First of All, I wanted to thank you for supporting my last article; Just seeing the stats, gives me enough motivation to write more and share my thoughts here. ✌️🚀
I know, I know. This shouldn’t be a big deal. It’s just a masonry list, only in reverse direction. right? NO!. I went to end of the rope just configuring that. :) Let me explain.
1. Using 3rd Party libraries
At first, I tried to use 3rd party libraries to ease the task. Therefore, after a couple of R&Ds, I came across two famous libraries that would just do the same:
— @react-native-seoul/masonry-list
— @shopify/flash-list
Both of these are great libraries to use in general, but since they are using FlatList, they lack RTL support by default. :(
Moreover, looking at their source code, the logic is not even properly examined.
Considering you have an array of Images, even ones would go to left and odd ones would go to right. To clear the problem, let me explain a thing about Masonry List.
Masonry lists are existing because we want to see the real image size without resizing it to a certain height and width. So what happens if we have 2 large images at left and 2 small ones at right?
But as far as I reviewed, there weren’t any logic to handle this case in these libraries. for example, this is the logic in react-native-masonry-list:
{
Array.from(Array(numColumns), (_, num) => {
return (
<View
style={{
flex: 1 / numColumns,
flexDirection: horizontal ? "row" : "column",
}}
>
{data
.map((el, i) => {
// THE REAL LOGIC:
if (i % numColumns === num) {
return (
<View
>
{renderItem({ item: el, i })}
</View>
);
}
return null;
})
.filter((e) => !!e)}
</View>
);
});
}
As you can see, there’s no logic to handle the case!
2. Writing it from scratch
It didn’t work out the easy way. so, I decided to go the hard way and write the logic myself. :)
So, what’s all the fuss is about? Images should be put in different groups based on their heights. so, we should do the following steps:
- Find the number of Columns
- Calculate all Images height
- Put each image in the column with the shortest height
- Calculate the column height for next iteration
First one’s the relaxing one. The component can get column numbers from props and have a default of 2.
const [cols, setCols] = useState(
Array(numColumns)
.fill(numColumns)
.map((_) => ({ bricks: [], colHeight: 0 }))
);
Second one’s also not a big deal. Each image object would have a height decided based on the image.
const processImage = async () => {
const processedImages = [...data];
for (const i in data) {
const item = processedImages[i];
await Image.getSize(item.image, (_, height) => {
processedImages[i] = { ...item, height: Math.min(500, height) };
});
}
return await layoutBricks(processedImages);
};
Third one’s a bit tricky. I decided to create a state for the whole data and in each iteration, decide the image place on the array.
Fourth one’s also done immediately after second one after placing it inside the correct column.
const layoutBricks = async (data) => {
const newCols = [...cols];
data.forEach((image) => {
let ht = image.height;
const heights = newCols.map(({ colHeight }) => colHeight);
const shortestColumnIndex = heights.findIndex((colH) => colH === Math.min.apply(Math, heights));
const shortestColumn = newCols[shortestColumnIndex];
newCols[shortestColumnIndex] = {
bricks: [...shortestColumn.bricks, image],
colHeight: shortestColumn.colHeight + ht,
};
});
setCols(newCols);
};
HOORAY! 🎊 Now, we’ve successfully put all images in their corresponding array. so, What’s the next step?
3. Improving the performance
In the current implementation, image height is calculated once and image rendering is done in another. The only way to improve this performance was to get the image ratio from backend side in order to remove the double downloading images.
Now we can easily render the image with the following style:
style={[styles.imageItem, { width: ITEM_WIDTH, aspectRatio: item.ratio || 1 }]}
This was the journey of creating a masonry list component from scratch. I would be happy to hear your thoughts and ideas on how to improve it even more. :); Also, I have a couple of ideas in mind before publishing it into NPM. But, if you’re curios to see my component, here’ the gist:
Thanks for following me in this article; See you in the next one :). 🚀