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

Add masonry layout with multiple column option #15

Merged
merged 7 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ DerivedData
*.ipa
*.xcuserstate
project.xcworkspace
.xcode.env.local

# Android/IJ
#
Expand Down
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ It invokes Yoga for precise layout measurements of Shadow Nodes and constructs a
| Nested ShadowList (ScrollView) | ✅ | ❌ |
| Natively Inverted List Support | ✅ | ❌ |
| Smooth Scrolling | ✅ | ❌ |
| Dynamic Components | ❌ | ✅ |

## Scroll Performance
| Number of Items | ShadowList | FlatList | FlashList |
Expand All @@ -28,18 +29,39 @@ It invokes Yoga for precise layout measurements of Shadow Nodes and constructs a
## Important Note
Shadowlist doesn't support state updates or dynamic prop calculations inside the renderItem function. Any changes to child components should be made through the data prop. This also applies to animations. This restriction will be addressed in future updates.

One temporary way to mitigate this is by implementing list pagination until the [following problem is addressed](https://github.com/reactwg/react-native-new-architecture/discussions/223).

## Installation
- CLI: Add the package to your project via `yarn add shadowlist` and run `pod install` in the `ios` directory.
- Expo: Add the package to your project via `npx expo install shadowlist` and run `npx expo prebuild` in the root directory.


## Usage

```js
import {Shadowlist} from 'shadowlist';

const stringify = (str: string) => `{{${str}}}`;

type ElementProps = {
data: Array<any>;
};

const Element = (props: ElementProps) => {
const handlePress = (event: GestureResponderEvent) => {
const elementDataIndex = __NATIVE_getRegistryElementMapping(
event.nativeEvent.target
);
props.data[elementDataIndex];
};

return (
<Pressable style={styles.container} onPress={handlePress}>
<Image source={{ uri: stringify('image') }} style={styles.image} />
<Text style={styles.title}>{stringify('id')}</Text>
<Text style={styles.content}>{stringify('text')}</Text>
<Text style={styles.footer}>index: {stringify('position')}</Text>
</Pressable>
);
};

<Shadowlist
contentContainerStyle={styles.container}
ref={shadowListContainerRef}
Expand All @@ -59,7 +81,7 @@ import {Shadowlist} from 'shadowlist';
## API
| Prop | Type | Required | Description |
|----------------------------|---------------------------|----------|-------------------------------------------------|
| `data` | Array | Required | An array of data to be rendered in the list. |
| `data` | Array | Required | An array of data to be rendered in the list, where each item *must* include a required `id` field. |
| `keyExtractor` | Function | Required | Used to extract a unique key for a given item at the specified index. |
| `contentContainerStyle` | ViewStyle | Optional | These styles will be applied to the scroll view content container which wraps all of the child views. |
| `ListHeaderComponent` | React component | Optional | A custom component to render at the top of the list. |
Expand All @@ -76,6 +98,7 @@ import {Shadowlist} from 'shadowlist';
| `onEndReachedThreshold` | Double | Optional | The threshold (in content length units) at which `onEndReached` is triggered. |
| `onStartReached` | Function | Optional | Called when the start of the content is within `onStartReachedThreshold`. |
| `onStartReachedThreshold` | Double | Optional | The threshold (in content length units) at which `onStartReached` is triggered. |
| `numColumns` | Number | Optional | Defines the number of columns in a grid layout. When enabled, the list will display items in a Masonry-style layout with variable item heights. |


## Methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case "initialNumToRender":
mViewManager.setInitialNumToRender(view, value == null ? 0 : ((Double) value).intValue());
break;
case "numColumns":
mViewManager.setNumColumns(view, value == null ? 0 : ((Double) value).intValue());
break;
case "initialScrollIndex":
mViewManager.setInitialScrollIndex(view, value == null ? 0 : ((Double) value).intValue());
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public interface SLContainerManagerInterface<T extends View> {
void setInverted(T view, boolean value);
void setHorizontal(T view, boolean value);
void setInitialNumToRender(T view, int value);
void setNumColumns(T view, int value);
void setInitialScrollIndex(T view, int value);
void scrollToIndex(T view, int index, boolean animated);
void scrollToOffset(T view, int offset, boolean animated);
Expand Down
2 changes: 2 additions & 0 deletions android/shadowlist/jni/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS
file(GLOB LIB_INCLUDES_SRCS CONFIGURE_DEPENDS
${LIB_CPP_DIR}/fenwick/*.cpp
${LIB_CPP_DIR}/json/*.hpp
${LIB_CPP_DIR}/helpers/*.cpp
)

add_library(
Expand All @@ -28,6 +29,7 @@ target_include_directories(react_codegen_RNShadowlistSpec PUBLIC
${LIB_JNI_DIR}/
${LIB_CPP_DIR}/fenwick
${LIB_CPP_DIR}/json
${LIB_CPP_DIR}/helpers
${LIB_CPP_DIR}/react/renderer/components/SLContainerSpec
${LIB_CPP_DIR}/react/renderer/components/SLElementSpec
${LIB_CPP_DIR}/react/renderer/components/RNShadowlistSpec
Expand Down
5 changes: 5 additions & 0 deletions android/src/main/java/com/shadowlist/SLContainerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ public void setHorizontal(SLContainer view, boolean horizontal) {
public void setInitialNumToRender(SLContainer view, int initialNumToRender) {
}

@ReactProp(name = "numColumns")
@Override
public void setNumColumns(SLContainer view, int numColumns) {
}

@ReactProp(name = "initialScrollIndex")
@Override
public void setInitialScrollIndex(SLContainer view, int initialScrollIndex) {
Expand Down
42 changes: 42 additions & 0 deletions cpp/helpers/Offsetter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class Offsetter {
private:
float* offsets;
int columns;

public:
Offsetter(int numColumns, float headerOffset = 0.0f) : columns(numColumns) {
offsets = new float[columns]();
for (int i = 0; i < columns; ++i) {
offsets[i] = headerOffset;
}
}

void add(int column, float px) {
if (column >= 0 && column < columns) {
offsets[column] += px;
}
}

float get(int column) const {
if (column >= 0 && column < columns) {
return offsets[column];
}
return 0;
}

float max() const {
if (columns == 0) return 0;

float maxOffset = offsets[0];
for (int i = 1; i < columns; ++i) {
if (offsets[i] > maxOffset) {
maxOffset = offsets[i];
}
}
return maxOffset;
}

~Offsetter() {
delete[] offsets;
}
};
74 changes: 23 additions & 51 deletions cpp/react/renderer/components/SLContainerSpec/SLContainerProps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,69 +4,41 @@

namespace facebook::react {

nlohmann::json convertDataProp(
const PropsParserContext& context,
const RawProps& rawProps,
const char* name,
const nlohmann::json sourceValue,
const nlohmann::json defaultValue) {
try {
std::string content = convertRawProp(context, rawProps, name, sourceValue, defaultValue);
nlohmann::json json = nlohmann::json::parse(content);

if (!json.is_array()) {
throw std::runtime_error("data prop must be an array");
}

return json;
} catch (...) {
return nlohmann::json::array();
}
}

std::vector<std::string> convertUniqueIdsProp(
const PropsParserContext& context,
const RawProps& rawProps,
const char* name,
const nlohmann::json sourceValue,
const nlohmann::json defaultValue) {
try {
std::vector<std::string> uniqueIds;
SLContainerProps::SLContainerProps(
const PropsParserContext &context,
const SLContainerProps &sourceProps,
const RawProps &rawProps): ViewProps(context, sourceProps, rawProps),

if (!defaultValue.is_array()) {
throw std::runtime_error("data prop must be an array");
data(convertRawProp(context, rawProps, "data", sourceProps.data, "[]")),
inverted(convertRawProp(context, rawProps, "inverted", sourceProps.inverted, false)),
horizontal(convertRawProp(context, rawProps, "horizontal", sourceProps.horizontal, false)),
initialNumToRender(convertRawProp(context, rawProps, "initialNumToRender", sourceProps.initialNumToRender, 10)),
numColumns(convertRawProp(context, rawProps, "numColumns", sourceProps.numColumns, 1)),
initialScrollIndex(convertRawProp(context, rawProps, "initialScrollIndex", sourceProps.initialScrollIndex, 0))
{
try {
uniqueIds = {};
parsed = nlohmann::json::parse(data).get<nlohmann::json>();
} catch (const nlohmann::json::parse_error& e) {
parsed = nlohmann::json::array();
std::cerr << "SLContainerProps data parse: " << e.what() << ", at: " << e.byte << std::endl;
} catch (...) {
parsed = nlohmann::json::array();
std::cerr << "SLContainerProps data parse: unknown" << std::endl;
}

for (const auto& item : defaultValue) {
for (const auto& item : parsed) {
if (item.contains("id") && item["id"].is_string()) {
uniqueIds.push_back(item["id"].get<std::string>());
}
}

return uniqueIds;
} catch (...) {
return std::vector<std::string>();
}
}

SLContainerProps::SLContainerProps(
const PropsParserContext &context,
const SLContainerProps &sourceProps,
const RawProps &rawProps): ViewProps(context, sourceProps, rawProps),

data(convertDataProp(context, rawProps, "data", sourceProps.data, nlohmann::json::array())),
uniqueIds(convertUniqueIdsProp(context, rawProps, "uniqueIds", sourceProps.uniqueIds, data)),
inverted(convertRawProp(context, rawProps, "inverted", sourceProps.inverted, {})),
horizontal(convertRawProp(context, rawProps, "horizontal", sourceProps.horizontal, {})),
initialNumToRender(convertRawProp(context, rawProps, "initialNumToRender", sourceProps.initialNumToRender, {})),
initialScrollIndex(convertRawProp(context, rawProps, "initialScrollIndex", sourceProps.initialScrollIndex, {}))
{}

const SLContainerProps::SLContainerDataItem& SLContainerProps::getElementByIndex(int index) const {
if (index < 0 || index >= data.size()) {
if (index < 0 || index >= parsed.size()) {
throw std::out_of_range("Index out of range");
}
return data[index];
return parsed[index];
}

std::string SLContainerProps::getElementValueByPath(const SLContainerDataItem& element, const SLContainerDataItemPath& path) {
Expand Down
11 changes: 10 additions & 1 deletion cpp/react/renderer/components/SLContainerSpec/SLContainerProps.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
#include "SLKeyExtractor.h"
#include "json.hpp"

#ifndef RCT_DEBUG
#include <iostream>
#ifdef ANDROID
#include <android/log.h>
#endif
#endif

namespace facebook::react {

class SLContainerProps final : public ViewProps {
Expand All @@ -16,11 +23,13 @@ class SLContainerProps final : public ViewProps {
using SLContainerDataItem = nlohmann::json;
using SLContainerDataItemPath = std::string;

nlohmann::json data;
std::string data;
nlohmann::json parsed;
std::vector<std::string> uniqueIds;
bool inverted = false;
bool horizontal = false;
int initialNumToRender = 10;
int numColumns = 1;
int initialScrollIndex = 0;

const SLContainerDataItem& getElementByIndex(int index) const;
Expand Down
Loading
Loading