From 8761edd0cf2e0745c5aed67f22e56f59756b3de9 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 28 Aug 2023 21:33:25 -0400 Subject: [PATCH] [WIP] feat: new scroll implementation on iOS (#53) * feat: new scroll implementation on iOS * fix * clean * clean * clean * bail out on no window change, ios scroll * More wip * wip --- .../main/java/com/wishlist/Orchestrator.kt | 6 +- .../src/main/java/com/wishlist/Wishlist.kt | 37 ++- .../java/com/wishlist/WishlistViewManager.kt | 4 + android/src/main/jni/Orchestrator.cpp | 21 +- android/src/main/jni/Orchestrator.hpp | 5 +- cpp/ItemProvider/ShadowNodeBinding.cpp | 1 - cpp/MGViewportCarer/MGViewportCarer.hpp | 10 +- cpp/MGViewportCarer/MGViewportCarerImpl.cpp | 212 ++++++++++++---- cpp/MGViewportCarer/MGViewportCarerImpl.h | 23 +- .../MGViewportCarerListener.hpp | 1 + cpp/Wishlist/MGWishlistShadowNode.cpp | 8 + cpp/Wishlist/MGWishlistShadowNode.h | 2 + cpp/Wishlist/MGWishlistState.cpp | 13 +- cpp/Wishlist/MGWishlistState.h | 1 + cpp/WishlistDefine.h | 3 + example/android/build.gradle | 2 +- example/src/Chat/ChatItem.tsx | 4 +- ios/MGDIIOS.cpp | 24 -- ios/MGDIIOS.h | 25 -- ios/MGWindowKeeper/MGBoundingBoxObserver.hpp | 20 -- ios/MGWindowKeeper/MGWindowKeeper.cpp | 38 --- ios/MGWindowKeeper/MGWindowKeeper.hpp | 34 --- ios/MGWishListComponent.h | 2 - ios/MGWishListComponent.mm | 127 ++++------ .../MGAnimations/MGAnimationSight.cpp | 14 -- .../MGAnimations/MGAnimationSight.hpp | 20 -- ios/Orchestrator/MGAnimations/MGAnimations.h | 31 --- ios/Orchestrator/MGAnimations/MGAnimations.mm | 169 ------------- ios/Orchestrator/MGOrchestrator.h | 35 +++ ios/Orchestrator/MGOrchestrator.mm | 139 +++++++++++ ios/Orchestrator/MGOrchestratorCppAdapter.cpp | 14 +- ios/Orchestrator/MGOrchestratorCppAdapter.hpp | 21 +- ios/Orchestrator/MGScrollViewOrchestrator.h | 43 ---- ios/Orchestrator/MGScrollViewOrchestrator.mm | 231 ------------------ 34 files changed, 512 insertions(+), 828 deletions(-) create mode 100644 cpp/WishlistDefine.h delete mode 100644 ios/MGDIIOS.cpp delete mode 100644 ios/MGDIIOS.h delete mode 100644 ios/MGWindowKeeper/MGBoundingBoxObserver.hpp delete mode 100644 ios/MGWindowKeeper/MGWindowKeeper.cpp delete mode 100644 ios/MGWindowKeeper/MGWindowKeeper.hpp delete mode 100644 ios/Orchestrator/MGAnimations/MGAnimationSight.cpp delete mode 100644 ios/Orchestrator/MGAnimations/MGAnimationSight.hpp delete mode 100644 ios/Orchestrator/MGAnimations/MGAnimations.h delete mode 100644 ios/Orchestrator/MGAnimations/MGAnimations.mm create mode 100644 ios/Orchestrator/MGOrchestrator.h create mode 100644 ios/Orchestrator/MGOrchestrator.mm delete mode 100644 ios/Orchestrator/MGScrollViewOrchestrator.h delete mode 100644 ios/Orchestrator/MGScrollViewOrchestrator.mm diff --git a/android/src/main/java/com/wishlist/Orchestrator.kt b/android/src/main/java/com/wishlist/Orchestrator.kt index 6ec9659..7a2a34c 100644 --- a/android/src/main/java/com/wishlist/Orchestrator.kt +++ b/android/src/main/java/com/wishlist/Orchestrator.kt @@ -18,7 +18,7 @@ class Orchestrator(private val mWishlist: Wishlist, wishlistId: String, viewport external fun renderAsync( width: Float, height: Float, - initialOffset: Float, + initialContentSize: Float, originItem: Int, templatesRef: Int, names: List, @@ -29,8 +29,10 @@ class Orchestrator(private val mWishlist: Wishlist, wishlistId: String, viewport external fun scrollToItem(index: Int) + external fun didUpdateContentOffset() + @DoNotStrip - private fun scrollToOffset(offset: Float) { + private fun scrollToOffset(offset: Float, animated: Boolean) { mWishlist.reactSmoothScrollTo(0, PixelUtil.toPixelFromDIP(offset).toInt()) } } diff --git a/android/src/main/java/com/wishlist/Wishlist.kt b/android/src/main/java/com/wishlist/Wishlist.kt index 3af8824..bb467a8 100644 --- a/android/src/main/java/com/wishlist/Wishlist.kt +++ b/android/src/main/java/com/wishlist/Wishlist.kt @@ -15,7 +15,9 @@ class Wishlist(reactContext: Context) : private var templatesRef: Int? = null private var names: List? = null private var didInitialScroll = false - private val initialOffset = 100000f + private val initialContentSize = 100000f + private var pendingScrollOffset = Int.MIN_VALUE + private var ignoreScrollEvents = false fun setTemplates(templatesRef: Int, names: List) { this.templatesRef = templatesRef @@ -42,7 +44,7 @@ class Wishlist(reactContext: Context) : orchestrator.renderAsync( PixelUtil.toDIPFromPixel(width.toFloat()), PixelUtil.toDIPFromPixel(height.toFloat()), - initialOffset, + initialContentSize, initialIndex, templatesRef, names, @@ -57,7 +59,7 @@ class Wishlist(reactContext: Context) : if (contentView == null || contentView.height == 0) { return } - scrollTo(0, PixelUtil.toPixelFromDIP(initialOffset).toInt()) + scrollTo(0, PixelUtil.toPixelFromDIP(initialContentSize / 2).toInt()) didInitialScroll = true } @@ -74,6 +76,7 @@ class Wishlist(reactContext: Context) : ) { super.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) initialScrollIfReady() + maybeScrollToOffsetForContentChange() } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { @@ -81,11 +84,16 @@ class Wishlist(reactContext: Context) : renderIfReady() initialScrollIfReady() + maybeScrollToOffsetForContentChange() } override fun onScrollChanged(x: Int, y: Int, oldX: Int, oldY: Int) { super.onScrollChanged(x, y, oldX, oldY) + if (ignoreScrollEvents) { + return + } + orchestrator?.didScrollAsync( PixelUtil.toDIPFromPixel(width.toFloat()), PixelUtil.toDIPFromPixel(height.toFloat()), @@ -96,4 +104,27 @@ class Wishlist(reactContext: Context) : fun scrollToItem(index: Int, animated: Boolean) { orchestrator?.scrollToItem(index) } + + fun scrollToOffsetForContentChange(offset: Float) { + // State is updated before content view is laid out so update the + // scroll position in layout handler. + pendingScrollOffset = PixelUtil.toPixelFromDIP(offset).toInt() + } + + private fun maybeScrollToOffsetForContentChange() { + if (pendingScrollOffset == Int.MIN_VALUE) { + return + } + val contentView = getChildAt(0) + if (contentView == null || + contentView.height == 0 || + pendingScrollOffset > contentView.height) { + return + } + ignoreScrollEvents = true + scrollTo(0, pendingScrollOffset) + orchestrator?.didUpdateContentOffset() + ignoreScrollEvents = false + pendingScrollOffset = Int.MIN_VALUE + } } diff --git a/android/src/main/java/com/wishlist/WishlistViewManager.kt b/android/src/main/java/com/wishlist/WishlistViewManager.kt index 69c501c..5cef065 100644 --- a/android/src/main/java/com/wishlist/WishlistViewManager.kt +++ b/android/src/main/java/com/wishlist/WishlistViewManager.kt @@ -31,6 +31,10 @@ class WishlistViewManager : ViewGroupManager(), MGWishlistManagerInter stateWrapper: StateWrapper? ): Any? { view.fabricViewStateManager.setStateWrapper(stateWrapper) + val stateData = stateWrapper?.stateData + if (stateData != null && stateData.hasKey("contentOffset")) { + view.scrollToOffsetForContentChange(stateData.getDouble("contentOffset").toFloat()) + } return null } diff --git a/android/src/main/jni/Orchestrator.cpp b/android/src/main/jni/Orchestrator.cpp index ff04b35..7fe7624 100644 --- a/android/src/main/jni/Orchestrator.cpp +++ b/android/src/main/jni/Orchestrator.cpp @@ -56,7 +56,7 @@ Orchestrator::Orchestrator( void Orchestrator::renderAsync( float width, float height, - float initialOffset, + float initialContentSize, int originItem, int templatesRef, jni::alias_ref> namesList, @@ -69,12 +69,11 @@ void Orchestrator::renderAsync( alreadyRendered_ = true; width_ = width; height_ = height; - contentOffset_ = initialOffset; inflatorId_ = inflatorId; di_->getViewportCarer()->initialRenderAsync( {width, height}, - initialOffset, + initialContentSize, originItem, templates, jListToVector(namesList), @@ -94,18 +93,18 @@ void Orchestrator::didScrollAsync( std::string inflatorId) { width_ = width; height_ = height; - contentOffset_ = contentOffset; inflatorId_ = inflatorId; - handleVSync(); + di_->getViewportCarer()->didScrollAsync( + {width, height}, contentOffset, inflatorId); } void Orchestrator::handleVSync() { - // TODO: These do not seem to be needed. - auto templates = - std::vector>(); - auto names = std::vector(); di_->getViewportCarer()->didScrollAsync( - {width_, height_}, templates, names, contentOffset_, inflatorId_); + {width_, height_}, MG_NO_OFFSET, inflatorId_); +} + +void Orchestrator::didUpdateContentOffset() { + di_->getViewportCarer()->didUpdateContentOffset(); } void Orchestrator::scrollToItem(int index) { @@ -160,6 +159,8 @@ void Orchestrator::registerNatives() { {makeNativeMethod("initHybrid", Orchestrator::initHybrid), makeNativeMethod("renderAsync", Orchestrator::renderAsync), makeNativeMethod("didScrollAsync", Orchestrator::didScrollAsync), + makeNativeMethod( + "didUpdateContentOffset", Orchestrator::didUpdateContentOffset), makeNativeMethod("scrollToItem", Orchestrator::scrollToItem)}); } diff --git a/android/src/main/jni/Orchestrator.hpp b/android/src/main/jni/Orchestrator.hpp index ef3d948..c94749d 100644 --- a/android/src/main/jni/Orchestrator.hpp +++ b/android/src/main/jni/Orchestrator.hpp @@ -27,7 +27,7 @@ class Orchestrator : public jni::HybridClass { void renderAsync( float width, float height, - float initialOffset, + float initialContentSize, int originItem, int templatesRef, jni::alias_ref> names, @@ -41,6 +41,8 @@ class Orchestrator : public jni::HybridClass { void handleVSync(); + void didUpdateContentOffset(); + void scrollToItem(int index); void didPushChildren(std::vector items); @@ -68,7 +70,6 @@ class Orchestrator : public jni::HybridClass { std::shared_ptr adapter_; float width_; float height_; - float contentOffset_; std::string inflatorId_; std::vector items_; int pendingScrollToItem_; diff --git a/cpp/ItemProvider/ShadowNodeBinding.cpp b/cpp/ItemProvider/ShadowNodeBinding.cpp index 288fd39..b5deccd 100644 --- a/cpp/ItemProvider/ShadowNodeBinding.cpp +++ b/cpp/ItemProvider/ShadowNodeBinding.cpp @@ -94,7 +94,6 @@ Value ShadowNodeBinding::get(Runtime &rt, const PropNameID &nameProp) { std::string callbackName = args[0].asString(rt).utf8(rt); int tag = sn_->getTag(); std::string eventName = std::to_string(tag) + callbackName; - std::cout << "register native for name " << eventName << std::endl; jsi::Function callback = args[1].asObject(rt).asFunction(rt); auto handlerRegistry = rt.global() diff --git a/cpp/MGViewportCarer/MGViewportCarer.hpp b/cpp/MGViewportCarer/MGViewportCarer.hpp index 7e74785..3a5b078 100644 --- a/cpp/MGViewportCarer/MGViewportCarer.hpp +++ b/cpp/MGViewportCarer/MGViewportCarer.hpp @@ -14,6 +14,8 @@ namespace Wishlist { using namespace facebook::react; +static float MG_NO_OFFSET = std::numeric_limits::min(); + struct MGDims { float width; float height; @@ -23,7 +25,7 @@ class MGViewportCarer { public: virtual void initialRenderAsync( MGDims dimensions, - float initialOffset, + float initialContentSize, int originItem, const std::vector> ®isteredViews, const std::vector &names, @@ -31,12 +33,10 @@ class MGViewportCarer { virtual void didScrollAsync( MGDims dimensions, - const std::vector> ®isteredViews, - const std::vector &names, - float newOffset, + float contentOffset, const std::string &inflatorId) = 0; - virtual ~MGViewportCarer() {} + virtual void didUpdateContentOffset() = 0; }; }; // namespace Wishlist diff --git a/cpp/MGViewportCarer/MGViewportCarerImpl.cpp b/cpp/MGViewportCarer/MGViewportCarerImpl.cpp index 266746f..d772d0a 100644 --- a/cpp/MGViewportCarer/MGViewportCarerImpl.cpp +++ b/cpp/MGViewportCarer/MGViewportCarerImpl.cpp @@ -3,12 +3,31 @@ #include "MGContentContainerShadowNode.h" #include "MGUIManagerHolder.h" #include "MGWishlistShadowNode.h" +#include "WishlistDefine.h" #include "WishlistJsRuntime.h" namespace Wishlist { using namespace facebook::react; +MGViewportCarerImpl::MGViewportCarerImpl() + : contentOffset_(0), + initialContentSize_(0), + windowHeight_(0), + windowWidth_(0), + surfaceId_(0), + inflatorId_(""), + componentsPool_(std::make_shared()), + itemProvider_(nullptr), + window_({}), + wishListNode_(nullptr), + lc_({}), + di_({}), + firstItemKeyForStartReached_(""), + lastItemKeyForEndReached_(""), + listener_({}), + ignoreScrollEvents_(false) {} + void MGViewportCarerImpl::setDI(const std::weak_ptr &di) { di_ = di; } @@ -27,7 +46,7 @@ void MGViewportCarerImpl::setInitialValues( void MGViewportCarerImpl::initialRenderAsync( MGDims dimensions, - float initialOffset, + float initialContentSize, int originItem, const std::vector> ®isteredViews, const std::vector &names, @@ -42,33 +61,39 @@ void MGViewportCarerImpl::initialRenderAsync( itemProvider_->setComponentsPool(componentsPool_); surfaceId_ = wishListNode_->getFamily().getSurfaceId(); - offset_ = initialOffset; + initialContentSize_ = initialContentSize; + contentOffset_ = initialContentSize / 2; windowHeight_ = dimensions.height; windowWidth_ = dimensions.width; inflatorId_ = inflatorId; window_.push_back(itemProvider_->provide(originItem, nullptr)); - window_.back().offset = initialOffset; + window_.back().offset = initialContentSize / 2; updateWindow(); }); } void MGViewportCarerImpl::didScrollAsync( MGDims dimensions, - const std::vector> ®isteredViews, - const std::vector &names, - float newOffset, + float contentOffset, const std::string &inflatorId) { - // TODO: Check why this happens. - if (newOffset == 0) { - return; - } +#if MG_WISHLIST_DEBUG + static int scrollEventId = 0; + int currentScrollEventId = scrollEventId++; + std::cout << "didScrollAsync UI {eventId: " << currentScrollEventId + << ", contentOffset: " << contentOffset << "}" << std::endl; +#endif WishlistJsRuntime::getInstance().accessRuntime([=](jsi::Runtime &rt) { - if (dimensions.width != windowWidth_ || !names.empty() || - inflatorId != inflatorId_) { - componentsPool_->setRegisteredViews(registeredViews); - componentsPool_->setNames(names); + if (ignoreScrollEvents_) { +#if MG_WISHLIST_DEBUG + std::cout << "didScrollAsync BG ignore events skip {eventId: " + << currentScrollEventId << ", offset: " << contentOffset << "}" + << std::endl; +#endif + return; + } + if (dimensions.width != windowWidth_ || inflatorId != inflatorId_) { itemProvider_ = std::static_pointer_cast( std::make_shared( di_, dimensions.width, lc_, inflatorId)); @@ -86,21 +111,73 @@ void MGViewportCarerImpl::didScrollAsync( } } - this->offset_ = newOffset; - this->windowHeight_ = dimensions.height; + // MG_NO_OFFSET means that we keep the current offset. + if (contentOffset != MG_NO_OFFSET) { + contentOffset_ = contentOffset; + } + windowHeight_ = dimensions.height; +#if MG_WISHLIST_DEBUG + std::cout << "didScrollAsync BG updateWindow {eventId: " + << currentScrollEventId << ", contentOffset: " << contentOffset + << "}" << std::endl; +#endif updateWindow(); }); } +void MGViewportCarerImpl::didUpdateContentOffset() { + WishlistJsRuntime::getInstance().accessRuntime([=](jsi::Runtime &rt) { +#if MG_WISHLIST_DEBUG + std::cout << "didUpdateContentOffset BG" << std::endl; +#endif + ignoreScrollEvents_ = false; + }); +} + void MGViewportCarerImpl::updateWindow() { - float topEdge = offset_ - windowHeight_; - float bottomEdge = offset_ + 2 * windowHeight_; + float topEdge = contentOffset_ - windowHeight_; + float bottomEdge = contentOffset_ + 2 * windowHeight_; bool startReached = false; bool endReached = false; + bool changed = false; assert(!window_.empty()); +#if MG_WISHLIST_DEBUG + std::cout << "updateWindow {contentOffset: " << contentOffset_ + << ", topEdge: " << topEdge << ", bottomEdge: " << bottomEdge << "}" + << std::endl; + std::cout << "before:" << std::endl; + for (auto &item : window_) { + std::cout << "{key: " << item.key << ", offset: " << item.offset + << ", height: " << item.height << "}" << std::endl; + } +#endif + + float currentOffset = window_[0].offset; + for (auto &item : window_) { + if (item.dirty) { + std::shared_ptr prevSn = nullptr; + if (item.sn) { + prevSn = std::make_shared( + item.sn, componentsPool_, item.type, item.key); + } + WishItem wishItem = itemProvider_->provide(item.index, prevSn); + if (wishItem.sn == nullptr) { + continue; + } + swap(item.sn, wishItem.sn); + item.offset = currentOffset - (wishItem.height - item.height); + item.height = wishItem.height; + item.type = wishItem.type; + item.key = wishItem.key; + item.dirty = false; + changed = true; + } + currentOffset = item.offset + item.height; + } + // Add above while (true) { WishItem item = window_.front(); @@ -113,6 +190,7 @@ void MGViewportCarerImpl::updateWindow() { } wishItem.offset = item.offset - wishItem.height; window_.push_front(wishItem); + changed = true; } else { break; } @@ -131,6 +209,7 @@ void MGViewportCarerImpl::updateWindow() { } wishItem.offset = bottom; window_.push_back(wishItem); + changed = true; } else { break; } @@ -145,6 +224,7 @@ void MGViewportCarerImpl::updateWindow() { if (bottom <= topEdge) { window_.pop_front(); itemsToRemove.push_back(item); + changed = true; continue; } else { break; @@ -157,35 +237,66 @@ void MGViewportCarerImpl::updateWindow() { if (item.offset >= bottomEdge) { window_.pop_back(); itemsToRemove.push_back(item); + changed = true; continue; } else { break; } } - float currentOffset = window_[0].offset; - for (auto &item : window_) { - if (item.dirty) { - std::shared_ptr prevSn = nullptr; - if (item.sn) { - prevSn = std::make_shared( - item.sn, componentsPool_, item.type, item.key); - } - WishItem wishItem = itemProvider_->provide(item.index, prevSn); - if (wishItem.sn == nullptr) { - continue; - } - item.offset = currentOffset; - swap(item.sn, wishItem.sn); - item.height = wishItem.height; - item.type = wishItem.type; - item.key = wishItem.key; - item.dirty = false; + // Bail out early if no changes to the window. + if (!changed) { + return; + } + + // This will be used to adjust scroll position to maintain the visible content + // position. + float contentOffsetAdjustment = 0; + + // Make sure we don't have negative offsets, this can happen when + // we are at the start of the list. + if (window_.front().offset < 0) { + contentOffsetAdjustment -= window_.front().offset; + float newOffset = 0; + for (auto &item : window_) { + item.offset = newOffset; + newOffset = newOffset + item.height; } - currentOffset = item.offset + item.height; } - pushChildren(); + // We reach the start of the list and still have extra offset + // we need to remove it so that the content size is exact and + // the list stops scrolling correctly. + if (startReached && window_.front().offset > 0) { + contentOffsetAdjustment -= window_.front().offset; + + float newOffset = 0; + for (auto &item : window_) { + item.offset = newOffset; + newOffset = newOffset + item.height; + } + } + // We are no longer at the start of the list and don't have extra offset + // we need to add it back. + else if (!startReached && window_.front().offset <= 0) { + auto newOffset = initialContentSize_ / 2; + for (auto &item : window_) { + item.offset += newOffset; + } + contentOffsetAdjustment += newOffset; + } + + if (contentOffsetAdjustment != 0) { + contentOffset_ += contentOffsetAdjustment; + ignoreScrollEvents_ = true; +#if MG_WISHLIST_DEBUG + std::cout << "updateWindow adjust content offset {adjustment: " + << contentOffsetAdjustment << ", offset: " << contentOffset_ + << "}" << std::endl; +#endif + } + + pushChildren(contentOffsetAdjustment != 0 ? contentOffset_ : MG_NO_OFFSET); for (auto &item : itemsToRemove) { componentsPool_->returnToPool(item.sn); @@ -209,6 +320,14 @@ void MGViewportCarerImpl::updateWindow() { } else { lastItemKeyForEndReached_ = ""; } + +#if MG_WISHLIST_DEBUG + std::cout << "after:" << std::endl; + for (auto &item : window_) { + std::cout << "{key: " << item.key << ", offset: " << item.offset + << ", height: " << item.height << "}" << std::endl; + } +#endif } std::shared_ptr MGViewportCarerImpl::getOffseter(float offset) { @@ -232,7 +351,7 @@ std::shared_ptr MGViewportCarerImpl::getOffseter(float offset) { return offseterTemplate->clone({newProps, nullptr, nullptr}); } -void MGViewportCarerImpl::pushChildren() { +void MGViewportCarerImpl::pushChildren(float contentOffset) { std::shared_ptr sWishList = wishListNode_; if (sWishList == nullptr) { return; @@ -286,8 +405,14 @@ void MGViewportCarerImpl::pushChildren() { std::make_shared( ShadowNode::ListOfShared{newContentContainer}); - return sn.clone( - ShadowNodeFragment{nullptr, wishlistChildren, nullptr}); + auto newWishlistSn = + std::static_pointer_cast( + sn.clone(ShadowNodeFragment{ + nullptr, wishlistChildren, nullptr})); + + newWishlistSn->updateContentOffset(contentOffset); + + return newWishlistSn; })); }; st.commit(transaction, {}); @@ -304,9 +429,8 @@ void MGViewportCarerImpl::notifyAboutPushedChildren() { for (auto &item : window_) { newWindow.push_back({item.offset, item.height, item.index, item.key}); } - di_.lock()->getUIScheduler()->scheduleOnUI([newWindow, listener]() { - listener->didPushChildren(std::move(newWindow)); - }); + di_.lock()->getUIScheduler()->scheduleOnUI( + [newWindow, listener]() { listener->didPushChildren(newWindow); }); WishlistJsRuntime::getInstance().accessRuntime([=](jsi::Runtime &rt) { jsi::Function didPushChildren = rt.global() diff --git a/cpp/MGViewportCarer/MGViewportCarerImpl.h b/cpp/MGViewportCarer/MGViewportCarerImpl.h index 55df5c0..a0cff53 100644 --- a/cpp/MGViewportCarer/MGViewportCarerImpl.h +++ b/cpp/MGViewportCarer/MGViewportCarerImpl.h @@ -17,8 +17,10 @@ namespace Wishlist { // TODO make this class testable by injecting componentsPool and itemProvider // or their factories -class MGViewportCarerImpl : public MGViewportCarer { +class MGViewportCarerImpl final : public MGViewportCarer { public: + MGViewportCarerImpl(); + void setInitialValues( const std::shared_ptr &wishListNode, const LayoutContext &lc); @@ -29,7 +31,7 @@ class MGViewportCarerImpl : public MGViewportCarer { void initialRenderAsync( MGDims dimensions, - float initialOffset, + float initialContentSize, int originItem, const std::vector> ®isteredViews, const std::vector &names, @@ -37,17 +39,19 @@ class MGViewportCarerImpl : public MGViewportCarer { void didScrollAsync( MGDims dimensions, - const std::vector> ®isteredViews, - const std::vector &names, - float newOffset, + float contentOffset, const std::string &inflatorId) override; + void didUpdateContentOffset() override; + private: void updateWindow(); + void updateContentOffset(float contentOffset); + std::shared_ptr getOffseter(float offset); - void pushChildren(); + void pushChildren(float contentOffset); void notifyAboutPushedChildren(); @@ -55,13 +59,13 @@ class MGViewportCarerImpl : public MGViewportCarer { void notifyAboutEndReached(); - float offset_; + float contentOffset_; + float initialContentSize_; float windowHeight_; float windowWidth_; int surfaceId_; std::string inflatorId_; - std::shared_ptr componentsPool_ = - std::make_shared(); + std::shared_ptr componentsPool_; std::shared_ptr itemProvider_; std::deque window_; std::shared_ptr wishListNode_; @@ -70,6 +74,7 @@ class MGViewportCarerImpl : public MGViewportCarer { std::string firstItemKeyForStartReached_; std::string lastItemKeyForEndReached_; std::weak_ptr listener_; + bool ignoreScrollEvents_; }; }; // namespace Wishlist diff --git a/cpp/MGViewportCarer/MGViewportCarerListener.hpp b/cpp/MGViewportCarer/MGViewportCarerListener.hpp index e3d49db..61d2e2d 100644 --- a/cpp/MGViewportCarer/MGViewportCarerListener.hpp +++ b/cpp/MGViewportCarer/MGViewportCarerListener.hpp @@ -9,6 +9,7 @@ #include #include +#include "MGViewportCarer.hpp" namespace Wishlist { diff --git a/cpp/Wishlist/MGWishlistShadowNode.cpp b/cpp/Wishlist/MGWishlistShadowNode.cpp index 0472580..284ae53 100644 --- a/cpp/Wishlist/MGWishlistShadowNode.cpp +++ b/cpp/Wishlist/MGWishlistShadowNode.cpp @@ -37,5 +37,13 @@ void MGWishlistShadowNode::updateStateIfNeeded() { } } +void MGWishlistShadowNode::updateContentOffset(float contentOffset) { + ensureUnsealed(); + + auto state = getStateData(); + state.contentOffset = contentOffset; + setStateData(std::move(state)); +} + } // namespace react } // namespace facebook diff --git a/cpp/Wishlist/MGWishlistShadowNode.h b/cpp/Wishlist/MGWishlistShadowNode.h index 28ed1db..38fe911 100644 --- a/cpp/Wishlist/MGWishlistShadowNode.h +++ b/cpp/Wishlist/MGWishlistShadowNode.h @@ -27,6 +27,8 @@ class JSI_EXPORT MGWishlistShadowNode public: void layout(LayoutContext layoutContext) override; + void updateContentOffset(float contentOffset); + private: void updateStateIfNeeded(); }; diff --git a/cpp/Wishlist/MGWishlistState.cpp b/cpp/Wishlist/MGWishlistState.cpp index 9db3263..7aaa34a 100644 --- a/cpp/Wishlist/MGWishlistState.cpp +++ b/cpp/Wishlist/MGWishlistState.cpp @@ -10,7 +10,8 @@ namespace react { MGWishlistState::MGWishlistState() : initialised(false), viewportCarer(std::make_shared()), - contentBoundingRect({}){}; + contentBoundingRect({}), + contentOffset(MG_NO_OFFSET){}; #ifdef ANDROID @@ -19,12 +20,18 @@ MGWishlistState::MGWishlistState( folly::dynamic data) : initialised(previousState.initialised), viewportCarer(previousState.viewportCarer), - contentBoundingRect(previousState.contentBoundingRect){}; + contentBoundingRect(previousState.contentBoundingRect), + contentOffset(MG_NO_OFFSET){}; folly::dynamic MGWishlistState::getDynamic() const { auto viewportCarerRef = Wishlist::JNIStateRegistry::getInstance().addValue( (void *)&viewportCarer); - return folly::dynamic::object("viewportCarer", viewportCarerRef); + folly::dynamic result = folly::dynamic::object(); + result["viewportCarer"] = viewportCarerRef; + if (contentOffset != MG_NO_OFFSET) { + result["contentOffset"] = contentOffset; + } + return result; }; MapBuffer MGWishlistState::getMapBuffer() const { diff --git a/cpp/Wishlist/MGWishlistState.h b/cpp/Wishlist/MGWishlistState.h index f062679..c2f205f 100644 --- a/cpp/Wishlist/MGWishlistState.h +++ b/cpp/Wishlist/MGWishlistState.h @@ -27,6 +27,7 @@ class JSI_EXPORT MGWishlistState final { bool initialised; std::shared_ptr viewportCarer; Rect contentBoundingRect; + float contentOffset; MGWishlistState(); diff --git a/cpp/WishlistDefine.h b/cpp/WishlistDefine.h new file mode 100644 index 0000000..eceb179 --- /dev/null +++ b/cpp/WishlistDefine.h @@ -0,0 +1,3 @@ +#pragma once + +#define MG_WISHLIST_DEBUG 0 diff --git a/example/android/build.gradle b/example/android/build.gradle index 5bcbbb0..cef2da2 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -8,7 +8,7 @@ buildscript { targetSdkVersion = 33 // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. - ndkVersion = "25.2.9519653" + ndkVersion = "25.1.8937393" kotlinVersion = "1.6.21" } diff --git a/example/src/Chat/ChatItem.tsx b/example/src/Chat/ChatItem.tsx index 4098801..a95fccf 100644 --- a/example/src/Chat/ChatItem.tsx +++ b/example/src/Chat/ChatItem.tsx @@ -68,7 +68,9 @@ export const ChatItemView: React.FC = ({ type, onAddReaction }) => { const avatarUrl = useTemplateValue((item: ChatItem) => { return item.avatarUrl; }); - const message = useTemplateValue((item: ChatItem) => item.message); + const message = useTemplateValue( + (item: ChatItem) => `${item.key}: ${item.message}`, + ); const likeText = useTemplateValue((item: ChatItem) => { if (item.liked) { return '♥️'; diff --git a/ios/MGDIIOS.cpp b/ios/MGDIIOS.cpp deleted file mode 100644 index 25cfd53..0000000 --- a/ios/MGDIIOS.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "MGDIIOS.h" - -namespace Wishlist { - -MGDIIOS::~MGDIIOS() {} - -std::shared_ptr MGDIIOS::getAnimationsSight() { - return animationSight; -} - -std::shared_ptr MGDIIOS::getBoundingBoxObserver() { - return boundingBoxObserver_; -} - -void MGDIIOS::setAnimationsSight(const std::shared_ptr &as) { - animationSight = as; -} - -void MGDIIOS::setBoundingBoxObserver( - const std::shared_ptr &bbo) { - boundingBoxObserver_ = bbo; -} - -}; // namespace Wishlist diff --git a/ios/MGDIIOS.h b/ios/MGDIIOS.h deleted file mode 100644 index d01c2d8..0000000 --- a/ios/MGDIIOS.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "MGAnimationSight.hpp" -#include "MGBoundingBoxObserver.hpp" -#include "MGDIImpl.hpp" - -namespace Wishlist { - -class MGDIIOS : public MGDIImpl { - public: - std::shared_ptr getAnimationsSight(); - std::shared_ptr getBoundingBoxObserver(); - - void setAnimationsSight(const std::shared_ptr &as); - void setBoundingBoxObserver( - const std::shared_ptr &bbo); - - virtual ~MGDIIOS(); - - private: - std::shared_ptr animationSight; - std::shared_ptr boundingBoxObserver_; -}; - -}; // namespace Wishlist diff --git a/ios/MGWindowKeeper/MGBoundingBoxObserver.hpp b/ios/MGWindowKeeper/MGBoundingBoxObserver.hpp deleted file mode 100644 index 09873b4..0000000 --- a/ios/MGWindowKeeper/MGBoundingBoxObserver.hpp +++ /dev/null @@ -1,20 +0,0 @@ -// -// MGBoundingBoxObserver.hpp -// MGWishList -// -// Created by Szymon on 13/01/2023. -// - -#pragma once - -#include -#include - -namespace Wishlist { - -struct MGBoundingBoxObserver { - virtual void boundingBoxDidChange( - std::pair TopAndBottomEdge) = 0; -}; - -}; // namespace Wishlist diff --git a/ios/MGWindowKeeper/MGWindowKeeper.cpp b/ios/MGWindowKeeper/MGWindowKeeper.cpp deleted file mode 100644 index 7263fc9..0000000 --- a/ios/MGWindowKeeper/MGWindowKeeper.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// -// MGWindowKeeper.cpp -// MGWishList -// -// Created by Szymon on 13/01/2023. -// - -#include "MGWindowKeeper.hpp" - -namespace Wishlist { - -MGWindowKeeper::MGWindowKeeper(std::weak_ptr _di) : di(_di) {} - -float MGWindowKeeper::getOffsetIfItemIsAlreadyRendered(int index) { - for (auto &item : this->items) { - if (item.index == index) { - return item.offset; - } - } - return NOT_FOUND; -} - -bool MGWindowKeeper::isTargetItemLocatedBelow(int targetItem) { - return this->items.back().index < targetItem; -} - -void MGWindowKeeper::didPushChildren(std::vector newWindow) { - this->items = newWindow; - std::shared_ptr retainedDI = di.lock(); - if (retainedDI != nullptr && newWindow.size() > 0) { - float topEdge = newWindow[0].offset; - float bottomEdge = newWindow.back().offset + newWindow.back().height; - retainedDI->getBoundingBoxObserver()->boundingBoxDidChange( - {topEdge, bottomEdge}); - } -} - -}; // namespace Wishlist diff --git a/ios/MGWindowKeeper/MGWindowKeeper.hpp b/ios/MGWindowKeeper/MGWindowKeeper.hpp deleted file mode 100644 index 5223af5..0000000 --- a/ios/MGWindowKeeper/MGWindowKeeper.hpp +++ /dev/null @@ -1,34 +0,0 @@ -// -// MGWindowKeeper.hpp -// MGWishList -// -// Created by Szymon on 13/01/2023. -// - -#pragma once - -#include -#include -#include "MGAnimationSight.hpp" -#include "MGDIIOS.h" -#include "MGViewportCarerListener.hpp" - -namespace Wishlist { - -struct MGWindowKeeper final : MGAnimationSight, MGViewportCarerListener { - std::vector items; - std::weak_ptr di; - - MGWindowKeeper(std::weak_ptr _di); - -#pragma mark MGViewportCarerListener - - void didPushChildren(std::vector newWindow) override; - -#pragma mark MGAnimationSight - - float getOffsetIfItemIsAlreadyRendered(int index) override; - bool isTargetItemLocatedBelow(int targetItem) override; -}; - -}; // namespace Wishlist diff --git a/ios/MGWishListComponent.h b/ios/MGWishListComponent.h index d723ec9..5ce27dc 100644 --- a/ios/MGWishListComponent.h +++ b/ios/MGWishListComponent.h @@ -12,8 +12,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)setInflatorId:(std::string)inflatorId; - (void)setWishlistId:(std::string)wishlistId; -- (void)handlePan:(UIPanGestureRecognizer *)gesture; - @end NS_ASSUME_NONNULL_END diff --git a/ios/MGWishListComponent.mm b/ios/MGWishListComponent.mm index 918edca..9483fde 100644 --- a/ios/MGWishListComponent.mm +++ b/ios/MGWishListComponent.mm @@ -9,15 +9,12 @@ #import #include #include -#import "MGDIIOS.h" -#import "MGDataBindingImpl.hpp" -#import "MGErrorHandlerIOS.h" -#import "MGOrchestratorCPPAdapter.hpp" -#import "MGScrollViewOrchestrator.h" -#import "MGUIScheduleriOS.hpp" -#import "MGWindowKeeper.hpp" +#import "MGOrchestrator.h" #import "MGWishlistComponentDescriptor.h" #import "RCTFabricComponentsPlugins.h" +#include "WishlistDefine.h" + +#define MG_INITIAL_CONTENT_SIZE 100000 using namespace facebook::react; @@ -28,29 +25,20 @@ - (void)scrollViewDidScroll:(UIScrollView *)sv; @end @implementation MGWishListComponent { - MGWishlistShadowNode::ConcreteState::Shared _sharedState; - bool _alreadyRendered; + MGWishlistShadowNode::ConcreteState::Shared _state; std::string _inflatorId; std::string _wishlistId; - MGScrollViewOrchestrator *_orchestrator; + MGOrchestrator *_orchestrator; std::shared_ptr _emitter; int _initialIndex; - std::shared_ptr _di; + BOOL _ignoreScrollEvents; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { - self.scrollView.delaysContentTouches = NO; - self.scrollView.canCancelContentTouches = true; - _alreadyRendered = false; - - [self.scrollView removeGestureRecognizer:self.scrollView.panGestureRecognizer]; - - UIPanGestureRecognizer *customR = [UIPanGestureRecognizer new]; - [customR setMinimumNumberOfTouches:1]; - [self.scrollView addGestureRecognizer:customR]; - [customR addTarget:self action:@selector(handlePan:)]; + self.scrollView.showsVerticalScrollIndicator = NO; + _ignoreScrollEvents = NO; } return self; } @@ -70,17 +58,6 @@ - (void)setWishlistId:(std::string)wishlistId _wishlistId = wishlistId; } -- (void)handlePan:(UIPanGestureRecognizer *)gesture -{ - if (_orchestrator != nil) { - PanEvent *panEvent = [PanEvent new]; - panEvent.state = gesture.state; - panEvent.velocity = [gesture velocityInView:self].y; - panEvent.translation = [gesture translationInView:self.scrollView].y; - [_orchestrator notifyAboutEvent:panEvent]; - } -} - - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView { [self scrollToItem:0 animated:YES]; // Maybe it would be better to scroll to initial (which is not always 0) @@ -90,43 +67,21 @@ - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView - (void)setTemplates:(std::vector>)templates withNames:(std::vector)names { - if (!_alreadyRendered && names.size() > 0 && names.size() == templates.size()) { - _alreadyRendered = true; - CGRect frame = self.frame; - - self.scrollView.contentSize = CGSizeMake(frame.size.width, 10000000); - self.scrollView.frame = - CGRectMake(self.scrollView.frame.origin.x, self.scrollView.frame.origin.y, frame.size.width, frame.size.height); - - _di = std::make_shared(); - auto weakDI = std::weak_ptr(_di); - std::shared_ptr viewportCarer = _sharedState->getData().viewportCarer; - viewportCarer->setDI(_di); - _di->setViewportCarer(viewportCarer); - - _orchestrator = [[MGScrollViewOrchestrator alloc] initWith:self.scrollView - di:weakDI - inflatorId:_inflatorId - wishlistId:_wishlistId]; - __weak MGScrollViewOrchestrator *weakOrchestrator = _orchestrator; - auto orchestratorAdapter = std::make_shared( - [=](float top, float bottom) { [weakOrchestrator edgesChangedWithTopEdge:top bottomEdge:bottom]; }, - [=]() { [weakOrchestrator requestVSync]; }); - _di->setVSyncRequester(orchestratorAdapter); - _di->setBoundingBoxObserver(orchestratorAdapter); - _di->setDataBinding(std::make_shared(_wishlistId, weakDI)); - auto windowKeeper = std::make_shared(weakDI); - _di->setAnimationsSight(windowKeeper); - _di->setUIScheduler(std::make_shared()); - _di->setErrorHandler(std::make_shared()); - - viewportCarer->setListener(std::weak_ptr(windowKeeper)); - - [_orchestrator runWithTemplates:templates names:names initialIndex:_initialIndex]; - - } else { - [_orchestrator notifyAboutNewTemplates:templates withNames:names inflatorId:_inflatorId]; + if (!_orchestrator) { + self.scrollView.contentSize = CGSizeMake(self.frame.size.width, MG_INITIAL_CONTENT_SIZE); + self.scrollView.contentOffset = CGPointMake(0, MG_INITIAL_CONTENT_SIZE / 2); + + std::shared_ptr viewportCarer = _state->getData().viewportCarer; + _orchestrator = [[MGOrchestrator alloc] initWith:self wishlistId:_wishlistId viewportCarer:viewportCarer]; } + + [_orchestrator + renderAsyncWithDimensions:{(float)self.scrollView.frame.size.width, (float)self.scrollView.frame.size.height} + initialContentSize:MG_INITIAL_CONTENT_SIZE + initialIndex:_initialIndex + templates:templates + names:names + inflatorId:_inflatorId]; } #pragma mark - RCTComponentViewProtocol @@ -149,15 +104,29 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & - (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState { - if (state == nullptr) - return; - auto newState = std::static_pointer_cast(state); - auto &data = newState->getData(); - _sharedState = newState; + // Updating content size or offset can trigger scroll events but we do not want to + // process those as they can have invalid offset or were already processed. + _ignoreScrollEvents = YES; + + assert(std::dynamic_pointer_cast(state)); + _state = std::static_pointer_cast(state); + auto &data = _state->getData(); CGSize contentSize = RCTCGSizeFromSize(data.contentBoundingRect.size); + self.contentView.frame = CGRect{RCTCGPointFromPoint(data.contentBoundingRect.origin), contentSize}; + self.scrollView.contentSize = contentSize; + +#if MG_WISHLIST_DEBUG + std::cout << "updateState {offset: " << data.contentOffset << ", contentHeight: " << contentSize.height << "}" + << std::endl; +#endif - self.containerView.frame = CGRect{RCTCGPointFromPoint(data.contentBoundingRect.origin), contentSize}; + if (data.contentOffset != MG_NO_OFFSET) { + [self.scrollView setContentOffset:{0, data.contentOffset} animated:NO]; + data.viewportCarer->didUpdateContentOffset(); + } + + _ignoreScrollEvents = NO; } #pragma clang diagnostic push @@ -170,17 +139,19 @@ - (void)updateEventEmitter:(EventEmitter::Shared const &)eventEmitter - (void)scrollViewDidScroll:(UIScrollView *)scrollView { - if (_sharedState == nullptr) { + if (_ignoreScrollEvents) { return; } + + [_orchestrator didScrollAsyncWithDimensions:{(float)scrollView.frame.size.width, (float)scrollView.frame.size.height} + contentOffset:scrollView.contentOffset.y + inflatorId:_inflatorId]; } - (void)prepareForRecycle { - _sharedState.reset(); + _state.reset(); _orchestrator = nil; - _di = nullptr; - _alreadyRendered = NO; [super prepareForRecycle]; } diff --git a/ios/Orchestrator/MGAnimations/MGAnimationSight.cpp b/ios/Orchestrator/MGAnimations/MGAnimationSight.cpp deleted file mode 100644 index 211d550..0000000 --- a/ios/Orchestrator/MGAnimations/MGAnimationSight.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// -// MGAnimationSight.cpp -// MGWishList -// -// Created by Szymon on 13/01/2023. -// - -#include "MGAnimationSight.hpp" - -namespace Wishlist { - -float MGAnimationSight::NOT_FOUND = -1; - -}; // namespace Wishlist diff --git a/ios/Orchestrator/MGAnimations/MGAnimationSight.hpp b/ios/Orchestrator/MGAnimations/MGAnimationSight.hpp deleted file mode 100644 index 57bcc91..0000000 --- a/ios/Orchestrator/MGAnimations/MGAnimationSight.hpp +++ /dev/null @@ -1,20 +0,0 @@ -// -// MGAnimationSight.hpp -// MGWishList -// -// Created by Szymon on 13/01/2023. -// - -#pragma once - -#include - -namespace Wishlist { - -struct MGAnimationSight { - virtual float getOffsetIfItemIsAlreadyRendered(int index) = 0; - virtual bool isTargetItemLocatedBelow(int targetItem) = 0; - static float NOT_FOUND; -}; - -}; // namespace Wishlist diff --git a/ios/Orchestrator/MGAnimations/MGAnimations.h b/ios/Orchestrator/MGAnimations/MGAnimations.h deleted file mode 100644 index f36595c..0000000 --- a/ios/Orchestrator/MGAnimations/MGAnimations.h +++ /dev/null @@ -1,31 +0,0 @@ -#import "MGAnimationSight.hpp" - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -using namespace Wishlist; - -@protocol MGScrollAnimation - -- (void)setupWithTimestamp:(double)timestamp; -- (CGFloat)getDiffWithTimestamp:(double)timestamp; -- (BOOL)isFinished; -- (BOOL)needsSetup; - -@end - -// https://medium.com/@esskeetit/scrolling-mechanics-of-uiscrollview-142adee1142c -// most likly it can be optimised by estimating that function on [lastTimestamp, timestamp] interval -@interface MGDecayAnimation : NSObject -- (instancetype)initWithVelocity:(double)v; -@end - -@interface MGScrollToItemAnimation : NSObject -- (instancetype)initWithIndex:(int)index - offset:(CGFloat)offset - itemSight:(std::weak_ptr)animationSight; -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/Orchestrator/MGAnimations/MGAnimations.mm b/ios/Orchestrator/MGAnimations/MGAnimations.mm deleted file mode 100644 index 53103cf..0000000 --- a/ios/Orchestrator/MGAnimations/MGAnimations.mm +++ /dev/null @@ -1,169 +0,0 @@ -#import "MGAnimations.h" - -@implementation MGDecayAnimation { - double _lastTimestamp; - double _initialTimestamp; - double _intialVelocity; - BOOL _isFinished; - double _destination; - double _totalDistanceTraveled; - double _decRate; - BOOL _isInitialized; -} - -- (instancetype)initWithVelocity:(double)v -{ - if (self = [super init]) { - _intialVelocity = v; - _totalDistanceTraveled = 0; - _decRate = 0.998; - _isFinished = NO; - _isInitialized = NO; - [self computeDestination]; - } - return self; -} - -- (void)computeDestination -{ - double d = 1000.0 * log(_decRate); - _destination = -(_intialVelocity / d); -} - -- (CGFloat)getValueAtTimestamp:(double)timestamp -{ - double d = 1000.0 * log(_decRate); - return (pow(_decRate, 1000.0 * (timestamp - _initialTimestamp)) - 1.0) / d * _intialVelocity; -} - -- (CGFloat)getDiffWithTimestamp:(double)timestamp -{ - double nextVal = [self getValueAtTimestamp:timestamp]; - double diff = nextVal - _totalDistanceTraveled; - _totalDistanceTraveled = nextVal; - if (_lastTimestamp != timestamp && abs(diff) < 0.1) { - _isFinished = YES; - } - _lastTimestamp = timestamp; - return diff; -} - -- (BOOL)isFinished -{ - return _isFinished; -} - -- (BOOL)needsSetup -{ - return !_isInitialized; -} - -- (void)setupWithTimestamp:(double)timestamp -{ - _isInitialized = YES; - _initialTimestamp = timestamp; - _lastTimestamp = timestamp; -} - -@end - -const float maxVelocity = 20000; // TODO (it should be adjusted) -const float stiffness = 10; -const float mass = 0.5; -const float damping = 5; - -@implementation MGScrollToItemAnimation { - std::weak_ptr _animationSight; - - BOOL _needsSetup; - BOOL _isFinished; - CGFloat _lastOffset; - CGFloat _targetOffset; - int _targetIndex; - double _lastTimestamp; - double _velocity; - double _velCoef; -} - -- (instancetype)initWithIndex:(int)index - offset:(CGFloat)offset - itemSight:(std::weak_ptr)animationSight -{ - if (self = [super init]) { - _animationSight = animationSight; - _lastOffset = offset; - _targetIndex = index; - _targetOffset = MGAnimationSight::NOT_FOUND; - _velocity = 0; - - _velCoef = 1; - - _isFinished = NO; - _needsSetup = YES; - } - return self; -} - -- (void)tryToFindTargetOffset -{ - _targetOffset = _animationSight.lock()->getOffsetIfItemIsAlreadyRendered(_targetIndex); -} - -// TODO Should be clamped on ends -// TODO It won't work if we will change sizes of items during scrollTo. -// inspired by https://blog.maximeheckel.com/posts/the-physics-behind-spring-animations/ -- (CGFloat)getDiffWithTimestamp:(double)timestamp -{ - timestamp *= 1000.0; - [self tryToFindTargetOffset]; - - CGFloat timeDiff = fmin((timestamp - _lastTimestamp), 16 * 3); - - if (_targetOffset != MGAnimationSight::NOT_FOUND) { - CGFloat k = -stiffness; - CGFloat d = -damping; - - CGFloat FSpring = k * (_targetOffset - _lastOffset); - CGFloat FDamping = d * _velocity; - - CGFloat a = (FSpring + FDamping) / mass; - _velocity += a * timeDiff / 1000.0; - _velocity = fmin(abs(_velocity), abs(maxVelocity)) * ((_velocity < 0) ? -1.0 : 1.0); - } else { - if (!_animationSight.lock()->isTargetItemLocatedBelow(_targetIndex)) { - _velCoef = 1; - } else { - _velCoef = -1; - } - _velocity = _velCoef * maxVelocity; - } - - CGFloat diff = _velocity * timeDiff / 1000.0 * (-1.0); - _lastOffset += diff; - _lastTimestamp = timestamp; - - if (abs(diff) < 0.1) { - _isFinished = YES; - } - - return -diff; -} - -- (BOOL)isFinished -{ - return _isFinished; -} - -- (BOOL)needsSetup -{ - return _needsSetup; -} - -- (void)setupWithTimestamp:(double)timestamp -{ - timestamp *= 1000.0; - _needsSetup = false; - _lastTimestamp = timestamp - 16; -} - -@end diff --git a/ios/Orchestrator/MGOrchestrator.h b/ios/Orchestrator/MGOrchestrator.h new file mode 100644 index 0000000..a511b4c --- /dev/null +++ b/ios/Orchestrator/MGOrchestrator.h @@ -0,0 +1,35 @@ +#import +#import +#import +#import +#import +#import +#import "MGDI.hpp" +#import "MGViewportCarerImpl.h" + +NS_ASSUME_NONNULL_BEGIN + +using namespace Wishlist; + +@class MGWishListComponent; + +@interface MGOrchestrator : NSObject + +- (instancetype)initWith:(MGWishListComponent *)wishlist + wishlistId:(std::string)wishlistId + viewportCarer:(std::shared_ptr)viewportCarer; + +- (void)renderAsyncWithDimensions:(MGDims)dimensions + initialContentSize:(CGFloat)initialContentSize + initialIndex:(NSInteger)initialIndex + templates:(std::vector>)templates + names:(std::vector)names + inflatorId:(std::string)inflatorId; +- (void)didScrollAsyncWithDimensions:(MGDims)dimensions + contentOffset:(float)contentOffset + inflatorId:(std::string)inflatorId; +- (void)scrollToItem:(int)index; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/Orchestrator/MGOrchestrator.mm b/ios/Orchestrator/MGOrchestrator.mm new file mode 100644 index 0000000..452f376 --- /dev/null +++ b/ios/Orchestrator/MGOrchestrator.mm @@ -0,0 +1,139 @@ +#import "MGOrchestrator.h" + +#include +#include "MGDIImpl.hpp" +#include "MGDataBindingImpl.hpp" +#include "MGErrorHandlerIOS.h" +#include "MGOrchestratorCppAdapter.hpp" +#include "MGUIScheduleriOS.hpp" +#include "MGViewportCarerImpl.h" +#include "MGWishListComponent.h" +#include "WishlistJsRuntime.h" + +using namespace Wishlist; + +@implementation MGOrchestrator { + MGWishListComponent *_wishlist; + std::shared_ptr _di; + std::string _inflatorId; + std::string _wishlistId; + MGDims _dimensions; + BOOL _alreadyRendered; + std::shared_ptr _adapter; + std::vector _items; + int _pendingScrollToItem; +} + +- (instancetype)initWith:(MGWishListComponent *)wishlist + wishlistId:(std::string)wishlistId + viewportCarer:(std::shared_ptr)viewportCarer +{ + if (self = [super init]) { + _wishlist = wishlist; + _wishlistId = wishlistId; + _alreadyRendered = NO; + _pendingScrollToItem = -1; + _di = std::make_shared(); + auto weakDI = std::weak_ptr(_di); + viewportCarer->setDI(_di); + _di->setViewportCarer(viewportCarer); + + __weak __typeof(self) weakSelf = self; + _adapter = std::make_shared( + [weakSelf]() { [weakSelf handleVSync]; }, + [weakSelf](std::vector items) { [weakSelf didPushChildren:std::move(items)]; }); + _di->setVSyncRequester(_adapter); + _di->setDataBinding(std::make_shared(_wishlistId, weakDI)); + _di->setUIScheduler(std::make_shared()); + _di->setErrorHandler(std::make_shared()); + + viewportCarer->setListener(std::weak_ptr(_adapter)); + } + return self; +} + +- (void)handleVSync +{ + _di->getViewportCarer()->didScrollAsync(_dimensions, MG_NO_OFFSET, _inflatorId); +} + +- (void)renderAsyncWithDimensions:(MGDims)dimensions + initialContentSize:(CGFloat)initialContentSize + initialIndex:(NSInteger)initialIndex + templates:(std::vector>)templates + names:(std::vector)names + inflatorId:(std::string)inflatorId +{ + if (!_alreadyRendered && names.size() > 0 && names.size() == templates.size()) { + _alreadyRendered = YES; + _dimensions = dimensions; + _inflatorId = inflatorId; + _di->getViewportCarer()->initialRenderAsync( + dimensions, initialContentSize, initialIndex, templates, names, inflatorId); + } else { + _dimensions = dimensions; + _inflatorId = inflatorId; + [self handleVSync]; + } +} + +- (void)didScrollAsyncWithDimensions:(MGDims)dimensions + contentOffset:(float)contentOffset + inflatorId:(std::string)inflatorId +{ + _dimensions = dimensions; + _inflatorId = inflatorId; + _di->getViewportCarer()->didScrollAsync(dimensions, contentOffset, inflatorId); +} +- (void)notifyAboutNewTemplates:(std::vector>)templates + withNames:(std::vector)names + inflatorId:(std::string)inflatorId +{ + _inflatorId = inflatorId; + [self handleVSync]; +} + +- (void)scrollToItem:(int)index +{ + float offset = -1; + for (auto &item : _items) { + if (item.index == index) { + offset = item.offset; + break; + } + } + + if (offset == -1) { + _pendingScrollToItem = index; + bool isBelow = _items.back().index < index; + // TODO: Implement proper animation for items outside the window. + if (isBelow) { + [self scrollToOffset:_items.back().offset + 10000]; + } else { + [self scrollToOffset:_items.back().offset - 10000]; + } + } else { + _pendingScrollToItem = -1; + [self scrollToOffset:offset]; + } +} + +- (void)scrollToOffset:(float)offset +{ + [_wishlist.scrollView setContentOffset:{0, offset} animated:YES]; +} + +- (void)didPushChildren:(std::vector)items +{ + _items = std::move(items); + if (_pendingScrollToItem != -1) { + [self scrollToItem:_pendingScrollToItem]; + } +} + +- (void)dealloc +{ + _wishlist = nil; +} + +@end diff --git a/ios/Orchestrator/MGOrchestratorCppAdapter.cpp b/ios/Orchestrator/MGOrchestratorCppAdapter.cpp index 092c992..0ca3d0e 100644 --- a/ios/Orchestrator/MGOrchestratorCppAdapter.cpp +++ b/ios/Orchestrator/MGOrchestratorCppAdapter.cpp @@ -10,18 +10,16 @@ namespace Wishlist { MGOrchestratorCppAdapter::MGOrchestratorCppAdapter( - std::function _onBoundingBoxDidChange, - std::function _onRequestVSync) - : onBoundingBoxDidChange(_onBoundingBoxDidChange), - onRequestVSync(_onRequestVSync) {} + std::function onRequestVSync, + std::function items)> didPushChildren) + : onRequestVSync_(onRequestVSync), didPushChildren_(didPushChildren) {} -void MGOrchestratorCppAdapter::boundingBoxDidChange( - std::pair topAndBottomEdges) { - onBoundingBoxDidChange(topAndBottomEdges.first, topAndBottomEdges.second); +void MGOrchestratorCppAdapter::didPushChildren(std::vector newWindow) { + didPushChildren_(std::move(newWindow)); } void MGOrchestratorCppAdapter::requestVSync() { - onRequestVSync(); + onRequestVSync_(); } }; // namespace Wishlist diff --git a/ios/Orchestrator/MGOrchestratorCppAdapter.hpp b/ios/Orchestrator/MGOrchestratorCppAdapter.hpp index bde1130..eab6d7b 100644 --- a/ios/Orchestrator/MGOrchestratorCppAdapter.hpp +++ b/ios/Orchestrator/MGOrchestratorCppAdapter.hpp @@ -9,23 +9,24 @@ #include #include -#include "MGBoundingBoxObserver.hpp" #include "MGVSyncRequester.hpp" +#include "MGViewportCarerListener.hpp" namespace Wishlist { -struct MGOrchestratorCppAdapter final : MGVSyncRequester, - MGBoundingBoxObserver { - std::function onBoundingBoxDidChange; - std::function onRequestVSync; - +class MGOrchestratorCppAdapter final : public MGVSyncRequester, + public MGViewportCarerListener { + public: MGOrchestratorCppAdapter( - std::function onBoundingBoxDidChange, - std::function onRequestVSync); - - void boundingBoxDidChange(std::pair topAndBottomEdges) override; + std::function onRequestVSync, + std::function items)> didPushChildren); + private: + void didPushChildren(std::vector newWindow) override; void requestVSync() override; + + std::function onRequestVSync_; + std::function items)> didPushChildren_; }; }; // namespace Wishlist diff --git a/ios/Orchestrator/MGScrollViewOrchestrator.h b/ios/Orchestrator/MGScrollViewOrchestrator.h deleted file mode 100644 index 43948a4..0000000 --- a/ios/Orchestrator/MGScrollViewOrchestrator.h +++ /dev/null @@ -1,43 +0,0 @@ -#import -#import -#import -#import -#import -#import -#import "MGAnimations.h" -#import "MGDIIOS.h" - -NS_ASSUME_NONNULL_BEGIN - -using namespace Wishlist; - -@interface PanEvent : NSObject - -@property (nonatomic, assign) UIGestureRecognizerState state; -@property (nonatomic, assign) CGFloat translation; -@property (nonatomic, assign) CGFloat velocity; - -@end - -@interface MGScrollViewOrchestrator : NSObject - -- (instancetype)initWith:(UIScrollView *)scrollView - di:(std::weak_ptr)di - inflatorId:(std::string)inflatorId - wishlistId:(std::string)wishlistId; - -- (void)runWithTemplates:(std::vector>)templates - names:(std::vector)names - initialIndex:(int)initialIndex; - -- (void)notifyAboutEvent:(PanEvent *)event; -- (void)notifyAboutNewTemplates:(std::vector>)templates - withNames:(std::vector)names - inflatorId:(std::string)inflatorId; -- (void)scrollToItem:(int)index; -- (void)requestVSync; -- (void)edgesChangedWithTopEdge:(float)topEdge bottomEdge:(float)bottomEdge; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/Orchestrator/MGScrollViewOrchestrator.mm b/ios/Orchestrator/MGScrollViewOrchestrator.mm deleted file mode 100644 index f3d1a4d..0000000 --- a/ios/Orchestrator/MGScrollViewOrchestrator.mm +++ /dev/null @@ -1,231 +0,0 @@ -#import "MGScrollViewOrchestrator.h" -#include -#import "MGDIIOS.h" -#include "WishlistJsRuntime.h" - -using namespace Wishlist; - -@implementation MGScrollViewOrchestrator { - UIScrollView *_scrollView; - CADisplayLink *_displayLink; - - // Events - NSMutableArray *_touchEvents; - CGFloat _lastTranslation; - BOOL _doWeHaveOngoingEvent; - BOOL areEventsBlocked; - - // PendingTemplates - std::vector> _pendingTemplates; - std::vector _pendingNames; - - // ViewportObserer - std::weak_ptr _di; - std::string _inflatorId; - BOOL _needsVSync; - - id _currentAnimation; - std::string _wishlistId; - - // content edges - CGFloat topElementY; - CGFloat bottomElementBottomEdgeY; -} - -- (instancetype)initWith:(UIScrollView *)scrollView - di:(std::weak_ptr)di - inflatorId:(std::string)inflatorId - wishlistId:(std::string)wishlistId -{ - if (self = [super init]) { - _scrollView = scrollView; - _wishlistId = wishlistId; - - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleVSync:)]; - [_displayLink addToRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode]; - [_displayLink setPaused:YES]; - _displayLink.preferredFramesPerSecond = 120; - if (@available(iOS 15.0, *)) { - _displayLink.preferredFrameRateRange = CAFrameRateRangeMake(60, 120, 120); - } - areEventsBlocked = YES; - - _di = di; - - _scrollView.contentOffset = CGPointMake(0, 500000); - - _doWeHaveOngoingEvent = NO; - _inflatorId = inflatorId; - _touchEvents = [NSMutableArray new]; - _needsVSync = NO; - } - return self; -} - -- (void)runWithTemplates:(std::vector>)templates - names:(std::vector)names - initialIndex:(int)initialIndex -{ - _di.lock()->getViewportCarer()->initialRenderAsync( - {(float)_scrollView.frame.size.width, (float)_scrollView.frame.size.height}, - 500000, - initialIndex, - templates, - names, - _inflatorId); -} - -- (void)maybeRegisterForNextVSync -{ - if ([_displayLink isPaused]) { - [_displayLink setPaused:NO]; - } -} - -- (void)handleVSync:(CADisplayLink *)displayLink -{ - _needsVSync = NO; - CGFloat yDiff = 0; - // Check Touch Events - if (_touchEvents.count > 0) { - _currentAnimation = nil; - for (PanEvent *event in _touchEvents) { - if (event.state == UIGestureRecognizerStateBegan) { - _lastTranslation = 0; - _doWeHaveOngoingEvent = YES; - } - if (event.state == UIGestureRecognizerStateChanged) { - yDiff = event.translation - _lastTranslation; - _lastTranslation = event.translation; - } - if (event.state == UIGestureRecognizerStateEnded) { - _doWeHaveOngoingEvent = NO; - // start Animation with velocity - _currentAnimation = [[MGDecayAnimation alloc] initWithVelocity:event.velocity]; - } - } - - [_touchEvents removeAllObjects]; - } - // Run Animations - if (_currentAnimation != nil) { - if ([_currentAnimation needsSetup]) { - [_currentAnimation setupWithTimestamp:displayLink.timestamp]; - } - yDiff += [_currentAnimation getDiffWithTimestamp:displayLink.timestamp]; - if ([_currentAnimation isFinished]) { - _currentAnimation = nil; - } - } - - // Update content Offset - if (yDiff != 0) { - CGPoint oldOffset = _scrollView.contentOffset; - _scrollView.contentOffset = CGPointMake(oldOffset.x, oldOffset.y - yDiff); - } - - std::shared_ptr di = _di.lock(); - if (di != nullptr) { - auto templatesCopy = std::vector>(); - auto namesCopy = std::vector(); - - namesCopy.swap(_pendingNames); - templatesCopy.swap(_pendingTemplates); - - di->getViewportCarer()->didScrollAsync( - {(float)_scrollView.frame.size.width, (float)_scrollView.frame.size.height}, - templatesCopy, - namesCopy, - ((float)_scrollView.contentOffset.y), - _inflatorId); - } - - [self avoidOverscroll]; - - // pause Vsync listener if there is nothing to do - if ([_touchEvents count] == 0 && _currentAnimation == nil && !_needsVSync) { - [_displayLink setPaused:YES]; - } -} - -- (void)notifyAboutEvent:(PanEvent *)event -{ - if (areEventsBlocked) { - return; - } - [_touchEvents addObject:event]; - [self maybeRegisterForNextVSync]; -} - -- (void)notifyAboutNewTemplates:(std::vector>)templates - withNames:(std::vector)names - inflatorId:(std::string)inflatorId -{ - _inflatorId = inflatorId; - [self requestVSync]; -} - -- (void)scrollToItem:(int)index -{ - if (_doWeHaveOngoingEvent) { - return; - } - _currentAnimation = [[MGScrollToItemAnimation alloc] initWithIndex:index - offset:_scrollView.contentOffset.y - itemSight:_di.lock()->getAnimationsSight()]; - - [self requestVSync]; -} - -- (void)requestVSync -{ - _needsVSync = YES; - [self maybeRegisterForNextVSync]; -} - -- (void)edgesChangedWithTopEdge:(float)topEdge bottomEdge:(float)bottomEdge -{ - topElementY = topEdge; - bottomElementBottomEdgeY = bottomEdge; - areEventsBlocked = NO; - - [self avoidOverscroll]; -} - -- (void)avoidOverscroll -{ - CGFloat topViewportEdge = _scrollView.contentOffset.y; - CGFloat bottomViewPortEdge = topViewportEdge + _scrollView.frame.size.height; - - if (bottomElementBottomEdgeY < bottomViewPortEdge) { - CGFloat diff = bottomElementBottomEdgeY - bottomViewPortEdge; - CGPoint oldOffset = _scrollView.contentOffset; - - _scrollView.contentOffset = CGPointMake(oldOffset.x, oldOffset.y + diff); - bottomViewPortEdge += diff; - topViewportEdge += diff; - } - - // topElementY > topViewPortEdge (top overscroll) - if (topElementY > topViewportEdge) { - CGFloat diff = topElementY - topViewportEdge; - CGPoint oldOffset = _scrollView.contentOffset; - - _scrollView.contentOffset = CGPointMake(oldOffset.x, oldOffset.y + diff); - bottomViewPortEdge += diff; - topViewportEdge += diff; - _currentAnimation = nil; - } -} - -- (void)dealloc -{ - _scrollView = nil; - [_displayLink invalidate]; -} - -@end - -@implementation PanEvent - -@end