Skip to content

Commit

Permalink
Add fabric support for maintainVisibleContentPosition on iOS
Browse files Browse the repository at this point in the history
  • Loading branch information
janicduplessis committed Jan 27, 2023
1 parent d3cc48d commit 95b4a85
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#import "RCTConversions.h"
#import "RCTEnhancedScrollView.h"
#import "RCTFabricComponentsPlugins.h"
#import "RCTPullToRefreshViewComponentView.h"

using namespace facebook::react;

Expand Down Expand Up @@ -99,6 +100,11 @@ @implementation RCTScrollViewComponentView {
BOOL _shouldUpdateContentInsetAdjustmentBehavior;

CGPoint _contentOffsetWhenClipped;

__weak UIView *_contentView;

CGRect _prevFirstVisibleFrame;
__weak UIView *_firstVisibleView;
}

+ (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view
Expand Down Expand Up @@ -148,10 +154,17 @@ - (void)dealloc

#pragma mark - RCTMountingTransactionObserving

- (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
[self _prepareForMaintainVisibleScrollPosition];
}

- (void)mountingTransactionDidMount:(MountingTransaction const &)transaction
withSurfaceTelemetry:(facebook::react::SurfaceTelemetry const &)surfaceTelemetry
{
[self _remountChildren];
[self _adjustForMaintainVisibleContentPosition];
}

#pragma mark - RCTComponentViewProtocol
Expand Down Expand Up @@ -336,11 +349,23 @@ - (void)_preserveContentOffsetIfNeededWithBlock:(void (^)())block
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[_containerView insertSubview:childComponentView atIndex:index];
if ([childComponentView isKindOfClass:RCTPullToRefreshViewComponentView.class]) {
// Ignore the pull to refresh component.
} else {
RCTAssert(_contentView == nil, @"RCTScrollView may only contain a single subview.");
_contentView = childComponentView;
}
}

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[childComponentView removeFromSuperview];
if ([childComponentView isKindOfClass:RCTPullToRefreshViewComponentView.class]) {
// Ignore the pull to refresh component.
} else {
RCTAssert(_contentView == childComponentView, @"Attempted to remove non-existent subview");
_contentView = nil;
}
}

