Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added MasonryFlashList which adds support for rendering masonry layouts #587

Merged
merged 49 commits into from
Sep 21, 2022
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6a4a4f1
Added support for masonry
naqvitalha Aug 29, 2022
000bd08
Fixed index calculations
naqvitalha Aug 29, 2022
c0bdbf2
simplified on end reached
naqvitalha Aug 29, 2022
d0f9489
Fix refs
naqvitalha Aug 30, 2022
33229fa
Fix viewability events
naqvitalha Aug 31, 2022
c6bc61b
Added masonry sample
naqvitalha Aug 31, 2022
55a5662
Empty list added
naqvitalha Aug 31, 2022
9c48478
unused items removed
naqvitalha Aug 31, 2022
88d3d00
fix display name
naqvitalha Aug 31, 2022
c58bf39
Fixed viewability precision
naqvitalha Sep 1, 2022
f708be7
Added some comments
naqvitalha Sep 1, 2022
4c3f57f
Fix header unmount
naqvitalha Sep 1, 2022
d9bb88a
Initial doc
naqvitalha Sep 2, 2022
3cb9678
fix link
naqvitalha Sep 2, 2022
96e54f0
Fixed docs
naqvitalha Sep 2, 2022
8660504
Update masonry-layout.md
naqvitalha Sep 2, 2022
93bf355
Improve docs
naqvitalha Sep 2, 2022
2e75055
Remove unused variables
naqvitalha Sep 2, 2022
e40104b
Added key extractor support
naqvitalha Sep 2, 2022
7ad5dee
Improve data set compute
naqvitalha Sep 5, 2022
e76c89f
set horizontal to false
naqvitalha Sep 5, 2022
043fa0f
log removed
naqvitalha Sep 6, 2022
32cd4b1
Added tests
naqvitalha Sep 6, 2022
95a2a80
Added e2e test
naqvitalha Sep 7, 2022
67fdc2a
Merge branch 'main' into masonry
naqvitalha Sep 7, 2022
9147296
Add changelog
naqvitalha Sep 7, 2022
b4fa940
test fixes
naqvitalha Sep 7, 2022
bc29bc5
Finished unit tests
naqvitalha Sep 7, 2022
668e345
Updated docs
naqvitalha Sep 7, 2022
9fbd057
Added autoOptimize algorithm
naqvitalha Sep 8, 2022
b92a3a5
Added test for auto arrangement
naqvitalha Sep 8, 2022
5d56ca8
improve e2e
naqvitalha Sep 8, 2022
ede204f
fix default layout value
naqvitalha Sep 8, 2022
0ab54ec
update podfile
naqvitalha Sep 8, 2022
d7f073b
fix onend reached callback
naqvitalha Sep 10, 2022
8b09e94
Fixed some review comments
naqvitalha Sep 15, 2022
785e8f4
change to getColumnFlex
naqvitalha Sep 16, 2022
e1c85eb
Ignore lint
naqvitalha Sep 16, 2022
5227f35
Added some more comments
naqvitalha Sep 20, 2022
fff9773
Merge branch 'main' into masonry
naqvitalha Sep 20, 2022
0ab98ca
Update documentation/docs/guides/masonry-layout.md
naqvitalha Sep 21, 2022
1a233c8
Update documentation/docs/guides/masonry-layout.md
naqvitalha Sep 21, 2022
024fe6d
Update documentation/docs/guides/masonry-layout.md
naqvitalha Sep 21, 2022
7944889
Update documentation/docs/guides/masonry-layout.md
naqvitalha Sep 21, 2022
225b714
Update documentation/docs/guides/masonry-layout.md
naqvitalha Sep 21, 2022
a00fefb
Update documentation/docs/guides/masonry-layout.md
naqvitalha Sep 21, 2022
47dce69
Update documentation/docs/guides/masonry-layout.md
naqvitalha Sep 21, 2022
72cbbfe
Address some more review comments
naqvitalha Sep 21, 2022
b3efcd0
Added code sample
naqvitalha Sep 21, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]

- Added `MasonryFlashList` which adds support for rendering masonry layouts
- https://github.com/Shopify/flash-list/pull/587

## [1.2.2] - 2022-09-06

- Fixes type checking error in `AutoLayoutView` due to `children` not being an explicit type
Expand Down
3 changes: 1 addition & 2 deletions documentation/docs/fundamentals/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ Scroll to a specific content pixel offset in the list.

Param `offset` expects the offset to scroll to. In case of `horizontal` is true, the offset is the x-value, in any other case the offset is the y-value.

