Skip to content

Commit

Permalink
Merge pull request #15 from azimgd/multicol
Browse files Browse the repository at this point in the history
Add masonry layout with multiple column option
  • Loading branch information
azimgd authored Jan 9, 2025
2 parents 27eb07a + 37f7427 commit c096841
Show file tree
Hide file tree
Showing 15 changed files with 208 additions and 93 deletions.
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

0 comments on commit c096841

Please sign in to comment.