diff --git a/README.md b/README.md index f71e3ca..f1661c5 100644 --- a/README.md +++ b/README.md @@ -52,27 +52,30 @@ import ShadowListContainer from 'shadowlist'; ## API | Prop | Type | Required | Description | |----------------------------|---------------------------|----------|-------------------------------------------------| -| `data` | Array | Required | An array of data to be rendered in the list. | -| `contentContainerStyle` | ViewStyle | Optional | These styles will be applied to the scroll view content container which wraps all of the child views. | -| `ListHeaderComponent` | React component or null | Optional | A custom component to render at the top of the list. | +| `data` | Array | Required | An array of data to be rendered in the list. | +| `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. | | `ListHeaderComponentStyle` | ViewStyle | Optional | Styling for internal View for `ListHeaderComponent` | -| `ListFooterComponent` | React component or null | Optional | A custom component to render at the bottom of the list. | +| `ListFooterComponent` | React component | Optional | A custom component to render at the bottom of the list. | | `ListFooterComponentStyle` | ViewStyle | Optional | Styling for internal View for `ListFooterComponent` | -| `ListEmptyComponent` | React component or null | Optional | A custom component to render when the list is empty. | +| `ListEmptyComponent` | React component | Optional | A custom component to render when the list is empty. | | `ListEmptyComponentStyle` | ViewStyle | Optional | Styling for internal View for `ListEmptyComponent` | | `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. | -| `onBatchLayout` | `({ size: Int32 }) => void` | Optional | Called when a batch of layout calculations is complete. | -| `onEndReached` | `({ distanceFromEnd: Int32 }) => void` | Optional | Called when the end of the content is within `onEndReachedThreshold`. | +| `onEndReached` | Function | Optional | Called when the end of the content is within `onEndReachedThreshold`. | | `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. | + ## Methods | Method | Type | Description | |-----------------|-------------------------------------|-----------------------------------------------------------| -| `scrollToIndex` | `({ index: number; animated: boolean }) => void` | Scrolls the list to the specified index. | -| `scrollToOffset`| `({ offset: number; animated: boolean }) => void` | Scrolls the list to the specified offset. | +| `scrollToIndex` | Function | Scrolls the list to the specified index. | +| `scrollToOffset`| Function | Scrolls the list to the specified offset. | ## Contributing @@ -81,4 +84,3 @@ See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the ## License MIT - diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/Scrollable.cpp b/cpp/react/renderer/components/RNShadowListContainerSpec/Scrollable.cpp deleted file mode 100644 index 0474d15..0000000 --- a/cpp/react/renderer/components/RNShadowListContainerSpec/Scrollable.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "Scrollable.h" - -namespace facebook::react { - -float Scrollable::getScrollPositionOffset(const Point& scrollPosition, bool horizontal) { - if (horizontal) { - return scrollPosition.x; - } else { - return scrollPosition.y; - } -} - -float Scrollable::getScrollContentSize(const Size& scrollContent, bool horizontal) { - if (horizontal) { - return scrollContent.width; - } else { - return scrollContent.height; - } -} - -float Scrollable::getScrollContainerSize(const Size& scrollContainer, bool horizontal) { - if (horizontal) { - return scrollContainer.width; - } else { - return scrollContainer.height; - } -} - -float Scrollable::getScrollContentItemSize(const Size& scrollContentItem, bool horizontal) { - if (horizontal) { - return scrollContentItem.width; - } else { - return scrollContentItem.height; - } -} - -int Scrollable::getVirtualizedOffset() { - return 10; -} - -} diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/Scrollable.h b/cpp/react/renderer/components/RNShadowListContainerSpec/Scrollable.h deleted file mode 100644 index c9e84b0..0000000 --- a/cpp/react/renderer/components/RNShadowListContainerSpec/Scrollable.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef Scrollable_h -#define Scrollable_h - -#include -#include -#include -#include - -namespace facebook::react { - -class Scrollable final { - public: - 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(); -}; - -} - -#endif diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerEventEmitter.cpp b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerEventEmitter.cpp index d743410..54cb4ab 100644 --- a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerEventEmitter.cpp +++ b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerEventEmitter.cpp @@ -11,20 +11,19 @@ void ShadowListContainerEventEmitter::onVisibleChange(VisibleMetrics $event) con }); } -void ShadowListContainerEventEmitter::onBatchLayout(BatchLayout $event) const { - dispatchEvent("batchLayout", [$event = std::move($event)](jsi::Runtime &runtime) { +void ShadowListContainerEventEmitter::onEndReached(EndReached $event) const { + dispatchEvent("endReached", [$event = std::move($event)](jsi::Runtime &runtime) { auto $payload = jsi::Object(runtime); - $payload.setProperty(runtime, "size", $event.size); + $payload.setProperty(runtime, "distanceFromEnd", $event.distanceFromEnd); return $payload; }); } -void ShadowListContainerEventEmitter::onEndReached(EndReached $event) const { - dispatchEvent("endReached", [$event = std::move($event)](jsi::Runtime &runtime) { +void ShadowListContainerEventEmitter::onStartReached(StartReached $event) const { + dispatchEvent("startReached", [$event = std::move($event)](jsi::Runtime &runtime) { auto $payload = jsi::Object(runtime); - $payload.setProperty(runtime, "distanceFromEnd", $event.distanceFromEnd); + $payload.setProperty(runtime, "distanceFromStart", $event.distanceFromStart); return $payload; }); } - } diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerEventEmitter.h b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerEventEmitter.h index 820183c..e85a9fe 100644 --- a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerEventEmitter.h +++ b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerEventEmitter.h @@ -13,17 +13,17 @@ class ShadowListContainerEventEmitter : public ViewEventEmitter { int end; }; - struct BatchLayout { - int size; - }; - struct EndReached { int distanceFromEnd; }; + struct StartReached { + int distanceFromStart; + }; + void onVisibleChange(VisibleMetrics value) const; - void onBatchLayout(BatchLayout value) const; void onEndReached(EndReached value) const; + void onStartReached(StartReached value) const; }; } diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerProps.cpp b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerProps.cpp index 99bdb35..ab72a0e 100644 --- a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerProps.cpp +++ b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerProps.cpp @@ -11,10 +11,9 @@ ShadowListContainerProps::ShadowListContainerProps( 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})), - onEndReachedThreshold(convertRawProp(context, rawProps, "onEndReachedThreshold", sourceProps.onEndReachedThreshold, {0})) + onEndReachedThreshold(convertRawProp(context, rawProps, "onEndReachedThreshold", sourceProps.onEndReachedThreshold, {0})), + onStartReachedThreshold(convertRawProp(context, rawProps, "onStartReachedThreshold", sourceProps.onStartReachedThreshold, {0})) {} } diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerProps.h b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerProps.h index 376ab59..03e2115 100644 --- a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerProps.h +++ b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerProps.h @@ -12,10 +12,9 @@ class ShadowListContainerProps final : public ViewProps { bool inverted{false}; bool horizontal{false}; - bool hasListHeaderComponent{false}; - bool hasListFooterComponent{false}; int initialScrollIndex{0}; double onEndReachedThreshold{0}; + double onStartReachedThreshold{0}; }; } diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerShadowNode.cpp b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerShadowNode.cpp index 3491051..6d9ae24 100644 --- a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerShadowNode.cpp +++ b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerShadowNode.cpp @@ -4,70 +4,4 @@ namespace facebook::react { extern const char ShadowListContainerComponentName[] = "ShadowListContainer"; -/* - * Native layout function - */ -void ShadowListContainerShadowNode::layout(LayoutContext layoutContext) { - ensureUnsealed(); - ConcreteShadowNode::layout(layoutContext); - - auto &props = getConcreteProps(); - auto state = getStateData(); - - calculateContainerMeasurements( - layoutContext, - props.horizontal, - props.inverted - ); - - if (scrollContainer_.size != state.scrollContainer) { - state.scrollContainer = scrollContainer_.size; - } - - if (scrollContent_.size != state.scrollContent) { - state.scrollContent = scrollContent_.size; - state.scrollContentTree = scrollContentTree_; - } - - if (props.initialScrollIndex && props.horizontal) { - state.scrollPosition = Point{state.calculateItemOffset(props.initialScrollIndex), 0}; - } else if (props.initialScrollIndex) { - state.scrollPosition = Point{0, state.calculateItemOffset(props.initialScrollIndex)}; - } else if (props.inverted && props.horizontal) { - state.scrollPosition = Point{scrollContent_.size.width - scrollContainer_.size.width, 0}; - } else if (props.inverted) { - state.scrollPosition = Point{0, scrollContent_.size.height - scrollContainer_.size.height}; - } else { - state.scrollPosition = Point{0, 0}; - } - - setStateData(std::move(state)); - - getConcreteEventEmitter().onBatchLayout({ - .size = static_cast(scrollContentTree_.size()) - }); -} - -/* - * Measure visible container, and all childs aka list - */ -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, horizontal); - } - - scrollContent_ = scrollContent; - scrollContainer_ = getLayoutMetrics().frame; - scrollContentTree_ = scrollContentTree; -} - -YogaLayoutableShadowNode& ShadowListContainerShadowNode::shadowNodeFromContext(YGNodeConstRef yogaNode) { - return dynamic_cast(*static_cast(YGNodeGetContext(yogaNode))); -} } diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerShadowNode.h b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerShadowNode.h index a9f13aa..fd2b7cc 100644 --- a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerShadowNode.h +++ b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerShadowNode.h @@ -3,7 +3,6 @@ #include "ShadowListContainerEventEmitter.h" #include "ShadowListContainerProps.h" #include "ShadowListContainerState.h" -#include "ShadowListFenwickTree.hpp" #include #include #include @@ -26,24 +25,6 @@ class ShadowListContainerShadowNode final : public ConcreteViewShadowNode< public: using ConcreteViewShadowNode::ConcreteViewShadowNode; - - void layout(LayoutContext layoutContext) override; - - void calculateContainerMeasurements(LayoutContext layoutContext, bool horizontal, bool inverted); - - private: - - /* - * Measurements - */ - Rect scrollContainer_; - Rect scrollContent_; - ShadowListFenwickTree scrollContentTree_; - - /* - * Caster - */ - static YogaLayoutableShadowNode& shadowNodeFromContext(YGNodeConstRef yogaNode); }; } diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerState.cpp b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerState.cpp index b851eed..95ea99c 100644 --- a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerState.cpp +++ b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerState.cpp @@ -5,81 +5,9 @@ namespace facebook::react { ShadowListContainerState::ShadowListContainerState( Point scrollPosition, Size scrollContainer, - Size scrollContent, - ShadowListFenwickTree scrollContentTree) : + Size scrollContent) : scrollPosition(scrollPosition), scrollContainer(scrollContainer), - scrollContent(scrollContent), - scrollContentTree(scrollContentTree) {} - -/* - * Measure layout and children metrics - */ -ShadowListContainerExtendedMetrics ShadowListContainerState::calculateExtendedMetrics( - Point scrollPosition, - bool horizontal, - bool inverted) const { - - auto virtualizedOffset = Scrollable::getVirtualizedOffset(); - auto scrollPositionOffset = Scrollable::getScrollPositionOffset(scrollPosition, horizontal); - auto scrollContentSize = Scrollable::getScrollContentSize(scrollContent, horizontal); - auto scrollContainerSize = Scrollable::getScrollContainerSize(scrollContainer, horizontal); - - auto visibleStartPixels = std::max(0.f, static_cast(scrollPositionOffset)); - auto visibleEndPixels = std::min(scrollContentSize, scrollPositionOffset + scrollContainerSize); - - int visibleStartIndex = scrollContentTree.lower_bound(visibleStartPixels); - visibleStartIndex = std::max(0, visibleStartIndex - virtualizedOffset); - - int visibleEndIndex = scrollContentTree.lower_bound(visibleEndPixels); - visibleEndIndex = std::min(scrollContentTree.size(), size_t(visibleEndIndex + virtualizedOffset)); - - int blankTopStartIndex = 0; - int blankTopEndIndex = std::max(0, visibleStartIndex - 1); - - auto blankTopStartPixels = 0.0; - auto blankTopEndPixels = scrollContentTree.sum(blankTopStartIndex, blankTopEndIndex); - - int blankBottomStartIndex = std::min(size_t(visibleEndIndex + 1), scrollContentTree.size()); - int blankBottomEndIndex = scrollContentTree.size(); - - auto blankBottomStartPixels = scrollContentTree.sum(blankBottomStartIndex, scrollContentTree.size()); - auto blankBottomEndPixels = scrollContentTree.sum(0, scrollContentTree.size()); - - return ShadowListContainerExtendedMetrics{ - visibleStartIndex, - visibleEndIndex, - visibleStartPixels, - visibleEndPixels, - blankTopStartIndex, - blankTopEndIndex, - blankTopStartPixels, - blankTopEndPixels, - blankBottomStartIndex, - blankBottomEndIndex, - blankBottomStartPixels, - blankBottomEndPixels, - }; -} - -/* - * Measure layout - */ -ShadowListContainerLayoutMetrics ShadowListContainerState::calculateLayoutMetrics() const { - auto height = scrollContentTree.sum(0, scrollContentTree.size()); - - return ShadowListContainerLayoutMetrics{ - height - }; -} - -float ShadowListContainerState::calculateItemOffset(int index) const { - return scrollContentTree.sum(0, index); -} - -int ShadowListContainerState::countTree() const { - return scrollContentTree.size(); -} - + scrollContent(scrollContent) {} } diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerState.h b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerState.h index 78a0e2b..4b53614 100644 --- a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerState.h +++ b/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListContainerState.h @@ -1,7 +1,5 @@ #pragma once -#include "ShadowListFenwickTree.hpp" -#include "Scrollable.h" #include #include @@ -28,38 +26,12 @@ constexpr static MapBuffer::Key CX_STATE_KEY_SCROLL_CONTENT_WIDTH = 0; constexpr static MapBuffer::Key CX_STATE_KEY_SCROLL_CONTENT_HEIGHT = 1; #endif - -struct ShadowListContainerLayoutMetrics { - double height; -}; - -struct ShadowListContainerExtendedMetrics { - int visibleStartIndex; - int visibleEndIndex; - - double visibleStartPixels; - double visibleEndPixels; - - int blankTopStartIndex; - int blankTopEndIndex; - - double blankTopStartPixels; - double blankTopEndPixels; - - int blankBottomStartIndex; - int blankBottomEndIndex; - - double blankBottomStartPixels; - double blankBottomEndPixels; -}; - class ShadowListContainerState { public: ShadowListContainerState( Point scrollPosition, Size scrollContainer, - Size scrollContent, - ShadowListFenwickTree scrollContentTree); + Size scrollContent); ShadowListContainerState() = default; /* @@ -70,69 +42,6 @@ class ShadowListContainerState { Point scrollPosition; Size scrollContainer; Size scrollContent; - - /* - * Binary tree, expensive for updates, cheap for reads - */ - ShadowListFenwickTree scrollContentTree; - - /* - * Measure layout and children metrics - */ - ShadowListContainerExtendedMetrics calculateExtendedMetrics( - Point scrollPosition, - bool horizontal, - bool inverted) const; - ShadowListContainerLayoutMetrics calculateLayoutMetrics() const; - float calculateItemOffset(int index) const; - int countTree() const; - -#ifdef ANDROID - ShadowListContainerState(ShadowListContainerState const &previousState, folly::dynamic data){}; - - folly::dynamic getDynamic() const { - folly::dynamic newState = folly::dynamic::object(); - - folly::dynamic newScrollPosition = folly::dynamic::object(); - newScrollPosition["x"] = this->scrollPosition.x; - newScrollPosition["y"] = this->scrollPosition.y; - newState["scrollPosition"] = newScrollPosition; - - folly::dynamic newScrollContainer = folly::dynamic::object(); - newScrollContainer["height"] = this->scrollContainer.height; - newScrollContainer["width"] = this->scrollContainer.width; - newState["scrollContainer"] = newScrollContainer; - - folly::dynamic newScrollContent = folly::dynamic::object(); - newScrollContent["height"] = this->scrollContent.height; - newScrollContent["width"] = this->scrollContent.width; - newState["scrollContent"] = newScrollContent; - - return newState; - }; - - MapBuffer getMapBuffer() const { - auto builder = MapBufferBuilder(); - - auto scrollPositionMapBuffer = MapBufferBuilder(); - scrollPositionMapBuffer.putDouble(CX_STATE_KEY_SCROLL_POSITION_X, this->scrollPosition.x); - scrollPositionMapBuffer.putDouble(CX_STATE_KEY_SCROLL_POSITION_Y, this->scrollPosition.y); - builder.putMapBuffer(CX_STATE_KEY_SCROLL_POSITION, scrollPositionMapBuffer.build()); - - auto scrollContainerMapBuffer = MapBufferBuilder(); - scrollContainerMapBuffer.putDouble(CX_STATE_KEY_SCROLL_CONTAINER_WIDTH, this->scrollContainer.width); - scrollContainerMapBuffer.putDouble(CX_STATE_KEY_SCROLL_CONTAINER_HEIGHT, this->scrollContainer.height); - builder.putMapBuffer(CX_STATE_KEY_SCROLL_CONTAINER, scrollContainerMapBuffer.build()); - - auto scrollContentMapBuffer = MapBufferBuilder(); - scrollContentMapBuffer.putDouble(CX_STATE_KEY_SCROLL_CONTENT_WIDTH, this->scrollContent.width); - scrollContentMapBuffer.putDouble(CX_STATE_KEY_SCROLL_CONTENT_HEIGHT, this->scrollContent.height); - builder.putMapBuffer(CX_STATE_KEY_SCROLL_CONTENT, scrollContentMapBuffer.build()); - - return builder.build(); - }; -#endif - }; } diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ComponentDescriptors.h b/cpp/react/renderer/components/RNShadowListContentSpec/ComponentDescriptors.h new file mode 100644 index 0000000..6117d26 --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ComponentDescriptors.h @@ -0,0 +1 @@ +#include "ShadowListContentComponentDescriptor.h" diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentComponentDescriptor.cpp b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentComponentDescriptor.cpp new file mode 100644 index 0000000..c390d41 --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentComponentDescriptor.cpp @@ -0,0 +1,12 @@ +#include "ShadowListContentComponentDescriptor.h" +#include +#include + +namespace facebook::react { + +void RNShadowListContentSpec_registerComponentDescriptorsFromCodegen( + std::shared_ptr registry) { + registry->add(concreteComponentDescriptorProvider()); +} + +} diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentComponentDescriptor.h b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentComponentDescriptor.h new file mode 100644 index 0000000..5a8635b --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentComponentDescriptor.h @@ -0,0 +1,21 @@ +#pragma once + +#include "ShadowListContentShadowNode.h" +#include +#include +#include + +namespace facebook::react { + +class ShadowListContentComponentDescriptor : public ConcreteComponentDescriptor { + using ConcreteComponentDescriptor::ConcreteComponentDescriptor; + + void adopt(ShadowNode& shadowNode) const override { + ConcreteComponentDescriptor::adopt(shadowNode); + } +}; + +void RNShadowListContentSpec_registerComponentDescriptorsFromCodegen( + std::shared_ptr registry); + +} diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentEventEmitter.cpp b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentEventEmitter.cpp new file mode 100644 index 0000000..33e72cb --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentEventEmitter.cpp @@ -0,0 +1,5 @@ +#include "ShadowListContentEventEmitter.h" + +namespace facebook::react { + +} diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentEventEmitter.h b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentEventEmitter.h new file mode 100644 index 0000000..dff316e --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentEventEmitter.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace facebook::react { + +class ShadowListContentEventEmitter : public ViewEventEmitter { + public: + using ViewEventEmitter::ViewEventEmitter; + +}; + +} diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentHelpers.h b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentHelpers.h new file mode 100644 index 0000000..4ccf4b7 --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentHelpers.h @@ -0,0 +1,11 @@ +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol RCTShadowListContentViewProtocol + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentProps.cpp b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentProps.cpp new file mode 100644 index 0000000..d497195 --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentProps.cpp @@ -0,0 +1,18 @@ +#include "ShadowListContentProps.h" +#include +#include + +namespace facebook::react { + +ShadowListContentProps::ShadowListContentProps( + const PropsParserContext &context, + const ShadowListContentProps &sourceProps, + 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})) + {} + +} diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentProps.h b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentProps.h new file mode 100644 index 0000000..744feba --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentProps.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace facebook::react { + +class ShadowListContentProps final : public ViewProps { + public: + ShadowListContentProps() = default; + ShadowListContentProps(const PropsParserContext& context, const ShadowListContentProps &sourceProps, const RawProps &rawProps); + + bool inverted{false}; + bool horizontal{false}; + bool hasListHeaderComponent{false}; + bool hasListFooterComponent{false}; +}; + +} diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentShadowNode.cpp b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentShadowNode.cpp new file mode 100644 index 0000000..61a7631 --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentShadowNode.cpp @@ -0,0 +1,48 @@ +#include "ShadowListContentShadowNode.h" + +namespace facebook::react { + +extern const char ShadowListContentComponentName[] = "ShadowListContent"; + +/* + * Native layout function + */ +void ShadowListContentShadowNode::layout(LayoutContext layoutContext) { + ensureUnsealed(); + ConcreteShadowNode::layout(layoutContext); + + auto &props = getConcreteProps(); + auto state = getStateData(); + + state.contentViewMeasurements = calculateContentViewMeasurements( + layoutContext, + props.horizontal, + props.inverted + ); + setStateData(std::move(state)); +} + +/* + * Measure visible container, and all childs aka list + */ +ShadowListFenwickTree ShadowListContentShadowNode::calculateContentViewMeasurements(LayoutContext layoutContext, bool horizontal, bool inverted) { + auto contentViewMeasurements = ShadowListFenwickTree(yogaNode_.getChildCount()); + + for (std::size_t index = 0; index < yogaNode_.getChildCount(); ++index) { + auto childYogaNode = yogaNode_.getChild(index); + auto childNodeMetrics = shadowNodeFromContext(childYogaNode).getLayoutMetrics(); + + if (horizontal) { + contentViewMeasurements[index] = childNodeMetrics.frame.size.width; + } else { + contentViewMeasurements[index] = childNodeMetrics.frame.size.height; + } + } + + return contentViewMeasurements; +} + +YogaLayoutableShadowNode& ShadowListContentShadowNode::shadowNodeFromContext(YGNodeConstRef yogaNode) { + return dynamic_cast(*static_cast(YGNodeGetContext(yogaNode))); +} +} diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentShadowNode.h b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentShadowNode.h new file mode 100644 index 0000000..cd08916 --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentShadowNode.h @@ -0,0 +1,46 @@ +#pragma once + +#include "ShadowListContentEventEmitter.h" +#include "ShadowListContentProps.h" +#include "ShadowListContentState.h" +#include "ShadowListFenwickTree.hpp" +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +JSI_EXPORT extern const char ShadowListContentComponentName[]; + +/* + * `ShadowNode` for component. + */ +class ShadowListContentShadowNode final : public ConcreteViewShadowNode< + ShadowListContentComponentName, + ShadowListContentProps, + ShadowListContentEventEmitter, + ShadowListContentState> { + + public: + using ConcreteViewShadowNode::ConcreteViewShadowNode; + + void layout(LayoutContext layoutContext) override; + + /* + * Measure children and return Fenwick BIT + */ + ShadowListFenwickTree calculateContentViewMeasurements( + LayoutContext layoutContext, + bool horizontal, + bool inverted); + + /* + * Caster + */ + static YogaLayoutableShadowNode& shadowNodeFromContext(YGNodeConstRef yogaNode); +}; + +} diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentState.cpp b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentState.cpp new file mode 100644 index 0000000..2c41c8e --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentState.cpp @@ -0,0 +1,9 @@ +#include "ShadowListContentState.h" + +namespace facebook::react { + +ShadowListContentState::ShadowListContentState( + ShadowListFenwickTree contentViewMeasurements) : + contentViewMeasurements(contentViewMeasurements) {} + +} diff --git a/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentState.h b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentState.h new file mode 100644 index 0000000..c1838fe --- /dev/null +++ b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListContentState.h @@ -0,0 +1,26 @@ +#pragma once + +#include "ShadowListFenwickTree.hpp" +#include +#include + +#ifdef ANDROID +#include +#include +#include +#endif + +namespace facebook::react { + +class ShadowListContentState { + public: + ShadowListContentState(ShadowListFenwickTree contentViewMeasurements); + ShadowListContentState() = default; + + /* + * Binary tree, expensive for updates, cheap for reads + */ + ShadowListFenwickTree contentViewMeasurements; +}; + +} diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListFenwickTree.cpp b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListFenwickTree.cpp similarity index 100% rename from cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListFenwickTree.cpp rename to cpp/react/renderer/components/RNShadowListContentSpec/ShadowListFenwickTree.cpp diff --git a/cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListFenwickTree.hpp b/cpp/react/renderer/components/RNShadowListContentSpec/ShadowListFenwickTree.hpp similarity index 100% rename from cpp/react/renderer/components/RNShadowListContainerSpec/ShadowListFenwickTree.hpp rename to cpp/react/renderer/components/RNShadowListContentSpec/ShadowListFenwickTree.hpp diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9d12798..4153046 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1165,7 +1165,7 @@ PODS: - React-utils (= 0.74.0) - RNFlashList (1.6.4): - React-Core - - shadowlist (0.4.2): + - shadowlist (0.4.3): - DoubleConversion - glog - hermes-engine @@ -1423,7 +1423,7 @@ SPEC CHECKSUMS: React-utils: f013537c3371270d2095bff1d594d00d4bc9261b ReactCommon: 2cde697fd80bd31da1d6448d25a5803088585219 RNFlashList: b521ebdd7f9352673817f1d98e8bdc0c8cf8545b - shadowlist: 6eac35f8ef348a14b845a55008d2bbb3338cc077 + shadowlist: f09672cf7ae13d71b8b090a695bd2d588b48e376 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 56f906bf6c11c931588191dde1229fd3e4e3d557 diff --git a/example/src/App.tsx b/example/src/App.tsx index d691fa2..2b56fcd 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -107,6 +107,13 @@ export const ShadowListExample = ({ data }: { data: any[] }) => { renderItem={({ item, index }) => ( )} + inverted={false} + horizontal={false} + initialScrollIndex={0} + onEndReached={(event) => console.log(event.nativeEvent)} + onEndReachedThreshold={2} + onStartReached={(event) => console.log(event.nativeEvent)} + onStartReachedThreshold={2} /> ); }; diff --git a/ios/ShadowListContainer.h b/ios/ShadowListContainer.h index 8741106..cdd0091 100644 --- a/ios/ShadowListContainer.h +++ b/ios/ShadowListContainer.h @@ -1,4 +1,6 @@ #ifdef RCT_NEW_ARCH_ENABLED +#import "ShadowListContainerDelegate.h" +#import "ShadowListContentDelegate.h" #import #import @@ -7,8 +9,8 @@ NS_ASSUME_NONNULL_BEGIN -@interface ShadowListContainer : RCTViewComponentView - +@interface ShadowListContainer : RCTViewComponentView +@property (nonatomic, weak) id delegate; @end NS_ASSUME_NONNULL_END diff --git a/ios/ShadowListContainer.mm b/ios/ShadowListContainer.mm index 400c160..1b24fc5 100644 --- a/ios/ShadowListContainer.mm +++ b/ios/ShadowListContainer.mm @@ -1,11 +1,10 @@ #import "ShadowListContainer.h" +#import "ShadowListContent.h" #import "ShadowListContainerComponentDescriptor.h" #import "ShadowListContainerEventEmitter.h" #import "ShadowListContainerProps.h" #import "ShadowListContainerHelpers.h" -#import "Scrollable.h" -#import "CachedComponentPool/CachedComponentPool.h" #import "RCTConversions.h" #import "RCTFabricComponentsPlugins.h" @@ -17,12 +16,8 @@ @interface ShadowListContainer () @end @implementation ShadowListContainer { - UIScrollView* _scrollContainer; + UIScrollView* _contentView; ShadowListContainerShadowNode::ConcreteState::Shared _state; - CachedComponentPool *_cachedComponentPool; - int _cachedComponentPoolDriftCount; - BOOL _scrollContainerLayoutHorizontal; - BOOL _scrollContainerLayoutInverted; BOOL _scrollContainerScrolling; } @@ -35,27 +30,11 @@ - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared(); - - _cachedComponentPoolDriftCount = 0; - _scrollContainerLayoutInverted = defaultProps->inverted; - _scrollContainerLayoutHorizontal = defaultProps->horizontal; - _props = defaultProps; - _scrollContainer = [UIScrollView new]; - _scrollContainer.delegate = self; - _scrollContainer.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - - self.contentView = _scrollContainer; - - auto onCachedComponentMount = ^(NSInteger poolIndex) { - [self->_scrollContainer insertSubview:[self->_cachedComponentPool getComponentView:poolIndex] atIndex:poolIndex]; - }; - auto onCachedComponentUnmount = ^(NSInteger poolIndex) { - [[self->_cachedComponentPool getComponentView:poolIndex] removeFromSuperview]; - }; - _cachedComponentPool = [[CachedComponentPool alloc] initWithObservable:@[] - onCachedComponentMount:onCachedComponentMount - onCachedComponentUnmount:onCachedComponentUnmount]; + _contentView = [UIScrollView new]; + _contentView.delegate = self; + _contentView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + self.contentView = _contentView; } return self; @@ -63,29 +42,11 @@ - (instancetype)initWithFrame:(CGRect)frame - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps { - const auto &oldConcreteProps = static_cast(*_props); - const auto &newConcreteProps = static_cast(*props); - - self->_scrollContainerLayoutInverted = newConcreteProps.inverted; - self->_scrollContainerLayoutHorizontal = newConcreteProps.horizontal; - [super updateProps:props oldProps:oldProps]; } - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState { - assert(std::dynamic_pointer_cast(state)); - self->_state = std::static_pointer_cast(state); - const auto &stateData = _state->getData(); - const auto &props = static_cast(*_props); - - self->_scrollContainer.contentSize = RCTCGSizeFromSize(stateData.scrollContent); - self->_scrollContainer.frame.size = RCTCGSizeFromSize(stateData.scrollContainer); - self->_scrollContainer.contentOffset = RCTCGPointFromPoint(stateData.scrollPosition); - - _cachedComponentPoolDriftCount = stateData.countTree() - [self->_cachedComponentPool countPool]; - - [self recycle]; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { @@ -113,65 +74,133 @@ - (void)scrollViewDidEndScrolling:(UIScrollView *)scrollView { - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (_scrollContainerScrolling) { - return self->_scrollContainer; + return self->_contentView; } return [super hitTest:point withEvent:event]; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { - const auto &props = static_cast(*_props); const auto &eventEmitter = static_cast(*_eventEmitter); + + if ([self.delegate respondsToSelector:@selector(listContainerScrollOffsetChange:)]) { + CGPoint listContainerScrollOffset = scrollView.contentOffset; + [self.delegate listContainerScrollOffsetChange:listContainerScrollOffset]; + } - auto distanceFromEnd = [self distanceFromEndRespectfully:props.onEndReachedThreshold]; + int distanceFromEnd = [self measureDistanceFromEnd]; + int distanceFromStart = [self measureDistanceFromStart]; + if (distanceFromEnd > 0) { eventEmitter.onEndReached({ distanceFromEnd = distanceFromEnd }); } - [self recycle]; + if (distanceFromStart > 0) { + eventEmitter.onStartReached({ distanceFromStart = distanceFromStart }); + } } - (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { - if ([childComponentView superview]) [childComponentView removeFromSuperview]; - [self->_cachedComponentPool insertCachedComponentPoolItem:childComponentView poolIndex:index]; - - if (_cachedComponentPoolDriftCount > 0) { - _cachedComponentPoolDriftCount -= 1; - if (_cachedComponentPoolDriftCount == 0) [self recycle]; - } + [self->_contentView mountChildComponentView:childComponentView index:index]; } - (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { - if ([childComponentView superview]) [childComponentView removeFromSuperview]; - [self->_cachedComponentPool removeCachedComponentPoolItem:childComponentView poolIndex:index]; + [self->_contentView unmountChildComponentView:childComponentView index:index]; } -- (int)distanceFromEndRespectfully:(float)threshold { - if (self->_scrollContainerLayoutHorizontal) { - auto containerSize = self->_scrollContainer.bounds.size.width; - auto contentSize = self->_scrollContainer.contentSize.width; - auto offset = self->_scrollContainer.contentOffset.x; +- (void)layoutSubviews +{ + const auto &props = static_cast(*_props); + + RCTAssert( + [self->_contentView.subviews.firstObject isKindOfClass:ShadowListContent.class], + @"ShadowListContainer must be an ancestor of ShadowListContent"); + ShadowListContent *shadowListContent = self->_contentView.subviews.firstObject; + shadowListContent.delegate = self; + + /* + * Scrollbar position adjustments when initialScrollIndex not provided + */ + if (props.initialScrollIndex && [self.delegate respondsToSelector:@selector(listContainerScrollFocusIndexChange:)]) { + CGPoint listContainerScrollOffset = [self.delegate listContainerScrollFocusIndexChange:props.initialScrollIndex]; + [self->_contentView setContentOffset:listContainerScrollOffset]; + } + + /* + * Scrollbar position adjustments when initialScrollIndex not provided + */ + if (!props.initialScrollIndex && [self.delegate respondsToSelector:@selector(listContainerScrollFocusOffsetChange:)]) { + NSInteger offset = 0; + + if (props.horizontal && props.inverted) { + offset = MAX(self->_contentView.contentSize.width - self->_contentView.frame.size.width, 0); + } else if (!props.horizontal && props.inverted) { + offset = MAX(self->_contentView.contentSize.height - self->_contentView.frame.size.height, 0); + } + + /* + * Manually trigger scrollevent for non-inverted list to run virtualization + */ + if (!offset) { + [self scrollViewDidScroll:self->_contentView]; + } - auto triggerPoint = contentSize - (threshold * containerSize); - return offset >= triggerPoint ? (int)(contentSize - offset) : 0; + CGPoint listContainerScrollOffset = [self.delegate listContainerScrollFocusOffsetChange:offset]; + [self->_contentView setContentOffset:listContainerScrollOffset]; + } +} + +- (void)listContentSizeChange:(CGSize)listContentSize { + [self->_contentView setContentSize:listContentSize]; + + /* + * Stick scrollbar to bottom when scroll container is inverted vertically and to right when inverted horizontally + */ + const auto &props = static_cast(*_props); + if (props.horizontal && props.inverted) { + CGPoint nextContentOffset = CGPointMake(self->_contentView.contentSize.height - self->_contentView.frame.size.height, 0); + [self->_contentView setContentOffset:nextContentOffset]; + } else if (!props.horizontal && props.inverted) { + CGPoint nextContentOffset = CGPointMake(0, self->_contentView.contentSize.height - self->_contentView.frame.size.height); + [self->_contentView setContentOffset:nextContentOffset]; + } +} + +- (NSInteger)measureDistanceFromEnd { + const auto &props = static_cast(*_props); + + if (props.horizontal && props.inverted) { + auto triggerPoint = (props.onEndReachedThreshold * self->_contentView.frame.size.width); + return self->_contentView.contentOffset.x >= triggerPoint ? self->_contentView.contentOffset.x : 0; + } else if (!props.horizontal && props.inverted) { + auto triggerPoint = (props.onEndReachedThreshold * self->_contentView.frame.size.height); + return self->_contentView.contentOffset.y >= triggerPoint ? self->_contentView.contentOffset.y : 0; + } else if (props.horizontal && !props.inverted) { + auto triggerPoint = self->_contentView.contentSize.width - (props.onEndReachedThreshold * self->_contentView.frame.size.width); + return self->_contentView.contentOffset.x >= triggerPoint ? self->_contentView.contentSize.width - self->_contentView.contentOffset.x : 0; } else { - auto containerSize = self->_scrollContainer.bounds.size.height; - auto contentSize = self->_scrollContainer.contentSize.height; - auto offset = self->_scrollContainer.contentOffset.y; - - auto triggerPoint = contentSize - (threshold * containerSize); - return offset >= triggerPoint ? (int)(contentSize - offset) : 0; + auto triggerPoint = self->_contentView.contentSize.height - (props.onEndReachedThreshold * self->_contentView.frame.size.height); + return self->_contentView.contentOffset.y >= triggerPoint ? self->_contentView.contentSize.height - self->_contentView.contentOffset.y : 0; } } -- (void)scrollRespectfully:(float)contentOffset animated:(BOOL)animated -{ - if (self->_scrollContainerLayoutInverted) { - [self->_scrollContainer setContentOffset:CGPointMake(contentOffset, 0) animated:animated]; +- (NSInteger)measureDistanceFromStart { + const auto &props = static_cast(*_props); + + if (props.horizontal && props.inverted) { + auto triggerPoint = self->_contentView.contentSize.width - (props.onStartReachedThreshold * self->_contentView.frame.size.width); + return self->_contentView.contentOffset.x <= triggerPoint ? self->_contentView.contentSize.width - self->_contentView.contentOffset.x : 0; + } else if (!props.horizontal && props.inverted) { + auto triggerPoint = self->_contentView.contentSize.height - (props.onStartReachedThreshold * self->_contentView.frame.size.height); + return self->_contentView.contentOffset.y <= triggerPoint ? self->_contentView.contentSize.height - self->_contentView.contentOffset.y : 0; + } else if (props.horizontal && !props.inverted) { + auto triggerPoint = (props.onStartReachedThreshold * self->_contentView.frame.size.width); + return self->_contentView.contentOffset.x <= triggerPoint ? self->_contentView.contentOffset.x : 0; } else { - [self->_scrollContainer setContentOffset:CGPointMake(0, contentOffset) animated:animated]; + auto triggerPoint = (props.onStartReachedThreshold * self->_contentView.frame.size.height); + return self->_contentView.contentOffset.y <= triggerPoint ? self->_contentView.contentOffset.y : 0; } } @@ -184,28 +213,18 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args - (void)scrollToIndexNativeCommand:(int)index animated:(BOOL)animated { - auto &stateData = _state->getData(); - [self scrollRespectfully:stateData.calculateItemOffset(index) animated:animated]; - - [self recycle]; + if ([self.delegate respondsToSelector:@selector(listContainerScrollFocusIndexChange:)]) { + CGPoint listContainerScrollOffset = [self.delegate listContainerScrollFocusIndexChange:(NSInteger)index]; + [self->_contentView setContentOffset:listContainerScrollOffset]; + } } - (void)scrollToOffsetNativeCommand:(int)offset animated:(BOOL)animated { - [self scrollRespectfully:offset animated:animated]; - - [self recycle]; -} - -- (void)recycle { - assert(std::dynamic_pointer_cast(_state)); - auto &stateData = _state->getData(); - auto extendedMetrics = stateData.calculateExtendedMetrics( - RCTPointFromCGPoint(self->_scrollContainer.contentOffset), - self->_scrollContainerLayoutHorizontal, - self->_scrollContainerLayoutInverted - ); - [self->_cachedComponentPool recycle:extendedMetrics.visibleStartIndex visibleEndIndex:extendedMetrics.visibleEndIndex]; + if ([self.delegate respondsToSelector:@selector(listContainerScrollFocusOffsetChange:)]) { + CGPoint listContainerScrollOffset = [self.delegate listContainerScrollFocusOffsetChange:offset]; + [self->_contentView setContentOffset:listContainerScrollOffset]; + } } Class ShadowListContainerCls(void) diff --git a/ios/ShadowListContainerDelegate.h b/ios/ShadowListContainerDelegate.h new file mode 100644 index 0000000..5570bfe --- /dev/null +++ b/ios/ShadowListContainerDelegate.h @@ -0,0 +1,20 @@ +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import + +#ifndef ShadowListContainerDelegate_h +#define ShadowListContainerDelegate_h + +NS_ASSUME_NONNULL_BEGIN + +@protocol ShadowListContainerDelegate +@required +- (CGPoint)listContainerScrollOffsetChange:(CGPoint)listContainerScrollOffset; +- (CGPoint)listContainerScrollFocusIndexChange:(NSInteger)focusIndex; +- (CGPoint)listContainerScrollFocusOffsetChange:(NSInteger)focusOffset; +@end + +NS_ASSUME_NONNULL_END + +#endif +#endif diff --git a/ios/ShadowListContainerManager.mm b/ios/ShadowListContainerManager.mm index 18a15c0..b611405 100644 --- a/ios/ShadowListContainerManager.mm +++ b/ios/ShadowListContainerManager.mm @@ -15,7 +15,7 @@ - (UIView *)view } RCT_EXPORT_VIEW_PROPERTY(onVisibleChange, RCTDirectEventBlock) -RCT_EXPORT_VIEW_PROPERTY(onBatchLayout, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onEndReached, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onStartReached, RCTDirectEventBlock) @end diff --git a/ios/ShadowListContent.h b/ios/ShadowListContent.h new file mode 100644 index 0000000..88bd407 --- /dev/null +++ b/ios/ShadowListContent.h @@ -0,0 +1,19 @@ +#ifdef RCT_NEW_ARCH_ENABLED +#import "ShadowListContentDelegate.h" +#import "ShadowListContainerDelegate.h" +#import +#import + +#ifndef ShadowListContentNativeComponent_h +#define ShadowListContentNativeComponent_h + +NS_ASSUME_NONNULL_BEGIN + +@interface ShadowListContent : RCTViewComponentView +@property (nonatomic, weak) id delegate; +@end + +NS_ASSUME_NONNULL_END + +#endif +#endif diff --git a/ios/ShadowListContent.mm b/ios/ShadowListContent.mm new file mode 100644 index 0000000..1258ad7 --- /dev/null +++ b/ios/ShadowListContent.mm @@ -0,0 +1,199 @@ +#import "ShadowListContent.h" +#import "ShadowListContainer.h" + +#import "ShadowListContentComponentDescriptor.h" +#import "ShadowListContentEventEmitter.h" +#import "ShadowListContentProps.h" +#import "ShadowListContentHelpers.h" +#import "CachedComponentPool/CachedComponentPool.h" + +#import "RCTConversions.h" +#import "RCTFabricComponentsPlugins.h" + +using namespace facebook::react; + +@interface ShadowListContent () + +@end + +@implementation ShadowListContent { + UIView* _contentView; + ShadowListContentShadowNode::ConcreteState::Shared _state; + CachedComponentPool *_cachedComponentPool; +} + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + _contentView = [UIView new]; + self.contentView = _contentView; + + auto onCachedComponentMount = ^(NSInteger poolIndex) { + [self->_contentView insertSubview:[self->_cachedComponentPool getComponentView:poolIndex] atIndex:poolIndex]; + }; + auto onCachedComponentUnmount = ^(NSInteger poolIndex) { + [[self->_cachedComponentPool getComponentView:poolIndex] removeFromSuperview]; + }; + _cachedComponentPool = [[CachedComponentPool alloc] initWithObservable:@[] + onCachedComponentMount:onCachedComponentMount + onCachedComponentUnmount:onCachedComponentUnmount]; + } + + return self; +} + +- (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +{ + if ([childComponentView superview]) [childComponentView removeFromSuperview]; + [self->_cachedComponentPool insertCachedComponentPoolItem:childComponentView poolIndex:index]; +} + +- (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +{ + if ([childComponentView superview]) [childComponentView removeFromSuperview]; + [self->_cachedComponentPool removeCachedComponentPoolItem:childComponentView poolIndex:index]; +} + +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps +{ + [super updateProps:props oldProps:oldProps]; +} + +- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState +{ + assert(std::dynamic_pointer_cast(state)); + self->_state = std::static_pointer_cast(state); + const auto &stateData = _state->getData(); + const auto &props = static_cast(*_props); + + /* + * Direction agnostic sum of all sizes, height for vertical, width for horizontal + */ + const auto contentViewTotal = stateData.contentViewMeasurements.sum(stateData.contentViewMeasurements.size()); + + if ([self.delegate respondsToSelector:@selector(listContentSizeChange:)]) { + CGSize listContentSize; + + if (props.horizontal) { + listContentSize = CGSizeMake(contentViewTotal, self->_contentView.frame.size.height); + } else if (!props.horizontal) { + listContentSize = CGSizeMake(self->_contentView.frame.size.width, contentViewTotal); + } + + [self.delegate listContentSizeChange:listContentSize]; + } +} + +- (void)layoutSubviews +{ + RCTAssert( + [self.superview.superview isKindOfClass:ShadowListContainer.class], + @"ShadowListContent must be a descendant of ShadowListContainer"); + ShadowListContainer *shadowListContainer = (ShadowListContainer *)self.superview.superview; + shadowListContainer.delegate = self; +} + +- (CGPoint)listContainerScrollOffsetChange:(CGPoint)listContainerScrollOffset +{ + assert(std::dynamic_pointer_cast(self->_state)); + const auto &stateData = self->_state->getData(); + const auto &props = static_cast(*_props); + + /* + * Direction agnostic sum of all sizes, height for vertical, width for horizontal + */ + const auto contentViewCount = stateData.contentViewMeasurements.size(); + const auto contentViewTotal = stateData.contentViewMeasurements.sum(contentViewCount); + + /* + * Inverted scroll events on inverted scroll container + */ + NSInteger visibleStartIndex; + NSInteger visibleEndIndex; + NSInteger visibleStartOffset; + NSInteger visibleEndOffset; + CGPoint visibleOffset; + if (props.horizontal && props.inverted) { + visibleStartOffset = contentViewTotal - listContainerScrollOffset.x - self->_contentView.frame.size.width; + visibleEndOffset = contentViewTotal - listContainerScrollOffset.x; + visibleStartIndex = self->_state->getData().contentViewMeasurements.lower_bound(visibleStartOffset); + visibleEndIndex = self->_state->getData().contentViewMeasurements.lower_bound(visibleEndOffset); + } else if (!props.horizontal && props.inverted) { + visibleStartOffset = contentViewTotal - listContainerScrollOffset.y - self->_contentView.frame.size.height; + visibleEndOffset = contentViewTotal - listContainerScrollOffset.y; + visibleStartIndex = self->_state->getData().contentViewMeasurements.lower_bound(visibleStartOffset); + visibleEndIndex = self->_state->getData().contentViewMeasurements.lower_bound(visibleEndOffset); + } else if (props.horizontal && !props.inverted) { + visibleStartOffset = listContainerScrollOffset.x; + visibleEndOffset = listContainerScrollOffset.x + self.frame.size.width; + visibleStartIndex = self->_state->getData().contentViewMeasurements.lower_bound(visibleStartOffset); + visibleEndIndex = self->_state->getData().contentViewMeasurements.lower_bound(visibleEndOffset); + } else { + visibleStartOffset = listContainerScrollOffset.y; + visibleEndOffset = listContainerScrollOffset.y + self.frame.size.height; + visibleStartIndex = self->_state->getData().contentViewMeasurements.lower_bound(visibleStartOffset); + visibleEndIndex = self->_state->getData().contentViewMeasurements.lower_bound(visibleEndOffset); + } + + visibleStartIndex = MAX(0, visibleStartIndex - 2); + visibleEndIndex = MIN(contentViewCount, visibleEndIndex + 2); + + [self->_cachedComponentPool recycle:visibleStartIndex visibleEndIndex:visibleEndIndex]; + + if (props.horizontal) { + return CGPointMake(visibleStartOffset, 0); + } else { + return CGPointMake(0, visibleStartOffset); + } +} + +- (CGPoint)listContainerScrollFocusIndexChange:(NSInteger)focusIndex +{ + assert(std::dynamic_pointer_cast(self->_state)); + const auto &stateData = self->_state->getData(); + const auto &props = static_cast(*_props); + + const auto contentViewItem = stateData.contentViewMeasurements.sum((size_t)focusIndex); + + CGPoint listContainerScrollOffset; + + if (props.horizontal) { + listContainerScrollOffset = CGPointMake(contentViewItem, 0); + } else { + listContainerScrollOffset = CGPointMake(0, contentViewItem); + } + + [self listContainerScrollOffsetChange:listContainerScrollOffset]; + return listContainerScrollOffset; +} + +- (CGPoint)listContainerScrollFocusOffsetChange:(NSInteger)focusOffset +{ + assert(std::dynamic_pointer_cast(self->_state)); + const auto &stateData = self->_state->getData(); + const auto &props = static_cast(*_props); + + CGPoint listContainerScrollOffset; + + if (props.horizontal) { + listContainerScrollOffset = CGPointMake(focusOffset, 0); + } else { + listContainerScrollOffset = CGPointMake(0, focusOffset); + } + + [self listContainerScrollOffsetChange:listContainerScrollOffset]; + return listContainerScrollOffset; +} +Class ShadowListContentCls(void) +{ + return ShadowListContent.class; +} + +@end diff --git a/ios/ShadowListContentDelegate.h b/ios/ShadowListContentDelegate.h new file mode 100644 index 0000000..98c61db --- /dev/null +++ b/ios/ShadowListContentDelegate.h @@ -0,0 +1,18 @@ +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import + +#ifndef ShadowListContentDelegate_h +#define ShadowListContentDelegate_h + +NS_ASSUME_NONNULL_BEGIN + +@protocol ShadowListContentDelegate +@required +- (void)listContentSizeChange:(CGSize)listContentSize; +@end + +NS_ASSUME_NONNULL_END + +#endif +#endif diff --git a/ios/ShadowListContentManager.mm b/ios/ShadowListContentManager.mm new file mode 100644 index 0000000..404afc5 --- /dev/null +++ b/ios/ShadowListContentManager.mm @@ -0,0 +1,16 @@ +#import +#import + +@interface ShadowListContentManager : RCTViewManager +@end + +@implementation ShadowListContentManager + +RCT_EXPORT_MODULE(ShadowListContent) + +- (UIView *)view +{ + return [[UIView alloc] init]; +} + +@end diff --git a/src/ShadowListContainerNativeComponent.ts b/src/ShadowListContainerNativeComponent.ts index 43015f6..fbdb51b 100644 --- a/src/ShadowListContainerNativeComponent.ts +++ b/src/ShadowListContainerNativeComponent.ts @@ -8,14 +8,12 @@ import type { Double, } from 'react-native/Libraries/Types/CodegenTypes'; -export type OnBatchLayoutProps = { size: Int32 }; export type OnEndReachedProps = { distanceFromEnd: Int32 }; +export type OnStartReachedProps = { distanceFromStart: Int32 }; export interface NativeProps extends ViewProps { inverted?: boolean; horizontal?: boolean; - hasListHeaderComponent?: boolean; - hasListFooterComponent?: boolean; initialScrollIndex?: Int32; onVisibleChange?: DirectEventHandler< Readonly<{ @@ -23,9 +21,10 @@ export interface NativeProps extends ViewProps { end: Int32; }> >; - onBatchLayout?: DirectEventHandler>; onEndReached?: DirectEventHandler>; onEndReachedThreshold?: Double; + onStartReached?: DirectEventHandler>; + onStartReachedThreshold?: Double; } export interface NativeCommands { diff --git a/src/ShadowListContentNativeComponent.ts b/src/ShadowListContentNativeComponent.ts new file mode 100644 index 0000000..81fcbd4 --- /dev/null +++ b/src/ShadowListContentNativeComponent.ts @@ -0,0 +1,13 @@ +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import type { ViewProps } from 'react-native'; + +export interface NativeProps extends ViewProps { + inverted?: boolean; + horizontal?: boolean; + hasListHeaderComponent?: boolean; + hasListFooterComponent?: boolean; +} + +export default codegenNativeComponent('ShadowListContent', { + interfaceOnly: true, +}); diff --git a/src/index.tsx b/src/index.tsx index 2893aa8..40cb729 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,7 @@ import ShadowListContainerNativeComponent, { type NativeProps, type NativeCommands, } from './ShadowListContainerNativeComponent'; +import ShadowListContentNativeComponent from './ShadowListContentNativeComponent'; import ShadowListItemNativeComponent from './ShadowListItemNativeComponent'; type Component = @@ -30,6 +31,7 @@ const invoker = (Component: Component) => export type ShadowListContainerWrapperProps = { data: any[]; renderItem: (payload: { item: any; index: number }) => React.ReactElement; + keyExtractor?: ((item: any, index: number) => string) | undefined; contentContainerStyle?: ViewStyle; ListHeaderComponent?: Component; ListHeaderComponentStyle?: ViewStyle; @@ -49,30 +51,13 @@ export type ShadowListItemWrapperProps = { item: any; }; -/** - * Primitive batcher implementation - */ -const useBatcher = (index: number) => { - const [isReady, setIsReady] = React.useState(false); - - React.useEffect(() => { - const animationFrame = requestAnimationFrame(() => setIsReady(true)); - return () => cancelAnimationFrame(animationFrame); - }, [index]); - - return isReady; -}; - const ShadowListItemWrapper = ({ item, renderItem, index, }: ShadowListItemWrapperProps) => { - const isReady = useBatcher(index); - if (!isReady) return; - return ( - + {renderItem({ item, index })} ); @@ -102,100 +87,115 @@ const ShadowListContainerWrapper = ( })); const data = React.useMemo(() => { - return props.inverted ? props.data.reverse() : props.data; - }, [props.inverted, props.data]); - - const baseStyle = props.horizontal ? styles.horizontal : styles.vertical; + return props.data; + }, [props.data]); + + const containerStyle = props.horizontal + ? styles.containerHorizontal + : styles.containerVertical; + const contentStyle = props.horizontal + ? props.inverted + ? styles.contentHorizontalInverted + : styles.contentHorizontal + : props.inverted + ? styles.contentVerticalInverted + : styles.contentVertical; /** * ListHeaderComponent */ - const ListHeaderComponent = React.useMemo( - () => - props.ListHeaderComponent ? ( - - {invoker(props.ListHeaderComponent)} - - ) : null, - [props.ListHeaderComponent, props.ListHeaderComponentStyle] - ); + const ListHeaderComponent = React.useMemo(() => { + return props.ListHeaderComponent ? ( + + {invoker(props.ListHeaderComponent)} + + ) : null; + }, [props.ListHeaderComponent, props.ListHeaderComponentStyle]); /** * ListFooterComponent */ - const ListFooterComponent = React.useMemo( - () => - props.ListFooterComponent ? ( - - {invoker(props.ListFooterComponent)} - - ) : null, - [props.ListFooterComponent, props.ListFooterComponentStyle] - ); + const ListFooterComponent = React.useMemo(() => { + return props.ListFooterComponent ? ( + + {invoker(props.ListFooterComponent)} + + ) : null; + }, [props.ListFooterComponent, props.ListFooterComponentStyle]); /** * ListEmptyComponent */ - const ListEmptyComponent = React.useMemo( - () => - props.ListEmptyComponent ? ( - - {invoker(props.ListEmptyComponent)} - - ) : null, - [props.ListEmptyComponent, props.ListEmptyComponentStyle] - ); + const ListEmptyComponent = React.useMemo(() => { + return props.ListEmptyComponent ? ( + + {invoker(props.ListEmptyComponent)} + + ) : null; + }, [props.ListEmptyComponent, props.ListEmptyComponentStyle]); /** * ListChildrenComponent */ - const ListChildrenComponent = React.useMemo( - () => - data.map((item, index) => ( - - )), - [data, props.renderItem, props.inverted] - ); + const ListChildrenComponent = React.useMemo(() => { + return data.map((item, index) => ( + + )); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, props.renderItem, props.keyExtractor]); return ( - {!props.inverted ? ListHeaderComponent : ListFooterComponent} - {data.length ? ListChildrenComponent : ListEmptyComponent} - {!props.inverted ? ListFooterComponent : ListHeaderComponent} + + {ListHeaderComponent} + {data.length ? ListChildrenComponent : ListEmptyComponent} + {ListFooterComponent} + ); }; const styles = StyleSheet.create({ - vertical: { - flexGrow: 1, - flexShrink: 1, + contentHorizontal: { + flex: 1, + flexDirection: 'row', + }, + contentVertical: { + flex: 1, + flexDirection: 'column', + }, + contentHorizontalInverted: { + flex: 1, + flexDirection: 'row-reverse', + justifyContent: 'flex-end', + }, + contentVerticalInverted: { + flex: 1, + flexDirection: 'column-reverse', + justifyContent: 'flex-end', + }, + containerVertical: { + flex: 1, flexDirection: 'column', overflow: 'scroll', }, - horizontal: { - flexGrow: 1, - flexShrink: 1, + containerHorizontal: { + flex: 1, flexDirection: 'row', overflow: 'scroll', },