Skip to content

Commit 9a72702

Browse files
sherginfacebook-github-bot
authored andcommitted
Fabric: On-demand view mounting for <ScrollView>
Summary: This is the first implementation stage of on-demand child views mounting for <ScrollView> feature. It works this way. When mounting layer requests mounting of a subview of <ScrollView>, the component does not mount it immediately. Instead, it stores it in an internal collection, and then after the transaction finishes use that to mount views that are in a visible area. Then we re-evaluate which child views are should be mounted and unmounted on every significant onScroll event. We use some leeway to do it not so often to save CPU cycles on scrolling. This feature already works fine but to make it shippable we need to integrate it with an `overflow inset` feature to make it 100% reliable in complex cases when some views have overflow content. Changelog: [Internal] Fabric-specific internal change. Reviewed By: sammy-SC Differential Revision: D22304012 fbshipit-source-id: be4ae21378d9d1c457cad2826da7d8c8d6395be5
1 parent 7d8aeb4 commit 9a72702

File tree

2 files changed

+114
-8
lines changed

2 files changed

+114
-8
lines changed

React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@
77

88
#import <UIKit/UIKit.h>
99

10+
#import <React/RCTDefines.h>
1011
#import <React/RCTGenericDelegateSplitter.h>
12+
#import <React/RCTMountingTransactionObserving.h>
1113
#import <React/RCTScrollableProtocol.h>
1214
#import <React/RCTViewComponentView.h>
1315

16+
/*
17+
* Allows to enable or disable on-demand view mounting feature of ScrollView.
18+
* It's an experimental feature that improves performance and memory footprint of huge lists inside ScrollView.
19+
*/
20+
RCT_EXTERN void RCTSetEnableOnDemandViewMounting(BOOL value);
21+
1422
NS_ASSUME_NONNULL_BEGIN
1523

1624
/*
@@ -21,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN
2129
* keyboard-avoiding functionality and so on. All that complexity must be implemented inside those components in order
2230
* to keep the complexity of this component manageable.
2331
*/
24-
@interface RCTScrollViewComponentView : RCTViewComponentView
32+
@interface RCTScrollViewComponentView : RCTViewComponentView <RCTMountingTransactionObserving>
2533