/*
Expand Down Expand Up @@ -403,6 +428,9 @@ - (void)prepareForRecycle
CGRect oldFrame = self.frame;
self.frame = CGRectZero;
self.frame = oldFrame;
_contentView = nil;
_prevFirstVisibleFrame = CGRectZero;
_firstVisibleView = nil;
[super prepareForRecycle];
}

Expand Down Expand Up @@ -683,6 +711,74 @@ - (void)removeScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
[self.scrollViewDelegateSplitter removeDelegate:scrollListener];
}

#pragma mark - Maintain visible content position

- (void)_prepareForMaintainVisibleScrollPosition
{
const auto &props = *std::static_pointer_cast<const ScrollViewProps>(_props);
if (!props.maintainVisibleContentPosition) {
return;
}

BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width;
int minIdx = props.maintainVisibleContentPosition.value().minIndexForVisible;
for (NSUInteger ii = minIdx; ii < _contentView.subviews.count; ++ii) {
// Find the first entirely visible view.
UIView *subview = _contentView.subviews[ii];
BOOL hasNewView = NO;
if (horizontal) {
hasNewView = subview.frame.origin.x > _scrollView.contentOffset.x;
} else {
hasNewView = subview.frame.origin.y > _scrollView.contentOffset.y;
}
if (hasNewView || ii == _contentView.subviews.count - 1) {
_prevFirstVisibleFrame = subview.frame;
_firstVisibleView = subview;
break;
}
}
}

- (void)_adjustForMaintainVisibleContentPosition
{
const auto &props = *std::static_pointer_cast<const ScrollViewProps>(_props);
if (!props.maintainVisibleContentPosition) {
return;
}

std::optional<int> autoscrollThreshold = props.maintainVisibleContentPosition.value().autoscrollToTopThreshold;
BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width;
// TODO: detect and handle/ignore re-ordering
if (horizontal) {
CGFloat deltaX = _firstVisibleView.frame.origin.x - _prevFirstVisibleFrame.origin.x;
if (ABS(deltaX) > 0.5) {
CGFloat x = _scrollView.contentOffset.x;
[self _forceDispatchNextScrollEvent];
_scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x + deltaX, _scrollView.contentOffset.y);
if (autoscrollThreshold) {
// If the offset WAS within the threshold of the start, animate to the start.
if (x <= autoscrollThreshold.value()) {
[self scrollToOffset:CGPointMake(0, _scrollView.contentOffset.y) animated:YES];
}
}
}
} else {
CGRect newFrame = _firstVisibleView.frame;
CGFloat deltaY = newFrame.origin.y - _prevFirstVisibleFrame.origin.y;
if (ABS(deltaY) > 0.5) {
CGFloat y = _scrollView.contentOffset.y;
[self _forceDispatchNextScrollEvent];
_scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x, _scrollView.contentOffset.y + deltaY);
if (autoscrollThreshold) {
// If the offset WAS within the threshold of the start, animate to the start.
if (y <= autoscrollThreshold.value()) {
[self scrollToOffset:CGPointMake(_scrollView.contentOffset.x, 0) animated:YES];
}
}
}
}
}

@end

Class<RCTComponentViewProtocol> RCTScrollViewCls(void)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ ScrollViewProps::ScrollViewProps(
"keyboardDismissMode",
sourceProps.keyboardDismissMode,
{})),
maintainVisibleContentPosition(
CoreFeatures::enablePropIteratorSetter
? sourceProps.maintainVisibleContentPosition
: convertRawProp(
context,
rawProps,
"maintainVisibleContentPosition",
sourceProps.maintainVisibleContentPosition,
{})),
maximumZoomScale(
CoreFeatures::enablePropIteratorSetter
? sourceProps.maximumZoomScale
Expand Down Expand Up @@ -336,6 +345,7 @@ void ScrollViewProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(directionalLockEnabled, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(indicatorStyle, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(keyboardDismissMode, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(maintainVisibleContentPosition, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(maximumZoomScale, (Float)1.0);
RAW_SET_PROP_SWITCH_CASE_BASIC(minimumZoomScale, (Float)1.0);
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEnabled, true);
Expand Down Expand Up @@ -413,6 +423,10 @@ SharedDebugStringConvertibleList ScrollViewProps::getDebugProps() const {
"keyboardDismissMode",
keyboardDismissMode,
defaultScrollViewProps.keyboardDismissMode),
debugStringConvertibleItem(
"maintainVisibleContentPosition",
maintainVisibleContentPosition,
defaultScrollViewProps.maintainVisibleContentPosition),
debugStringConvertibleItem(
"maximumZoomScale",
maximumZoomScale,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include <react/renderer/components/view/ViewProps.h>
#include <react/renderer/core/PropsParserContext.h>

#include <optional>

namespace facebook {
namespace react {

Expand Down Expand Up @@ -43,6 +45,8 @@ class ScrollViewProps final : public ViewProps {
bool directionalLockEnabled{};
ScrollViewIndicatorStyle indicatorStyle{};
ScrollViewKeyboardDismissMode keyboardDismissMode{};
std::optional<ScrollViewMaintainVisibleContentPosition>
maintainVisibleContentPosition{};
Float maximumZoomScale{1.0f};
Float minimumZoomScale{1.0f};
bool scrollEnabled{true};
Expand Down
35 changes: 35 additions & 0 deletions ReactCommon/react/renderer/components/scrollview/conversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <folly/dynamic.h>
#include <react/renderer/components/scrollview/primitives.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/core/propsConversions.h>

namespace facebook {
namespace react {
Expand Down Expand Up @@ -98,6 +99,28 @@ inline void fromRawValue(
abort();
}

inline void fromRawValue(
const PropsParserContext &context,
const RawValue &value,
ScrollViewMaintainVisibleContentPosition &result) {
auto map = (butter::map<std::string, RawValue>)value;

auto minIndexForVisible = map.find("minIndexForVisible");
if (minIndexForVisible != map.end()) {
fromRawValue(
context, minIndexForVisible->second, result.minIndexForVisible);
}
auto autoscrollToTopThreshold = map.find("autoscrollToTopThreshold");
if (autoscrollToTopThreshold != map.end()) {
fromRawValue(
context,
autoscrollToTopThreshold->second,
result.autoscrollToTopThreshold);
}
}

#if RN_DEBUG_STRING_CONVERTIBLE

inline std::string toString(const ScrollViewSnapToAlignment &value) {
switch (value) {
case ScrollViewSnapToAlignment::Start:
Expand Down Expand Up @@ -144,5 +167,17 @@ inline std::string toString(const ContentInsetAdjustmentBehavior &value) {
}
}

inline std::string toString(
const std::optional<ScrollViewMaintainVisibleContentPosition> &value) {
if (!value) {
return "null";
}
return "{minIndexForVisible: " + toString(value.value().minIndexForVisible) +
", autoscrollToTopThreshold: " +
toString(value.value().autoscrollToTopThreshold) + "}";
}

#endif

} // namespace react
} // namespace facebook
18 changes: 18 additions & 0 deletions ReactCommon/react/renderer/components/scrollview/primitives.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

#pragma once

#include <optional>
#include <vector>

namespace facebook {
namespace react {

Expand All @@ -23,5 +26,20 @@ enum class ContentInsetAdjustmentBehavior {
Always
};

class ScrollViewMaintainVisibleContentPosition final {
public:
int minIndexForVisible{0};
std::optional<int> autoscrollToTopThreshold{};

bool operator==(const ScrollViewMaintainVisibleContentPosition &rhs) const {
return std::tie(this->minIndexForVisible, this->autoscrollToTopThreshold) ==
std::tie(rhs.minIndexForVisible, rhs.autoscrollToTopThreshold);
}

bool operator!=(const ScrollViewMaintainVisibleContentPosition &rhs) const {
return !(*this == rhs);
}
};

} // namespace react
} // namespace facebook
9 changes: 9 additions & 0 deletions ReactCommon/react/renderer/debug/DebugStringConvertible.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <climits>
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
#include <vector>
Expand Down Expand Up @@ -98,6 +99,14 @@ std::string toString(float const &value);
std::string toString(double const &value);
std::string toString(void const *value);

template <typename T>
std::string toString(const std::optional<T> &value) {
if (!value) {
return "null";
}
return toString(value.value());
}

/*
* *Informal* `DebugStringConvertible` interface.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class AppendingList extends React.Component<
<ScrollView
automaticallyAdjustContentInsets={false}
maintainVisibleContentPosition={{
minIndexForVisible: 1,
minIndexForVisible: 0,
autoscrollToTopThreshold: 10,
}}
nestedScrollEnabled
Expand Down

0 comments on commit 95b4a85

Please sign in to comment.