Param `animated` (`true` by default) defines whether the list should do an animation while scrolling.
Param `animated` (`false` by default) defines whether the list should do an animation while scrolling.
naqvitalha marked this conversation as resolved.
Show resolved Hide resolved

# ScrollView props

Expand All @@ -620,7 +620,6 @@ Unsupported methods:
- [`hasMore`](https://reactnative.dev/docs/virtualizedlist#hasmore)
- [`getChildContext`](https://reactnative.dev/docs/virtualizedlist#getchildcontext)
- [`getNativeScrollRef()`​](https://reactnative.dev/docs/flatlist#getnativescrollref)
- [`getScrollableNode`](https://reactnative.dev/docs/virtualizedlist#getscrollablenode)
- [`getScrollRef`](https://reactnative.dev/docs/virtualizedlist#getscrollref)
- [`getScrollResponder()`](https://reactnative.dev/docs/flatlist#getscrollresponder)

Expand Down
85 changes: 85 additions & 0 deletions documentation/docs/guides/masonry-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
id: masonry
title: Masonry Layout
---

Masonry Layout allows you to create a grid of items with different heights. It is a great way to display a collection of images with different sizes.

<div align="center">
<img src="https://user-images.githubusercontent.com/7811728/188055598-41f5c961-0dd0-4bb9-bc6e-22d78596a036.png" height="500"/>
</div>

To get started, import `MasonryFlashList` from `@shopify/flash-list` and use it just like you would use `FlashList`:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth here just putting a code snippet? I know it will be basically identical with flash-list but imho still a good practice.


**Note:** If you want `MasonryFlashList` to optimize item arrangement, enable `optimizeItemArrangement` and pass a valid [`overrideItemLayout`](../fundamentals/usage.md#overrideitemlayout) function.

## Unsupported Props

There are some props that `MasonryFlashList` does not support when compared to `FlashList`. These are:
naqvitalha marked this conversation as resolved.
Show resolved Hide resolved

- [`horizontal`](../fundamentals/usage.md#horizontal)
- [`inverted`](../fundamentals/usage.md#inverted)
- [`initialScrollIndex`](../fundamentals/usage.md#initialscrollindex)
- [`viewabilityConfigCallbackPairs`](../fundamentals/usage.md#viewabilityconfigcallbackpairs)
- [`onBlankArea`](../fundamentals/usage.md#onblankarea)

## Additional Props

There are some additional props that `MasonryFlashList` supports when compared to `FlashList`. These are:
naqvitalha marked this conversation as resolved.
Show resolved Hide resolved

### `optimizeItemArrangement`

```tsx
optimizeItemArrangement?: boolean;
```

If enabled, MasonryFlashList will try to reduce difference in column height by modifying item order. `overrideItemLayout` is required to make this work. Default value is `false`.
naqvitalha marked this conversation as resolved.
Show resolved Hide resolved

### `getColumnSizeMultiplier`

```tsx
getColumnSizeMultiplier?: (
items: T[],
columnIndex: number,
maxColumns: number,
extraData?: any
) => number | undefined;
```

Allows you to change the column widths of the list. This is helpful if you want some columns to be wider than the others.
naqvitalha marked this conversation as resolved.
Show resolved Hide resolved

Example:

```tsx
// if `numColumns` is 3, you can return 1.25 for index 1 and 0.75 for the rest to achieve a 1:2:1 split by width.
getColumnSizeMultiplier={(items, index, maxColumns, extraData) => {
return index === 1 ? 0.75 * 2 : 0.75 * 1;
naqvitalha marked this conversation as resolved.
Show resolved Hide resolved
}}
```

## Methods

`MasonryFlashList` exposes the some methods that `FlashList` does. These are:
fortmarek marked this conversation as resolved.
Show resolved Hide resolved
fortmarek marked this conversation as resolved.
Show resolved Hide resolved

### `scrollToEnd()`

```tsx
scrollToEnd?: (params?: { animated?: boolean | null | undefined });
```

Scrolls to the end of the content.

### `scrollToOffset()`

```tsx
scrollToOffset(params: {
animated?: boolean | null | undefined;
offset: number;
});
```

Scroll to a specific content pixel offset in the list.

Param `offset` expects the offset to scroll to.
naqvitalha marked this conversation as resolved.
Show resolved Hide resolved

Param `animated` (`false` by default) defines whether the list should do an animation while scrolling.
naqvitalha marked this conversation as resolved.
Show resolved Hide resolved
36 changes: 36 additions & 0 deletions fixture/e2e/Masonry.test.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { assertSnapshot } from "./utils/SnapshotAsserts";
import { wipeArtifactsLocation } from "./utils/SnapshotLocation";

describe("Masonry", () => {
const testNameLoad = "Masonry_with_FlashList_can_load";
const testNameScroll = "Masonry_with_FlashList_can_scroll";

beforeAll(async () => {
await device.launchApp({ newInstance: true });
wipeArtifactsLocation("diffs");
});

beforeEach(async () => {
await device.reloadReactNative();
await device.setOrientation("portrait");
});

it("can render columns correctly", async () => {
await element(by.id("Masonry")).tap();

const testRunScreenshotPath = await element(
by.id("MasonryList")
).takeScreenshot(testNameLoad);

assertSnapshot(testRunScreenshotPath, testNameLoad);
});
it("can scroll", async () => {
await element(by.id("Masonry")).tap();
await element(by.id("MasonryList")).scroll(2000, "down");
const testRunScreenshotPath = await element(
by.id("MasonryList")
).takeScreenshot(testNameScroll);

assertSnapshot(testRunScreenshotPath, testNameScroll);
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions fixture/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -366,9 +366,9 @@ PODS:
- React-Core
- SDWebImage (~> 5.11.1)
- SDWebImageWebPCoder (~> 0.8.4)
- RNFlashList (1.1.0):
- RNFlashList (1.2.2):
- React-Core
- RNFlashList/Tests (1.1.0):
- RNFlashList/Tests (1.2.2):
- React-Core
- RNGestureHandler (2.5.0):
- React-Core
Expand Down Expand Up @@ -597,11 +597,11 @@ SPEC CHECKSUMS:
Flipper-RSocket: d9d9ade67cbecf6ac10730304bf5607266dd2541
FlipperKit: cbdee19bdd4e7f05472a66ce290f1b729ba3cb86
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 476ee3e89abb49e07f822b48323c51c57124b572
glog: 85ecdd10ee8d8ec362ef519a6a45ff9aa27b2e85
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8
RCT-Folly: 803a9cfd78114b2ec0f140cfa6fa2a6bafb2d685
RCTRequired: 00581111c53531e39e3c6346ef0d2c0cf52a5a37
RCTTypeSafety: 07e03ee7800e7dd65cba8e52ad0c2edb06c96604
React: e61f4bf3c573d0c61c56b53dc3eb1d9daf0768a0
Expand Down Expand Up @@ -630,7 +630,7 @@ SPEC CHECKSUMS:
ReactCommon: bf2888a826ceedf54b99ad1b6182d1bc4a8a3984
ReactNativePerformanceListsProfiler: b9f7cfe8d08631fbce8e4729d388a5a3f7f562c2
RNFastImage: 1f2cab428712a4baaf78d6169eaec7f622556dd7
RNFlashList: 031c182b95ead44fd0715f6029a744cd5e10d51f
RNFlashList: 13d14d9502661134ad3ba892f81d76bdcbd79755
RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50
RNReanimated: 3d1432ce7b6b7fc31f375dcabe5b4585e0634a43
RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19
Expand Down
1 change: 1 addition & 0 deletions fixture/src/ExamplesScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const ExamplesScreen = () => {
title: "Twitter Custom Cell Container",
destination: "TwitterCustomCellContainer",
},
{ title: "Masonry", destination: "Masonry" },
];
return (
<>
Expand Down
112 changes: 112 additions & 0 deletions fixture/src/Masonry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from "react";
import { Text, View, StyleSheet, Platform } from "react-native";
import { MasonryFlashList } from "@shopify/flash-list";

interface MasonryData {
index: number;
height: number;
}

export function Masonry() {
const columnCount = 3;
const data: MasonryData[] = new Array(999).fill(null).map((_, index) => {
return {
index,
height: ((index * 10) % 100) + 100 / ((index % columnCount) + 1),
};
});
return (
<View style={styles.container}>
<MasonryFlashList
testID="MasonryList"
data={data}
optimizeItemArrangement
overrideItemLayout={(layout, item) => {
layout.size = item.height;
}}
numColumns={columnCount}
estimatedItemSize={150}
ListHeaderComponent={
<Component
item={{ index: 0, height: 100 }}
text="Header"
backgroundColor="red"
/>
}
ListFooterComponent={
<Component
item={{ index: 0, height: 100 }}
text="Footer"
backgroundColor="lightblue"
/>
}
ListEmptyComponent={
<Component
item={{ index: 0, height: 100 }}
text="Empty"
backgroundColor="black"
/>
}
onViewableItemsChanged={(info) => {
info.changed.forEach((item) => {
if (item.isViewable) {
console.log("Viewable:", item.index);
}
});
}}
keyExtractor={(item, index) => {
if (item.index !== index) {
console.log("Key Extractor issue @", index);
}
return item.index.toString();
}}
getItemType={(item, index) => {
if (item.index !== index) {
console.log(index);
}
return undefined;
}}
renderItem={({ item }) => {
return <Component item={item} />;
}}
getColumnSizeMultiplier={(_, index) => {
return index === 1 ? 0.75 * 2 : 0.75 * 1;
}}
onLoad={({ elapsedTimeInMs }) => {
console.log("List Load Time", elapsedTimeInMs);
}}
/>
</View>
);
}

const Component = (props: {
item: MasonryData;
text?: string;
backgroundColor?: string;
}) => {
naqvitalha marked this conversation as resolved.
Show resolved Hide resolved
return (
<View
style={{
height: props.item.height,
backgroundColor: props.backgroundColor ?? "darkgray",
margin: 2,
alignItems: "center",
justifyContent: "center",
borderRadius: 10,
}}
>
<Text>{props.text ?? props.item.index}</Text>
</View>
);
};

const styles = StyleSheet.create({
container: {
flex: Platform.OS === "web" ? undefined : 1,
height: Platform.OS === "web" ? window.innerHeight : undefined,
justifyContent: "center",
backgroundColor: "#ecf0f1",
paddingHorizontal: 2,
},
});
2 changes: 2 additions & 0 deletions fixture/src/NavigationTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { DebugScreen } from "./Debug";
import { Messages, MessagesFlatList } from "./Messages";
import TwitterBenchmark from "./twitter/TwitterBenchmark";
import TwitterCustomCellContainer from "./twitter/CustomCellRendererComponent";
import { Masonry } from "./Masonry";

const Stack = createStackNavigator<RootStackParamList>();

Expand Down Expand Up @@ -50,6 +51,7 @@ const NavigationTree = () => {
component={TwitterCustomCellContainer}
/>
</Stack.Group>
<Stack.Screen name="Masonry" component={Masonry} />
<Stack.Group screenOptions={{ presentation: "modal" }}>
<Stack.Screen name="Debug" component={DebugScreen} />
</Stack.Group>
Expand Down
1 change: 1 addition & 0 deletions fixture/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export type RootStackParamList = {
MessagesFlatList: undefined;
TwitterBenchmark: undefined;
TwitterCustomCellContainer: undefined;
Masonry: undefined;
};
13 changes: 9 additions & 4 deletions src/FlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ class FlashList<T> extends React.PureComponent<
return (
<>
<PureComponentWrapper
enabled={children.length > 0 || this.isEmptyList}
enabled={this.isListLoaded || children.length > 0 || this.isEmptyList}
naqvitalha marked this conversation as resolved.
Show resolved Hide resolved
contentStyle={this.props.contentContainerStyle}
horizontal={this.props.horizontal}
header={this.props.ListHeaderComponent}
Expand All @@ -448,7 +448,7 @@ class FlashList<T> extends React.PureComponent<
? this.getValidComponent(this.props.ListEmptyComponent)
: null}
<PureComponentWrapper
enabled={children.length > 0 || this.isEmptyList}
enabled={this.isListLoaded || children.length > 0 || this.isEmptyList}
contentStyle={this.props.contentContainerStyle}
horizontal={this.props.horizontal}
header={this.props.ListFooterComponent}
Expand Down Expand Up @@ -489,10 +489,15 @@ class FlashList<T> extends React.PureComponent<
};

private updateDistanceFromWindow = (event: LayoutChangeEvent) => {
this.distanceFromWindow = this.props.horizontal
const newDistanceFromWindow = this.props.horizontal
? event.nativeEvent.layout.x
: event.nativeEvent.layout.y;
this.windowCorrectionConfig.value.windowShift = -this.distanceFromWindow;

if (this.distanceFromWindow !== newDistanceFromWindow) {
this.distanceFromWindow = newDistanceFromWindow;
naqvitalha marked this conversation as resolved.
Show resolved Hide resolved
this.windowCorrectionConfig.value.windowShift = -this.distanceFromWindow;
this.viewabilityManager.updateViewableItems();
}
};

private getTransform() {
Expand Down
Loading