2634
/*
2735
* Finds and returns the closet RCTScrollViewComponentView component to the given view

React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
using namespace facebook::react;
2626

27+
static CGFloat const kClippingLeeway = 44.0;
28+
2729
static void RCTSendPaperScrollEvent_DEPRECATED(UIScrollView *scrollView, NSInteger tag)
2830
{
2931
static uint16_t coalescingKey = 0;
@@ -39,6 +41,13 @@ static void RCTSendPaperScrollEvent_DEPRECATED(UIScrollView *scrollView, NSInteg
3941
[[RCTBridge currentBridge].eventDispatcher sendEvent:scrollEvent];
4042
}
4143

44+
static BOOL isOnDemandViewMountingEnabledGlobally = NO;
45+
46+
void RCTSetEnableOnDemandViewMounting(BOOL value)
47+
{
48+
isOnDemandViewMountingEnabledGlobally = value;
49+
}
50+
4251
@interface RCTScrollViewComponentView () <UIScrollViewDelegate, RCTScrollViewProtocol, RCTScrollableProtocol>
4352

4453
@end
@@ -53,6 +62,10 @@ @implementation RCTScrollViewComponentView {
5362
// This helps to only update state from `scrollViewDidScroll` in case
5463
// some other part of the system scrolls scroll view.
5564
BOOL _isUserTriggeredScrolling;
65+
66+
BOOL _isOnDemandViewMountingEnabled;
67+
CGPoint _contentOffsetWhenClipped;
68+
NSMutableArray<UIView<RCTComponentViewProtocol> *> *_childComponentViews;
5669
}
5770

5871
+ (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view
@@ -69,6 +82,9 @@ - (instancetype)initWithFrame:(CGRect)frame
6982
static const auto defaultProps = std::make_shared<const ScrollViewProps>();
7083
_props = defaultProps;
7184

85+
_isOnDemandViewMountingEnabled = isOnDemandViewMountingEnabledGlobally;
86+
_childComponentViews = [[NSMutableArray alloc] init];
87+
7288
_scrollView = [[RCTEnhancedScrollView alloc] initWithFrame:self.bounds];
7389
_scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
7490
_scrollView.delaysContentTouches = NO;
@@ -98,6 +114,13 @@ - (void)dealloc
98114
return ((RCTEnhancedScrollView *)_scrollView).delegateSplitter;
99115
}
100116

117+
#pragma mark - RCTMountingTransactionObserving
118+
119+
- (void)mountingTransactionDidMountWithMetadata:(MountingTransactionMetadata const &)metadata
120+
{
121+
[self _remountChildren];
122+
}
123+
101124
#pragma mark - RCTComponentViewProtocol
102125

103126
+ (ComponentDescriptorProvider)componentDescriptorProvider
@@ -192,13 +215,27 @@ - (void)updateState:(State::Shared const &)state oldState:(State::Shared const &
192215

193216
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
194217
{
195-
[_containerView insertSubview:childComponentView atIndex:index];
218+
if (_isOnDemandViewMountingEnabled) {
219+
[_childComponentViews insertObject:childComponentView atIndex:index];
220+
} else {
221+
[_containerView insertSubview:childComponentView atIndex:index];
222+
}
196223
}
197224

198225
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
199226
{
200-
RCTAssert(childComponentView.superview == _containerView, @"Attempt to unmount improperly mounted component view.");
201-
[childComponentView removeFromSuperview];
227+
if (_isOnDemandViewMountingEnabled) {
228+
RCTAssert(
229+
[_childComponentViews objectAtIndex:index] == childComponentView,
230+
@"Attempt to unmount improperly mounted component view.");
231+
[_childComponentViews removeObjectAtIndex:index];
232+
// In addition to removing a view from `_childComponentViews`,
233+
// we have to unmount views immediately to not mess with recycling.
234+
[childComponentView removeFromSuperview];
235+
} else {
236+
RCTAssert(childComponentView.superview == _containerView, @"Attempt to unmount improperly mounted component view.");
237+
[childComponentView removeFromSuperview];
238+
}
202239
}
203240

204241
- (ScrollViewMetrics)_scrollViewMetrics
@@ -241,18 +278,19 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
241278
if (!_isUserTriggeredScrolling) {
242279
[self _updateStateWithContentOffset];
243280
}
244-
if (!_eventEmitter) {
245-
return;
246-
}
247281

248282
NSTimeInterval now = CACurrentMediaTime();
249283
if ((_lastScrollEventDispatchTime == 0) || (now - _lastScrollEventDispatchTime > _scrollEventThrottle)) {
250284
_lastScrollEventDispatchTime = now;
251-
std::static_pointer_cast<ScrollViewEventEmitter const>(_eventEmitter)->onScroll([self _scrollViewMetrics]);
285+
if (_eventEmitter) {
286+
std::static_pointer_cast<ScrollViewEventEmitter const>(_eventEmitter)->onScroll([self _scrollViewMetrics]);
287+
}
252288
// Once Fabric implements proper NativeAnimationDriver, this should be removed.
253289
// This is just a workaround to allow animations based on onScroll event.
254290
RCTSendPaperScrollEvent_DEPRECATED(scrollView, self.tag);
255291
}
292+
293+
[self _remountChildrenIfNeeded];
256294
}
257295

258296
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
@@ -395,6 +433,66 @@ - (void)scrollToEnd:(BOOL)animated
395433
[_scrollView setContentOffset:offset animated:animated];
396434
}
397435

436+
#pragma mark - Child views mounting
437+
438+
- (void)_remountChildrenIfNeeded
439+
{
440+
if (!_isOnDemandViewMountingEnabled) {
441+
return;
442+
}
443+
444+
CGPoint contentOffset = _scrollView.contentOffset;
445+
446+
if (std::abs(_contentOffsetWhenClipped.x - contentOffset.x) < kClippingLeeway &&
447+
std::abs(_contentOffsetWhenClipped.y - contentOffset.y) < kClippingLeeway) {
448+
return;
449+
}
450+
451+
_contentOffsetWhenClipped = contentOffset;
452+
453+
[self _remountChildren];
454+
}
455+
456+
- (void)_remountChildren
457+
{
458+
if (!_isOnDemandViewMountingEnabled) {
459+
return;
460+
}
461+
462+
CGRect containerFrame = CGRect{CGPointZero, _scrollView.bounds.size};
463+
containerFrame = UIEdgeInsetsInsetRect(containerFrame, _scrollView.contentInset);
464+
containerFrame.origin = _scrollView.contentOffset;
465+
466+
NSInteger mountedIndex = 0;
467+
for (UIView *componentView in _childComponentViews) {
468+
BOOL shouldBeMounted = YES;
469+
BOOL isMounted = componentView.superview != nil;
470+
471+
// If a view is mounted, it must be mounted exactly at `mountedIndex` position.
472+
RCTAssert(
473+
!isMounted || [_containerView.subviews objectAtIndex:mountedIndex] == componentView,
474+
@"Attempt to unmount improperly mounted component view.");
475+
476+
// It's simpler and faster to not mess with views that are not `RCTViewComponentView` subclasses.
477+
if ([componentView isKindOfClass:[RCTViewComponentView class]]) {
478+
CGRect viewFrameWithLeeway = CGRectInset(componentView.frame, -kClippingLeeway, -kClippingLeeway);
479+
shouldBeMounted = CGRectIntersectsRect(containerFrame, viewFrameWithLeeway);
480+
}
481+
482+
if (shouldBeMounted != isMounted) {
483+
if (shouldBeMounted) {
484+
[_containerView insertSubview:componentView atIndex:mountedIndex];
485+
} else {
486+
[componentView removeFromSuperview];
487+
}
488+
}
489+
490+
if (shouldBeMounted) {
491+
mountedIndex++;
492+
}
493+
}
494+
}
495+
398496
#pragma mark - RCTScrollableProtocol
399497

400498
- (CGSize)contentSize

0 commit comments

Comments
 (0)