Skip to content

Commit

Permalink
Added MasonryFlashList which adds support for rendering masonry lay…
Browse files Browse the repository at this point in the history
…outs (#587)

* Added support for masonry

* Fixed index calculations

* simplified on end reached

* Fix refs

* Fix viewability events

* Added masonry sample

* Empty list added

* unused items removed

* fix display name

* Fixed viewability precision

* Added some comments

* Fix header unmount

* Initial doc

* fix link

* Fixed docs

* Update masonry-layout.md

* Improve docs

* Remove unused variables

* Added key extractor support

* Improve data set compute

* set horizontal to false

* log removed

* Added tests

* Added e2e test

* Add changelog

* test fixes

* Finished unit tests

* Updated docs

* Added autoOptimize algorithm

* Added test for auto arrangement

* improve e2e

* fix default layout value

* update podfile

* fix onend reached callback

* Fixed some review comments

* change to getColumnFlex

* Ignore lint

* Added some more comments

* Update documentation/docs/guides/masonry-layout.md

Co-authored-by: Marek Fořt <[email protected]>

* Update documentation/docs/guides/masonry-layout.md

Co-authored-by: Marek Fořt <[email protected]>

* Update documentation/docs/guides/masonry-layout.md

Co-authored-by: Marek Fořt <[email protected]>

* Update documentation/docs/guides/masonry-layout.md

Co-authored-by: Marek Fořt <[email protected]>

* Update documentation/docs/guides/masonry-layout.md

Co-authored-by: Marek Fořt <[email protected]>

* Update documentation/docs/guides/masonry-layout.md

Co-authored-by: Marek Fořt <[email protected]>

* Update documentation/docs/guides/masonry-layout.md

Co-authored-by: Marek Fořt <[email protected]>

* Address some more review comments

* Added code sample

Co-authored-by: Marek Fořt <[email protected]>
  • Loading branch information
naqvitalha and Marek Fořt authored Sep 21, 2022
1 parent 456d4f5 commit b58b5a6
Show file tree
Hide file tree
Showing 23 changed files with 1,041 additions and 99 deletions.
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.

# 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
103 changes: 103 additions & 0 deletions documentation/docs/guides/masonry-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
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`:

```tsx
import React from "react";
import { View, Text, StatusBar } from "react-native";
import { MasonryFlashList } from "@shopify/flash-list";
import { DATA } from "./data";

const MyMasonryList = () => {
return (
<MasonryFlashList
data={DATA}
numColumns={2}
renderItem={({ item }) => <Text>{item.title}</Text>}
estimatedItemSize={200}
/>
);
};
```

**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`:

- [`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

`MasonryFlashList` supports these additional props on top of `FlashList`:

### `optimizeItemArrangement`

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

If enabled, MasonryFlashList will try to reduce difference in column height by modifying item order. If `true`, specifying [`overrideItemLayout`](../fundamentals/usage.md#overrideitemlayout) is required. Default value is `false`.

### `getColumnFlex`

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

`getColumnFlex` allows you to change the column widths of the list. This is helpful if you want some columns to be wider than the others.

Example:

```tsx
// if `numColumns` is `3`, you can return `2` for `index 1` and `1` for the rest to achieve a `1:2:1` split by width.
getColumnFlex={(items, index, maxColumns, extraData) => {
return index === 1 ? 2 : 1;
}}
```

## Methods

`MasonryFlashList` exposes the some methods that `FlashList` does. These are:

### `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.

Parameter `offset` expects the offset to scroll to.

Parameter `animated` (`false` by default) defines whether the list should animate while scrolling.
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} />;
}}
getColumnFlex={(_, index) => {
return index === 1 ? 2 : 1;
}}
onLoad={({ elapsedTimeInMs }) => {
console.log("List Load Time", elapsedTimeInMs);
}}
/>
</View>
);
}

const Component = (props: {
item: MasonryData;
text?: string;
backgroundColor?: string;
}) => {
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}
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;
this.windowCorrectionConfig.value.windowShift = -this.distanceFromWindow;
this.viewabilityManager.updateViewableItems();
}
};

private getTransform() {
Expand Down
Loading

0 comments on commit b58b5a6

Please sign in to comment.