Skip to content

Commit

Permalink
feat: ios horizontal scroll support
Browse files Browse the repository at this point in the history
  • Loading branch information
azimgd committed May 17, 2024
1 parent 3940150 commit 7b99326
Show file tree
Hide file tree
Showing 16 changed files with 92 additions and 27 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import ShadowListContainer from 'shadowlist';
| `renderItem` | Function | Required | A function to render each item in the list. It receives an object with `item` and `index` properties. |
| `initialScrollIndex` | Number | Optional | The initial index of the item to scroll to when the list mounts. |
| `inverted` | Boolean | Optional | If true, the list will be rendered in an inverted order. |
| `horizontal` | Boolean | Optional | If true, renders items next to each other horizontally instead of stacked vertically. |

## Methods
| Method | Type | Description |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public ShadowListContainer createViewInstance(ThemedReactContext context) {
public void setInverted(ShadowListContainer view, boolean inverted) {
}

@Override
@ReactProp(name = "horizontal")
public void setHorizontal(ShadowListContainer view, boolean horizontal) {
}

@Override
public void scrollToIndex(ShadowListContainer view, int index) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case "inverted":
mViewManager.setInverted(view, value == null ? false : (boolean) value);
break;
case "horizontal":
mViewManager.setHorizontal(view, value == null ? false : (boolean) value);
break;

default:
super.setProperty(view, propName, value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

public interface ShadowListContainerManagerInterface<T extends View> {
void setInverted(T view, boolean value);
void setHorizontal(T view, boolean value);
void scrollToIndex(T view, int index);
void scrollToOffset(T view, int offset);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,36 @@

namespace facebook::react {

float Scrollable::getScrollPositionOffset(const Point& scrollPosition) {
return scrollPosition.y;
float Scrollable::getScrollPositionOffset(const Point& scrollPosition, bool horizontal) {
if (horizontal) {
return scrollPosition.x;
} else {
return scrollPosition.y;
}
}

float Scrollable::getScrollContentSize(const Size& scrollContent) {
return scrollContent.height;
float Scrollable::getScrollContentSize(const Size& scrollContent, bool horizontal) {
if (horizontal) {
return scrollContent.width;
} else {
return scrollContent.height;
}
}

float Scrollable::getScrollContainerSize(const Size& scrollContainer) {
return scrollContainer.height;
float Scrollable::getScrollContainerSize(const Size& scrollContainer, bool horizontal) {
if (horizontal) {
return scrollContainer.width;
} else {
return scrollContainer.height;
}
}

float Scrollable::getScrollContentItemSize(const Size& scrollContentItem) {
return scrollContentItem.height;
float Scrollable::getScrollContentItemSize(const Size& scrollContentItem, bool horizontal) {
if (horizontal) {
return scrollContentItem.width;
} else {
return scrollContentItem.height;
}
}

int Scrollable::getVirtualizedOffset() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ namespace facebook::react {

class Scrollable final {
public:
static float getScrollPositionOffset(const Point& scrollPosition);
static float getScrollContainerSize(const Size& scrollContainer);
static float getScrollContentSize(const Size& scrollContent);
static float getScrollContentItemSize(const Size& scrollContentItem);
static float getScrollPositionOffset(const Point& scrollPosition, bool horizontal);
static float getScrollContainerSize(const Size& scrollContainer, bool horizontal);
static float getScrollContentSize(const Size& scrollContent, bool horizontal);
static float getScrollContentItemSize(const Size& scrollContentItem, bool horizontal);
static int getVirtualizedOffset();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ ShadowListContainerProps::ShadowListContainerProps(
const RawProps &rawProps): ViewProps(context, sourceProps, rawProps),

inverted(convertRawProp(context, rawProps, "inverted", sourceProps.inverted, {false})),
horizontal(convertRawProp(context, rawProps, "horizontal", sourceProps.horizontal, {false})),
hasListHeaderComponent(convertRawProp(context, rawProps, "hasListHeaderComponent", sourceProps.hasListHeaderComponent, {false})),
hasListFooterComponent(convertRawProp(context, rawProps, "hasListFooterComponent", sourceProps.hasListFooterComponent, {false})),
initialScrollIndex(convertRawProp(context, rawProps, "initialScrollIndex", sourceProps.initialScrollIndex, {0}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class ShadowListContainerProps final : public ViewProps {
ShadowListContainerProps(const PropsParserContext& context, const ShadowListContainerProps &sourceProps, const RawProps &rawProps);

bool inverted{false};
bool horizontal{false};
bool hasListHeaderComponent{false};
bool hasListFooterComponent{false};
int initialScrollIndex{0};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ void ShadowListContainerShadowNode::layout(LayoutContext layoutContext) {
ensureUnsealed();
ConcreteShadowNode::layout(layoutContext);

calculateContainerMeasurements(layoutContext);
calculateContainerMeasurements(
layoutContext,
getConcreteProps().horizontal,
getConcreteProps().inverted
);

auto state = getStateData();

Expand All @@ -30,15 +34,15 @@ void ShadowListContainerShadowNode::layout(LayoutContext layoutContext) {
/*
* Measure visible container, and all childs aka list
*/
void ShadowListContainerShadowNode::calculateContainerMeasurements(LayoutContext layoutContext) {
void ShadowListContainerShadowNode::calculateContainerMeasurements(LayoutContext layoutContext, bool horizontal, bool inverted) {
auto scrollContent = Rect{};
auto scrollContentTree = ShadowListFenwickTree(yogaNode_.getChildCount());

for (std::size_t index = 0; index < yogaNode_.getChildCount(); ++index) {
auto childYogaNode = yogaNode_.getChild(index);
auto childNodeMetrics = shadowNodeFromContext(childYogaNode).getLayoutMetrics();
scrollContent.unionInPlace(childNodeMetrics.frame);
scrollContentTree[index] = Scrollable::getScrollContentItemSize(childNodeMetrics.frame.size);
scrollContentTree[index] = Scrollable::getScrollContentItemSize(childNodeMetrics.frame.size, horizontal);
}

scrollContent_ = scrollContent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class ShadowListContainerShadowNode final : public ConcreteViewShadowNode<

void layout(LayoutContext layoutContext) override;

void calculateContainerMeasurements(LayoutContext layoutContext);
void calculateContainerMeasurements(LayoutContext layoutContext, bool horizontal, bool inverted);

private:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ ShadowListContainerState::ShadowListContainerState(
/*
* Measure layout and children metrics
*/
ShadowListContainerExtendedMetrics ShadowListContainerState::calculateExtendedMetrics(Point scrollPosition) const {
ShadowListContainerExtendedMetrics ShadowListContainerState::calculateExtendedMetrics(
Point scrollPosition,
bool horizontal,
bool inverted) const {

auto virtualizedOffset = Scrollable::getVirtualizedOffset();
auto scrollPositionOffset = Scrollable::getScrollPositionOffset(scrollPosition);
auto scrollContentSize = Scrollable::getScrollContentSize(scrollContent);
auto scrollContainerSize = Scrollable::getScrollContainerSize(scrollContainer);
auto scrollPositionOffset = Scrollable::getScrollPositionOffset(scrollPosition, horizontal);
auto scrollContentSize = Scrollable::getScrollContentSize(scrollContent, horizontal);
auto scrollContainerSize = Scrollable::getScrollContainerSize(scrollContainer, horizontal);

auto visibleStartPixels = std::max<float>(0.f, static_cast<double>(scrollPositionOffset));
auto visibleEndPixels = std::min<float>(scrollContentSize, scrollPositionOffset + scrollContainerSize);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ class ShadowListContainerState {
/*
* Measure layout and children metrics
*/
ShadowListContainerExtendedMetrics calculateExtendedMetrics(Point scrollPosition) const;
ShadowListContainerExtendedMetrics calculateExtendedMetrics(
Point scrollPosition,
bool horizontal,
bool inverted) const;
ShadowListContainerLayoutMetrics calculateLayoutMetrics() const;
float calculateItemOffset(int index) const;
int countTree() const;
Expand Down
2 changes: 2 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export const ShadowListExample = ({ data }: { data: any[] }) => {
renderItem={({ item, index }) => (
<CustomComponent item={item} index={index} />
)}
horizontal
initialScrollIndex={100}
/>
);
};
Expand Down
23 changes: 19 additions & 4 deletions ios/ShadowListContainer.mm
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ @implementation ShadowListContainer {
CachedComponentPool *_cachedComponentPool;
int _cachedComponentPoolDriftCount;
BOOL _scrollContainerLayoutComplete;
BOOL _scrollContainerLayoutHorizontal;
BOOL _scrollContainerLayoutInverted;
}

+ (ComponentDescriptorProvider)componentDescriptorProvider
Expand All @@ -37,6 +39,8 @@ - (instancetype)initWithFrame:(CGRect)frame

_cachedComponentPoolDriftCount = 0;
_scrollContainerLayoutComplete = false;
_scrollContainerLayoutInverted = defaultProps->inverted;
_scrollContainerLayoutHorizontal = defaultProps->horizontal;

_props = defaultProps;
_scrollContent = [UIView new];
Expand Down Expand Up @@ -66,6 +70,9 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
const auto &oldConcreteProps = static_cast<const ShadowListContainerProps &>(*_props);
const auto &newConcreteProps = static_cast<const ShadowListContainerProps &>(*props);

self->_scrollContainerLayoutInverted = newConcreteProps.inverted;
self->_scrollContainerLayoutHorizontal = newConcreteProps.horizontal;

[super updateProps:props oldProps:oldProps];
}

Expand All @@ -83,8 +90,8 @@ - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &
auto nextInitialScrollIndex = props.initialScrollIndex + (props.hasListHeaderComponent ? 1 : 0);
[self scrollRespectfully:stateData.calculateItemOffset(nextInitialScrollIndex) animated:false];
} else if (!self->_scrollContainerLayoutComplete && props.inverted) {
auto scrollContainerSize = Scrollable::getScrollContentSize(stateData.scrollContainer);
auto scrollContentSize = Scrollable::getScrollContentSize(stateData.scrollContent);
auto scrollContainerSize = Scrollable::getScrollContentSize(stateData.scrollContainer, self->_scrollContainerLayoutHorizontal);
auto scrollContentSize = Scrollable::getScrollContentSize(stateData.scrollContent, self->_scrollContainerLayoutHorizontal);
[self scrollRespectfully:(scrollContentSize - scrollContainerSize) animated:false];
}

Expand Down Expand Up @@ -121,7 +128,11 @@ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompo

- (void)scrollRespectfully:(float)contentOffset animated:(BOOL)animated
{
[self->_scrollContainer setContentOffset:CGPointMake(0, contentOffset) animated:animated];
if (self->_scrollContainerLayoutInverted) {
[self->_scrollContainer setContentOffset:CGPointMake(contentOffset, 0) animated:animated];
} else {
[self->_scrollContainer setContentOffset:CGPointMake(0, contentOffset) animated:animated];
}
}

#pragma mark - NativeCommands handlers
Expand Down Expand Up @@ -149,7 +160,11 @@ - (void)scrollToOffsetNativeCommand:(int)offset
- (void)recycle {
assert(std::dynamic_pointer_cast<ShadowListContainerShadowNode::ConcreteState const>(_state));
auto &stateData = _state->getData();
auto extendedMetrics = stateData.calculateExtendedMetrics(RCTPointFromCGPoint(self->_scrollContainer.contentOffset));
auto extendedMetrics = stateData.calculateExtendedMetrics(
RCTPointFromCGPoint(self->_scrollContainer.contentOffset),
self->_scrollContainerLayoutHorizontal,
self->_scrollContainerLayoutInverted
);
[self->_cachedComponentPool recycle:extendedMetrics.visibleStartIndex visibleEndIndex:extendedMetrics.visibleEndIndex];
}

Expand Down
1 change: 1 addition & 0 deletions src/ShadowListContainerNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {

export interface NativeProps extends ViewProps {
inverted?: boolean;
horizontal?: boolean;
hasListHeaderComponent?: boolean;
hasListFooterComponent?: boolean;
initialScrollIndex?: Int32;
Expand Down
12 changes: 10 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { type ViewStyle } from 'react-native';
import { StyleSheet, type ViewStyle } from 'react-native';
import ShadowListContainerNativeComponent, {
Commands,
type NativeProps,
Expand Down Expand Up @@ -50,12 +50,14 @@ const ShadowListContainerWrapper = (

const data = props.inverted ? props.data.reverse() : props.data;

const horizontalStyle = props.horizontal ? styles.horizontal : {};

return (
<ShadowListContainerNativeComponent
ref={instanceRef}
hasListHeaderComponent={!!props.ListHeaderComponent}
hasListFooterComponent={!!props.ListFooterComponent}
style={props.contentContainerStyle}
style={[props.contentContainerStyle, horizontalStyle]}
>
{props.ListHeaderComponent ? (
<ShadowListItemNativeComponent
Expand Down Expand Up @@ -90,4 +92,10 @@ const ShadowListContainerWrapper = (
);
};

const styles = StyleSheet.create({
horizontal: {
flexDirection: 'row',
},
});

export default React.forwardRef(ShadowListContainerWrapper);

0 comments on commit 7b99326

Please sign in to comment.