Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fabric support for maintainVisibleContentPosition on iOS #35319

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -337,6 +346,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);
RAW_SET_PROP_SWITCH_CASE_BASIC(minimumZoomScale);
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEnabled);
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,26 @@ 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);
}
}

inline std::string toString(const ScrollViewSnapToAlignment &value) {
switch (value) {
case ScrollViewSnapToAlignment::Start:
Expand All @@ -109,6 +130,8 @@ inline std::string toString(const ScrollViewSnapToAlignment &value) {
}
}

#if RN_DEBUG_STRING_CONVERTIBLE

inline std::string toString(const ScrollViewIndicatorStyle &value) {
switch (value) {
case ScrollViewIndicatorStyle::Default:
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
17 changes: 17 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,8 @@

#pragma once

#include <optional>

namespace facebook {
namespace react {

Expand All @@ -23,5 +25,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