1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | #import "RCTScrollView.h"
|
9 |
|
10 | #import <UIKit/UIKit.h>
|
11 |
|
12 | #import "RCTConvert.h"
|
13 | #import "RCTLog.h"
|
14 | #import "RCTUIManager.h"
|
15 | #import "RCTUIManagerObserverCoordinator.h"
|
16 | #import "RCTUIManagerUtils.h"
|
17 | #import "RCTUtils.h"
|
18 | #import "UIView+Private.h"
|
19 | #import "UIView+React.h"
|
20 | #import "RCTScrollEvent.h"
|
21 |
|
22 | #if !TARGET_OS_TV
|
23 | #import "RCTRefreshControl.h"
|
24 | #endif
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | @interface RCTCustomScrollView : UIScrollView<UIGestureRecognizerDelegate>
|
32 |
|
33 | @property (nonatomic, assign) BOOL centerContent;
|
34 | #if !TARGET_OS_TV
|
35 | @property (nonatomic, strong) UIView<RCTCustomRefreshContolProtocol> *customRefreshControl;
|
36 | @property (nonatomic, assign) BOOL pinchGestureEnabled;
|
37 | #endif
|
38 |
|
39 | @end
|
40 |
|
41 | @implementation RCTCustomScrollView
|
42 |
|
43 | - (instancetype)initWithFrame:(CGRect)frame
|
44 | {
|
45 | if ((self = [super initWithFrame:frame])) {
|
46 | [self.panGestureRecognizer addTarget:self action:@selector(handleCustomPan:)];
|
47 |
|
48 | if ([self respondsToSelector:@selector(setSemanticContentAttribute:)]) {
|
49 | // We intentionally force `UIScrollView`s `semanticContentAttribute` to `LTR` here
|
50 | // because this attribute affects a position of vertical scrollbar; we don't want this
|
51 | // scrollbar flip because we also flip it with whole `UIScrollView` flip.
|
52 | self.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
|
53 | }
|
54 |
|
55 | #if !TARGET_OS_TV
|
56 | _pinchGestureEnabled = YES;
|
57 | #endif
|
58 | }
|
59 | return self;
|
60 | }
|
61 |
|
62 | - (UIView *)contentView
|
63 | {
|
64 | return ((RCTScrollView *)self.superview).contentView;
|
65 | }
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | - (BOOL)_shouldDisableScrollInteraction
|
72 | {
|
73 | // Since this may be called on every pan, we need to make sure to only climb
|
74 | // the hierarchy on rare occasions.
|
75 | UIView *JSResponder = [RCTUIManager JSResponder];
|
76 | if (JSResponder && JSResponder != self.superview) {
|
77 | BOOL superviewHasResponder = [self isDescendantOfView:JSResponder];
|
78 | return superviewHasResponder;
|
79 | }
|
80 | return NO;
|
81 | }
|
82 |
|
83 | - (void)handleCustomPan:(__unused UIPanGestureRecognizer *)sender
|
84 | {
|
85 | if ([self _shouldDisableScrollInteraction] && ![[RCTUIManager JSResponder] isKindOfClass:[RCTScrollView class]]) {
|
86 | self.panGestureRecognizer.enabled = NO;
|
87 | self.panGestureRecognizer.enabled = YES;
|
88 | // TODO: If mid bounce, animate the scroll view to a non-bounced position
|
89 | // while disabling (but only if `stopScrollInteractionIfJSHasResponder` was
|
90 | // called *during* a `pan`). Currently, it will just snap into place which
|
91 | // is not so bad either.
|
92 | // Another approach:
|
93 | // self.scrollEnabled = NO;
|
94 | // self.scrollEnabled = YES;
|
95 | }
|
96 | }
|
97 |
|
98 | - (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated
|
99 | {
|
100 | // Limiting scroll area to an area where we actually have content.
|
101 | CGSize contentSize = self.contentSize;
|
102 | UIEdgeInsets contentInset = self.contentInset;
|
103 | CGSize fullSize = CGSizeMake(
|
104 | contentSize.width + contentInset.left + contentInset.right,
|
105 | contentSize.height + contentInset.top + contentInset.bottom);
|
106 |
|
107 | rect = CGRectIntersection((CGRect){CGPointZero, fullSize}, rect);
|
108 | if (CGRectIsNull(rect)) {
|
109 | return;
|
110 | }
|
111 |
|
112 | [super scrollRectToVisible:rect animated:animated];
|
113 | }
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
|
158 | {
|
159 | BOOL shouldDisableScrollInteraction = [self _shouldDisableScrollInteraction];
|
160 |
|
161 | if (shouldDisableScrollInteraction == NO) {
|
162 | [super touchesShouldCancelInContentView:view];
|
163 | }
|
164 |
|
165 | return !shouldDisableScrollInteraction;
|
166 | }
|
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 | - (void)setContentOffset:(CGPoint)contentOffset
|
175 | {
|
176 | UIView *contentView = [self contentView];
|
177 | if (contentView && _centerContent && !CGSizeEqualToSize(contentView.frame.size, CGSizeZero)) {
|
178 | CGSize subviewSize = contentView.frame.size;
|
179 | CGSize scrollViewSize = self.bounds.size;
|
180 | if (subviewSize.width <= scrollViewSize.width) {
|
181 | contentOffset.x = -(scrollViewSize.width - subviewSize.width) / 2.0;
|
182 | }
|
183 | if (subviewSize.height <= scrollViewSize.height) {
|
184 | contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0;
|
185 | }
|
186 | }
|
187 |
|
188 | super.contentOffset = CGPointMake(
|
189 | RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
|
190 | RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
|
191 | }
|
192 |
|
193 | - (void)setFrame:(CGRect)frame
|
194 | {
|
195 | // Preserving and revalidating `contentOffset`.
|
196 | CGPoint originalOffset = self.contentOffset;
|
197 |
|
198 | [super setFrame:frame];
|
199 |
|
200 | UIEdgeInsets contentInset = self.contentInset;
|
201 | CGSize contentSize = self.contentSize;
|
202 |
|
203 | // If contentSize has not been measured yet we can't check bounds.
|
204 | if (CGSizeEqualToSize(contentSize, CGSizeZero)) {
|
205 | self.contentOffset = originalOffset;
|
206 | } else {
|
207 | if (@available(iOS 11.0, *)) {
|
208 | if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, self.adjustedContentInset)) {
|
209 | contentInset = self.adjustedContentInset;
|
210 | }
|
211 | }
|
212 | CGSize boundsSize = self.bounds.size;
|
213 | CGFloat xMaxOffset = contentSize.width - boundsSize.width + contentInset.right;
|
214 | CGFloat yMaxOffset = contentSize.height - boundsSize.height + contentInset.bottom;
|
215 | // Make sure offset doesn't exceed bounds. This can happen on screen rotation.
|
216 | if ((originalOffset.x >= -contentInset.left) && (originalOffset.x <= xMaxOffset) &&
|
217 | (originalOffset.y >= -contentInset.top) && (originalOffset.y <= yMaxOffset)) {
|
218 | return;
|
219 | }
|
220 | self.contentOffset = CGPointMake(
|
221 | MAX(-contentInset.left, MIN(xMaxOffset, originalOffset.x)),
|
222 | MAX(-contentInset.top, MIN(yMaxOffset, originalOffset.y)));
|
223 | }
|
224 | }
|
225 |
|
226 | #if !TARGET_OS_TV
|
227 | - (void)setCustomRefreshControl:(UIView<RCTCustomRefreshContolProtocol> *)refreshControl
|
228 | {
|
229 | if (_customRefreshControl) {
|
230 | [_customRefreshControl removeFromSuperview];
|
231 | }
|
232 | _customRefreshControl = refreshControl;
|
233 | [self addSubview:_customRefreshControl];
|
234 | }
|
235 |
|
236 | - (void)setPinchGestureEnabled:(BOOL)pinchGestureEnabled
|
237 | {
|
238 | self.pinchGestureRecognizer.enabled = pinchGestureEnabled;
|
239 | _pinchGestureEnabled = pinchGestureEnabled;
|
240 | }
|
241 |
|
242 | - (void)didMoveToWindow
|
243 | {
|
244 | [super didMoveToWindow];
|
245 | // ScrollView enables pinch gesture late in its lifecycle. So simply setting it
|
246 | // in the setter gets overridden when the view loads.
|
247 | self.pinchGestureRecognizer.enabled = _pinchGestureEnabled;
|
248 | }
|
249 | #endif //TARGET_OS_TV
|
250 |
|
251 | - (BOOL)shouldGroupAccessibilityChildren
|
252 | {
|
253 | return YES;
|
254 | }
|
255 |
|
256 | @end
|
257 |
|
258 | @interface RCTScrollView () <RCTUIManagerObserver>
|
259 |
|
260 | @end
|
261 |
|
262 | @implementation RCTScrollView
|
263 | {
|
264 | RCTEventDispatcher *_eventDispatcher;
|
265 | CGRect _prevFirstVisibleFrame;
|
266 | __weak UIView *_firstVisibleView;
|
267 | RCTCustomScrollView *_scrollView;
|
268 | UIView *_contentView;
|
269 | NSTimeInterval _lastScrollDispatchTime;
|
270 | NSMutableArray<NSValue *> *_cachedChildFrames;
|
271 | BOOL _allowNextScrollNoMatterWhat;
|
272 | CGRect _lastClippedToRect;
|
273 | uint16_t _coalescingKey;
|
274 | NSString *_lastEmittedEventName;
|
275 | NSHashTable *_scrollListeners;
|
276 | }
|
277 |
|
278 | - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
|
279 | {
|
280 | RCTAssertParam(eventDispatcher);
|
281 |
|
282 | if ((self = [super initWithFrame:CGRectZero])) {
|
283 | _eventDispatcher = eventDispatcher;
|
284 |
|
285 | _scrollView = [[RCTCustomScrollView alloc] initWithFrame:CGRectZero];
|
286 | _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
287 | _scrollView.delegate = self;
|
288 | _scrollView.delaysContentTouches = NO;
|
289 |
|
290 | #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
|
291 | // `contentInsetAdjustmentBehavior` is only available since iOS 11.
|
292 | // We set the default behavior to "never" so that iOS
|
293 | // doesn't do weird things to UIScrollView insets automatically
|
294 | // and keeps it as an opt-in behavior.
|
295 | if ([_scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) {
|
296 | if (@available(iOS 11.0, *)) {
|
297 | _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
298 | }
|
299 | }
|
300 | #endif
|
301 |
|
302 | _automaticallyAdjustContentInsets = YES;
|
303 | _DEPRECATED_sendUpdatedChildFrames = NO;
|
304 | _contentInset = UIEdgeInsetsZero;
|
305 | _contentSize = CGSizeZero;
|
306 | _lastClippedToRect = CGRectNull;
|
307 |
|
308 | _scrollEventThrottle = 0.0;
|
309 | _lastScrollDispatchTime = 0;
|
310 | _cachedChildFrames = [NSMutableArray new];
|
311 |
|
312 | _scrollListeners = [NSHashTable weakObjectsHashTable];
|
313 |
|
314 | [self addSubview:_scrollView];
|
315 | }
|
316 | return self;
|
317 | }
|
318 |
|
319 | RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
|
320 | RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
321 |
|
322 | static inline void RCTApplyTransformationAccordingLayoutDirection(UIView *view, UIUserInterfaceLayoutDirection layoutDirection) {
|
323 | view.transform =
|
324 | layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ?
|
325 | CGAffineTransformIdentity :
|
326 | CGAffineTransformMakeScale(-1, 1);
|
327 | }
|
328 |
|
329 | - (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
|
330 | {
|
331 | [super setReactLayoutDirection:layoutDirection];
|
332 |
|
333 | RCTApplyTransformationAccordingLayoutDirection(_scrollView, layoutDirection);
|
334 | RCTApplyTransformationAccordingLayoutDirection(_contentView, layoutDirection);
|
335 | }
|
336 |
|
337 | - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
|
338 | {
|
339 | // Does nothing
|
340 | }
|
341 |
|
342 | - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
|
343 | {
|
344 | [super insertReactSubview:view atIndex:atIndex];
|
345 | #if !TARGET_OS_TV
|
346 | if ([view conformsToProtocol:@protocol(RCTCustomRefreshContolProtocol)]) {
|
347 | [_scrollView setCustomRefreshControl:(UIView<RCTCustomRefreshContolProtocol> *)view];
|
348 | if (![view isKindOfClass:[UIRefreshControl class]]
|
349 | && [view conformsToProtocol:@protocol(UIScrollViewDelegate)]) {
|
350 | [self addScrollListener:(UIView<UIScrollViewDelegate> *)view];
|
351 | }
|
352 | } else
|
353 | #endif
|
354 | {
|
355 | RCTAssert(_contentView == nil, @"RCTScrollView may only contain a single subview, the already set subview looks like: %@", [_contentView react_recursiveDescription]);
|
356 | _contentView = view;
|
357 | RCTApplyTransformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection);
|
358 | [_scrollView addSubview:view];
|
359 | }
|
360 | }
|
361 |
|
362 | - (void)removeReactSubview:(UIView *)subview
|
363 | {
|
364 | [super removeReactSubview:subview];
|
365 | #if !TARGET_OS_TV
|
366 | if ([subview conformsToProtocol:@protocol(RCTCustomRefreshContolProtocol)]) {
|
367 | [_scrollView setCustomRefreshControl:nil];
|
368 | if (![subview isKindOfClass:[UIRefreshControl class]]
|
369 | && [subview conformsToProtocol:@protocol(UIScrollViewDelegate)]) {
|
370 | [self removeScrollListener:(UIView<UIScrollViewDelegate> *)subview];
|
371 | }
|
372 | } else
|
373 | #endif
|
374 | {
|
375 | RCTAssert(_contentView == subview, @"Attempted to remove non-existent subview");
|
376 | _contentView = nil;
|
377 | }
|
378 | }
|
379 |
|
380 | - (void)didUpdateReactSubviews
|
381 | {
|
382 | // Do nothing, as subviews are managed by `insertReactSubview:atIndex:`
|
383 | }
|
384 |
|
385 | - (void)didSetProps:(NSArray<NSString *> *)changedProps
|
386 | {
|
387 | if ([changedProps containsObject:@"contentSize"]) {
|
388 | [self updateContentOffsetIfNeeded];
|
389 | }
|
390 | }
|
391 |
|
392 | - (BOOL)centerContent
|
393 | {
|
394 | return _scrollView.centerContent;
|
395 | }
|
396 |
|
397 | - (void)setCenterContent:(BOOL)centerContent
|
398 | {
|
399 | _scrollView.centerContent = centerContent;
|
400 | }
|
401 |
|
402 | - (void)setClipsToBounds:(BOOL)clipsToBounds
|
403 | {
|
404 | super.clipsToBounds = clipsToBounds;
|
405 | _scrollView.clipsToBounds = clipsToBounds;
|
406 | }
|
407 |
|
408 | - (void)dealloc
|
409 | {
|
410 | _scrollView.delegate = nil;
|
411 | [_eventDispatcher.bridge.uiManager.observerCoordinator removeObserver:self];
|
412 | }
|
413 |
|
414 | - (void)layoutSubviews
|
415 | {
|
416 | [super layoutSubviews];
|
417 | RCTAssert(self.subviews.count == 1, @"we should only have exactly one subview");
|
418 | RCTAssert([self.subviews lastObject] == _scrollView, @"our only subview should be a scrollview");
|
419 |
|
420 | #if !TARGET_OS_TV
|
421 | // Adjust the refresh control frame if the scrollview layout changes.
|
422 | UIView<RCTCustomRefreshContolProtocol> *refreshControl = _scrollView.customRefreshControl;
|
423 | if (refreshControl && refreshControl.isRefreshing) {
|
424 | refreshControl.frame = (CGRect){_scrollView.contentOffset, {_scrollView.frame.size.width, refreshControl.frame.size.height}};
|
425 | }
|
426 | #endif
|
427 |
|
428 | [self updateClippedSubviews];
|
429 | }
|
430 |
|
431 | - (void)updateClippedSubviews
|
432 | {
|
433 | // Find a suitable view to use for clipping
|
434 | UIView *clipView = [self react_findClipView];
|
435 | if (!clipView) {
|
436 | return;
|
437 | }
|
438 |
|
439 | static const CGFloat leeway = 1.0;
|
440 |
|
441 | const CGSize contentSize = _scrollView.contentSize;
|
442 | const CGRect bounds = _scrollView.bounds;
|
443 | const BOOL scrollsHorizontally = contentSize.width > bounds.size.width;
|
444 | const BOOL scrollsVertically = contentSize.height > bounds.size.height;
|
445 |
|
446 | const BOOL shouldClipAgain =
|
447 | CGRectIsNull(_lastClippedToRect) ||
|
448 | !CGRectEqualToRect(_lastClippedToRect, bounds) ||
|
449 | (scrollsHorizontally && (bounds.size.width < leeway || fabs(_lastClippedToRect.origin.x - bounds.origin.x) >= leeway)) ||
|
450 | (scrollsVertically && (bounds.size.height < leeway || fabs(_lastClippedToRect.origin.y - bounds.origin.y) >= leeway));
|
451 |
|
452 | if (shouldClipAgain) {
|
453 | const CGRect clipRect = CGRectInset(clipView.bounds, -leeway, -leeway);
|
454 | [self react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
|
455 | _lastClippedToRect = bounds;
|
456 | }
|
457 | }
|
458 |
|
459 | - (void)setContentInset:(UIEdgeInsets)contentInset
|
460 | {
|
461 | if (UIEdgeInsetsEqualToEdgeInsets(contentInset, _contentInset)) {
|
462 | return;
|
463 | }
|
464 |
|
465 | CGPoint contentOffset = _scrollView.contentOffset;
|
466 |
|
467 | _contentInset = contentInset;
|
468 | [RCTView autoAdjustInsetsForView:self
|
469 | withScrollView:_scrollView
|
470 | updateOffset:NO];
|
471 |
|
472 | _scrollView.contentOffset = contentOffset;
|
473 | }
|
474 |
|
475 | - (BOOL)isHorizontal:(UIScrollView *)scrollView
|
476 | {
|
477 | return scrollView.contentSize.width > self.frame.size.width;
|
478 | }
|
479 |
|
480 | - (void)scrollToOffset:(CGPoint)offset
|
481 | {
|
482 | [self scrollToOffset:offset animated:YES];
|
483 | }
|
484 |
|
485 | - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated
|
486 | {
|
487 | if (!CGPointEqualToPoint(_scrollView.contentOffset, offset)) {
|
488 | CGRect maxRect = CGRectMake(fmin(-_scrollView.contentInset.left, 0),
|
489 | fmin(-_scrollView.contentInset.top, 0),
|
490 | fmax(_scrollView.contentSize.width - _scrollView.bounds.size.width + _scrollView.contentInset.right + fmax(_scrollView.contentInset.left, 0), 0.01),
|
491 | fmax(_scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.contentInset.bottom + fmax(_scrollView.contentInset.top, 0), 0.01)); // Make width and height greater than 0
|
492 | // Ensure at least one scroll event will fire
|
493 | _allowNextScrollNoMatterWhat = YES;
|
494 | if (!CGRectContainsPoint(maxRect, offset) && !self.scrollToOverflowEnabled) {
|
495 | CGFloat x = fmax(offset.x, CGRectGetMinX(maxRect));
|
496 | x = fmin(x, CGRectGetMaxX(maxRect));
|
497 | CGFloat y = fmax(offset.y, CGRectGetMinY(maxRect));
|
498 | y = fmin(y, CGRectGetMaxY(maxRect));
|
499 | offset = CGPointMake(x, y);
|
500 | }
|
501 | [_scrollView setContentOffset:offset animated:animated];
|
502 | }
|
503 | }
|
504 |
|
505 |
|
506 |
|
507 |
|
508 |
|
509 | - (void)scrollToEnd:(BOOL)animated
|
510 | {
|
511 | BOOL isHorizontal = [self isHorizontal:_scrollView];
|
512 | CGPoint offset;
|
513 | if (isHorizontal) {
|
514 | CGFloat offsetX = _scrollView.contentSize.width - _scrollView.bounds.size.width + _scrollView.contentInset.right;
|
515 | offset = CGPointMake(fmax(offsetX, 0), 0);
|
516 | } else {
|
517 | CGFloat offsetY = _scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.contentInset.bottom;
|
518 | offset = CGPointMake(0, fmax(offsetY, 0));
|
519 | }
|
520 | if (!CGPointEqualToPoint(_scrollView.contentOffset, offset)) {
|
521 | // Ensure at least one scroll event will fire
|
522 | _allowNextScrollNoMatterWhat = YES;
|
523 | [_scrollView setContentOffset:offset animated:animated];
|
524 | }
|
525 | }
|
526 |
|
527 | - (void)zoomToRect:(CGRect)rect animated:(BOOL)animated
|
528 | {
|
529 | [_scrollView zoomToRect:rect animated:animated];
|
530 | }
|
531 |
|
532 | - (void)refreshContentInset
|
533 | {
|
534 | [RCTView autoAdjustInsetsForView:self
|
535 | withScrollView:_scrollView
|
536 | updateOffset:YES];
|
537 | }
|
538 |
|
539 | #pragma mark - ScrollView delegate
|
540 |
|
541 | #define RCT_SEND_SCROLL_EVENT(_eventName, _userData) { \
|
542 | NSString *eventName = NSStringFromSelector(@selector(_eventName)); \
|
543 | [self sendScrollEventWithName:eventName scrollView:_scrollView userData:_userData]; \
|
544 | }
|
545 |
|
546 | #define RCT_FORWARD_SCROLL_EVENT(call) \
|
547 | for (NSObject<UIScrollViewDelegate> *scrollViewListener in _scrollListeners) { \
|
548 | if ([scrollViewListener respondsToSelector:_cmd]) { \
|
549 | [scrollViewListener call]; \
|
550 | } \
|
551 | }
|
552 |
|
553 | #define RCT_SCROLL_EVENT_HANDLER(delegateMethod, eventName) \
|
554 | - (void)delegateMethod:(UIScrollView *)scrollView \
|
555 | { \
|
556 | RCT_SEND_SCROLL_EVENT(eventName, nil); \
|
557 | RCT_FORWARD_SCROLL_EVENT(delegateMethod:scrollView); \
|
558 | }
|
559 |
|
560 | RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin)
|
561 | RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
|
562 | RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
|
563 |
|
564 | - (void)addScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
|
565 | {
|
566 | [_scrollListeners addObject:scrollListener];
|
567 | }
|
568 |
|
569 | - (void)removeScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
|
570 | {
|
571 | [_scrollListeners removeObject:scrollListener];
|
572 | }
|
573 |
|
574 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
575 | {
|
576 | NSTimeInterval now = CACurrentMediaTime();
|
577 | [self updateClippedSubviews];
|
578 | |
579 |
|
580 |
|
581 |
|
582 |
|
583 |
|
584 |
|
585 |
|
586 |
|
587 | if (_allowNextScrollNoMatterWhat ||
|
588 | (_scrollEventThrottle > 0 && _scrollEventThrottle < MAX(0.017, now - _lastScrollDispatchTime))) {
|
589 |
|
590 | if (_DEPRECATED_sendUpdatedChildFrames) {
|
591 | // Calculate changed frames
|
592 | RCT_SEND_SCROLL_EVENT(onScroll, (@{@"updatedChildFrames": [self calculateChildFramesData]}));
|
593 | } else {
|
594 | RCT_SEND_SCROLL_EVENT(onScroll, nil);
|
595 | }
|
596 |
|
597 | // Update dispatch time
|
598 | _lastScrollDispatchTime = now;
|
599 | _allowNextScrollNoMatterWhat = NO;
|
600 | }
|
601 | RCT_FORWARD_SCROLL_EVENT(scrollViewDidScroll:scrollView);
|
602 | }
|
603 |
|
604 | - (NSArray<NSDictionary *> *)calculateChildFramesData
|
605 | {
|
606 | NSMutableArray<NSDictionary *> *updatedChildFrames = [NSMutableArray new];
|
607 | [[_contentView reactSubviews] enumerateObjectsUsingBlock:
|
608 | ^(UIView *subview, NSUInteger idx, __unused BOOL *stop) {
|
609 |
|
610 | // Check if new or changed
|
611 | CGRect newFrame = subview.frame;
|
612 | BOOL frameChanged = NO;
|
613 | if (self->_cachedChildFrames.count <= idx) {
|
614 | frameChanged = YES;
|
615 | [self->_cachedChildFrames addObject:[NSValue valueWithCGRect:newFrame]];
|
616 | } else if (!CGRectEqualToRect(newFrame, [self->_cachedChildFrames[idx] CGRectValue])) {
|
617 | frameChanged = YES;
|
618 | self->_cachedChildFrames[idx] = [NSValue valueWithCGRect:newFrame];
|
619 | }
|
620 |
|
621 | // Create JS frame object
|
622 | if (frameChanged) {
|
623 | [updatedChildFrames addObject: @{
|
624 | @"index": @(idx),
|
625 | @"x": @(newFrame.origin.x),
|
626 | @"y": @(newFrame.origin.y),
|
627 | @"width": @(newFrame.size.width),
|
628 | @"height": @(newFrame.size.height),
|
629 | }];
|
630 | }
|
631 | }];
|
632 |
|
633 | return updatedChildFrames;
|
634 | }
|
635 |
|
636 | - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
|
637 | {
|
638 | _allowNextScrollNoMatterWhat = YES; // Ensure next scroll event is recorded, regardless of throttle
|
639 | RCT_SEND_SCROLL_EVENT(onScrollBeginDrag, nil);
|
640 | RCT_FORWARD_SCROLL_EVENT(scrollViewWillBeginDragging:scrollView);
|
641 | }
|
642 |
|
643 | - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
|
644 | {
|
645 | if (self.snapToOffsets) {
|
646 | // An alternative to enablePaging and snapToInterval which allows setting custom
|
647 | // stopping points that don't have to be the same distance apart. Often seen in
|
648 | // apps which feature horizonally scrolling items. snapToInterval does not enforce
|
649 | // scrolling one interval at a time but guarantees that the scroll will stop at
|
650 | // a snap offset point.
|
651 |
|
652 | // Find which axis to snap
|
653 | BOOL isHorizontal = [self isHorizontal:scrollView];
|
654 | CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y;
|
655 | CGFloat offsetAlongAxis = isHorizontal ? _scrollView.contentOffset.x : _scrollView.contentOffset.y;
|
656 |
|
657 | // Calculate maximum content offset
|
658 | CGSize viewportSize = [self _calculateViewportSize];
|
659 | CGFloat maximumOffset = isHorizontal
|
660 | ? MAX(0, _scrollView.contentSize.width - viewportSize.width)
|
661 | : MAX(0, _scrollView.contentSize.height - viewportSize.height);
|
662 |
|
663 | // Calculate the snap offsets adjacent to the initial offset target
|
664 | CGFloat targetOffset = isHorizontal ? targetContentOffset->x : targetContentOffset->y;
|
665 | CGFloat smallerOffset = 0.0;
|
666 | CGFloat largerOffset = maximumOffset;
|
667 |
|
668 | for (unsigned long i = 0; i < self.snapToOffsets.count; i++) {
|
669 | CGFloat offset = [[self.snapToOffsets objectAtIndex:i] floatValue];
|
670 |
|
671 | if (offset <= targetOffset) {
|
672 | if (targetOffset - offset < targetOffset - smallerOffset) {
|
673 | smallerOffset = offset;
|
674 | }
|
675 | }
|
676 |
|
677 | if (offset >= targetOffset) {
|
678 | if (offset - targetOffset < largerOffset - targetOffset) {
|
679 | largerOffset = offset;
|
680 | }
|
681 | }
|
682 | }
|
683 |
|
684 | // Calculate the nearest offset
|
685 | CGFloat nearestOffset = targetOffset - smallerOffset < largerOffset - targetOffset
|
686 | ? smallerOffset
|
687 | : largerOffset;
|
688 |
|
689 | CGFloat firstOffset = [[self.snapToOffsets firstObject] floatValue];
|
690 | CGFloat lastOffset = [[self.snapToOffsets lastObject] floatValue];
|
691 |
|
692 | // if scrolling after the last snap offset and snapping to the
|
693 | // end of the list is disabled, then we allow free scrolling
|
694 | if (!self.snapToEnd && targetOffset >= lastOffset) {
|
695 | if (offsetAlongAxis >= lastOffset) {
|
696 | // free scrolling
|
697 | } else {
|
698 | // snap to end
|
699 | targetOffset = lastOffset;
|
700 | }
|
701 | } else if (!self.snapToStart && targetOffset <= firstOffset) {
|
702 | if (offsetAlongAxis <= firstOffset) {
|
703 | // free scrolling
|
704 | } else {
|
705 | // snap to beginning
|
706 | targetOffset = firstOffset;
|
707 | }
|
708 | } else if (velocityAlongAxis > 0.0) {
|
709 | targetOffset = largerOffset;
|
710 | } else if (velocityAlongAxis < 0.0) {
|
711 | targetOffset = smallerOffset;
|
712 | } else {
|
713 | targetOffset = nearestOffset;
|
714 | }
|
715 |
|
716 | // Make sure the new offset isn't out of bounds
|
717 | targetOffset = MIN(MAX(0, targetOffset), maximumOffset);
|
718 |
|
719 | // Set new targetContentOffset
|
720 | if (isHorizontal) {
|
721 | targetContentOffset->x = targetOffset;
|
722 | } else {
|
723 | targetContentOffset->y = targetOffset;
|
724 | }
|
725 | } else if (self.snapToInterval) {
|
726 | // An alternative to enablePaging which allows setting custom stopping intervals,
|
727 | // smaller than a full page size. Often seen in apps which feature horizonally
|
728 | // scrolling items. snapToInterval does not enforce scrolling one interval at a time
|
729 | // but guarantees that the scroll will stop at an interval point.
|
730 | CGFloat snapToIntervalF = (CGFloat)self.snapToInterval;
|
731 |
|
732 | // Find which axis to snap
|
733 | BOOL isHorizontal = [self isHorizontal:scrollView];
|
734 |
|
735 | // What is the current offset?
|
736 | CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y;
|
737 | CGFloat targetContentOffsetAlongAxis = targetContentOffset->y;
|
738 | if (isHorizontal) {
|
739 | // Use current scroll offset to determine the next index to snap to when momentum disabled
|
740 | targetContentOffsetAlongAxis = self.disableIntervalMomentum ? scrollView.contentOffset.x : targetContentOffset->x;
|
741 | }
|
742 |
|
743 | // Offset based on desired alignment
|
744 | CGFloat frameLength = isHorizontal ? self.frame.size.width : self.frame.size.height;
|
745 | CGFloat alignmentOffset = 0.0f;
|
746 | if ([self.snapToAlignment isEqualToString: @"center"]) {
|
747 | alignmentOffset = (frameLength * 0.5f) + (snapToIntervalF * 0.5f);
|
748 | } else if ([self.snapToAlignment isEqualToString: @"end"]) {
|
749 | alignmentOffset = frameLength;
|
750 | }
|
751 |
|
752 | // Pick snap point based on direction and proximity
|
753 | CGFloat fractionalIndex = (targetContentOffsetAlongAxis + alignmentOffset) / snapToIntervalF;
|
754 |
|
755 | NSInteger snapIndex =
|
756 | velocityAlongAxis > 0.0 ?
|
757 | ceil(fractionalIndex) :
|
758 | velocityAlongAxis < 0.0 ?
|
759 | floor(fractionalIndex) :
|
760 | round(fractionalIndex);
|
761 | CGFloat newTargetContentOffset = (snapIndex * snapToIntervalF) - alignmentOffset;
|
762 |
|
763 | // Set new targetContentOffset
|
764 | if (isHorizontal) {
|
765 | targetContentOffset->x = newTargetContentOffset;
|
766 | } else {
|
767 | targetContentOffset->y = newTargetContentOffset;
|
768 | }
|
769 | }
|
770 |
|
771 | NSDictionary *userData = @{
|
772 | @"velocity": @{
|
773 | @"x": @(velocity.x),
|
774 | @"y": @(velocity.y)
|
775 | },
|
776 | @"targetContentOffset": @{
|
777 | @"x": @(targetContentOffset->x),
|
778 | @"y": @(targetContentOffset->y)
|
779 | }
|
780 | };
|
781 | RCT_SEND_SCROLL_EVENT(onScrollEndDrag, userData);
|
782 | RCT_FORWARD_SCROLL_EVENT(scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset);
|
783 | }
|
784 |
|
785 | - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
|
786 | {
|
787 | RCT_FORWARD_SCROLL_EVENT(scrollViewDidEndDragging:scrollView willDecelerate:decelerate);
|
788 | }
|
789 |
|
790 | - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
|
791 | {
|
792 | RCT_SEND_SCROLL_EVENT(onScrollBeginDrag, nil);
|
793 | RCT_FORWARD_SCROLL_EVENT(scrollViewWillBeginZooming:scrollView withView:view);
|
794 | }
|
795 |
|
796 | - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale
|
797 | {
|
798 | RCT_SEND_SCROLL_EVENT(onScrollEndDrag, nil);
|
799 | RCT_FORWARD_SCROLL_EVENT(scrollViewDidEndZooming:scrollView withView:view atScale:scale);
|
800 | }
|
801 |
|
802 | - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
|
803 | {
|
804 | // Fire a final scroll event
|
805 | _allowNextScrollNoMatterWhat = YES;
|
806 | [self scrollViewDidScroll:scrollView];
|
807 |
|
808 | // Fire the end deceleration event
|
809 | RCT_SEND_SCROLL_EVENT(onMomentumScrollEnd, nil);
|
810 | RCT_FORWARD_SCROLL_EVENT(scrollViewDidEndDecelerating:scrollView);
|
811 | }
|
812 |
|
813 | - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
|
814 | {
|
815 | // Fire a final scroll event
|
816 | _allowNextScrollNoMatterWhat = YES;
|
817 | [self scrollViewDidScroll:scrollView];
|
818 |
|
819 | // Fire the end deceleration event
|
820 | RCT_SEND_SCROLL_EVENT(onMomentumScrollEnd, nil);
|
821 | RCT_FORWARD_SCROLL_EVENT(scrollViewDidEndScrollingAnimation:scrollView);
|
822 | }
|
823 |
|
824 | - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
|
825 | {
|
826 | for (NSObject<UIScrollViewDelegate> *scrollListener in _scrollListeners) {
|
827 | if ([scrollListener respondsToSelector:_cmd] &&
|
828 | ![scrollListener scrollViewShouldScrollToTop:scrollView]) {
|
829 | return NO;
|
830 | }
|
831 | }
|
832 |
|
833 | if (self.inverted) {
|
834 | [self scrollToEnd:YES];
|
835 | return NO;
|
836 | }
|
837 |
|
838 | return YES;
|
839 | }
|
840 |
|
841 | - (UIView *)viewForZoomingInScrollView:(__unused UIScrollView *)scrollView
|
842 | {
|
843 | return _contentView;
|
844 | }
|
845 |
|
846 | #pragma mark - Setters
|
847 |
|
848 | - (CGSize)_calculateViewportSize
|
849 | {
|
850 | CGSize viewportSize = self.bounds.size;
|
851 | if (_automaticallyAdjustContentInsets) {
|
852 | UIEdgeInsets contentInsets = [RCTView contentInsetsForView:self];
|
853 | viewportSize = CGSizeMake(self.bounds.size.width - contentInsets.left - contentInsets.right,
|
854 | self.bounds.size.height - contentInsets.top - contentInsets.bottom);
|
855 | }
|
856 | return viewportSize;
|
857 | }
|
858 |
|
859 | - (CGPoint)calculateOffsetForContentSize:(CGSize)newContentSize
|
860 | {
|
861 | CGPoint oldOffset = _scrollView.contentOffset;
|
862 | CGPoint newOffset = oldOffset;
|
863 |
|
864 | CGSize oldContentSize = _scrollView.contentSize;
|
865 | CGSize viewportSize = [self _calculateViewportSize];
|
866 |
|
867 | BOOL fitsinViewportY = oldContentSize.height <= viewportSize.height && newContentSize.height <= viewportSize.height;
|
868 | if (newContentSize.height < oldContentSize.height && !fitsinViewportY) {
|
869 | CGFloat offsetHeight = oldOffset.y + viewportSize.height;
|
870 | if (oldOffset.y < 0) {
|
871 | // overscrolled on top, leave offset alone
|
872 | } else if (offsetHeight > oldContentSize.height) {
|
873 | // overscrolled on the bottom, preserve overscroll amount
|
874 | newOffset.y = MAX(0, oldOffset.y - (oldContentSize.height - newContentSize.height));
|
875 | } else if (offsetHeight > newContentSize.height) {
|
876 | // offset falls outside of bounds, scroll back to end of list
|
877 | newOffset.y = MAX(0, newContentSize.height - viewportSize.height);
|
878 | }
|
879 | }
|
880 |
|
881 | BOOL fitsinViewportX = oldContentSize.width <= viewportSize.width && newContentSize.width <= viewportSize.width;
|
882 | if (newContentSize.width < oldContentSize.width && !fitsinViewportX) {
|
883 | CGFloat offsetHeight = oldOffset.x + viewportSize.width;
|
884 | if (oldOffset.x < 0) {
|
885 | // overscrolled at the beginning, leave offset alone
|
886 | } else if (offsetHeight > oldContentSize.width && newContentSize.width > viewportSize.width) {
|
887 | // overscrolled at the end, preserve overscroll amount as much as possible
|
888 | newOffset.x = MAX(0, oldOffset.x - (oldContentSize.width - newContentSize.width));
|
889 | } else if (offsetHeight > newContentSize.width) {
|
890 | // offset falls outside of bounds, scroll back to end
|
891 | newOffset.x = MAX(0, newContentSize.width - viewportSize.width);
|
892 | }
|
893 | }
|
894 |
|
895 | // all other cases, offset doesn't change
|
896 | return newOffset;
|
897 | }
|
898 |
|
899 |
|
900 |
|
901 |
|
902 |
|
903 |
|
904 | - (CGSize)contentSize
|
905 | {
|
906 | if (!CGSizeEqualToSize(_contentSize, CGSizeZero)) {
|
907 | return _contentSize;
|
908 | }
|
909 |
|
910 | return _contentView.frame.size;
|
911 | }
|
912 |
|
913 | - (void)updateContentOffsetIfNeeded
|
914 | {
|
915 | CGSize contentSize = self.contentSize;
|
916 | if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) {
|
917 | // When contentSize is set manually, ScrollView internals will reset
|
918 | // contentOffset to {0, 0}. Since we potentially set contentSize whenever
|
919 | // anything in the ScrollView updates, we workaround this issue by manually
|
920 | // adjusting contentOffset whenever this happens
|
921 | CGPoint newOffset = [self calculateOffsetForContentSize:contentSize];
|
922 | _scrollView.contentSize = contentSize;
|
923 | _scrollView.contentOffset = newOffset;
|
924 | }
|
925 | }
|
926 |
|
927 | // maintainVisibleContentPosition is used to allow seamless loading of content from both ends of
|
928 | // the scrollview without the visible content jumping in position.
|
929 | - (void)setMaintainVisibleContentPosition:(NSDictionary *)maintainVisibleContentPosition
|
930 | {
|
931 | if (maintainVisibleContentPosition != nil && _maintainVisibleContentPosition == nil) {
|
932 | [_eventDispatcher.bridge.uiManager.observerCoordinator addObserver:self];
|
933 | } else if (maintainVisibleContentPosition == nil && _maintainVisibleContentPosition != nil) {
|
934 | [_eventDispatcher.bridge.uiManager.observerCoordinator removeObserver:self];
|
935 | }
|
936 | _maintainVisibleContentPosition = maintainVisibleContentPosition;
|
937 | }
|
938 |
|
939 | #pragma mark - RCTUIManagerObserver
|
940 |
|
941 | - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager
|
942 | {
|
943 | RCTAssertUIManagerQueue();
|
944 | [manager prependUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
945 | BOOL horz = [self isHorizontal:self->_scrollView];
|
946 | NSUInteger minIdx = [self->_maintainVisibleContentPosition[@"minIndexForVisible"] integerValue];
|
947 | for (NSUInteger ii = minIdx; ii < self->_contentView.subviews.count; ++ii) {
|
948 | // Find the first entirely visible view. This must be done after we update the content offset
|
949 | // or it will tend to grab rows that were made visible by the shift in position
|
950 | UIView *subview = self->_contentView.subviews[ii];
|
951 | if ((horz
|
952 | ? subview.frame.origin.x >= self->_scrollView.contentOffset.x
|
953 | : subview.frame.origin.y >= self->_scrollView.contentOffset.y) ||
|
954 | ii == self->_contentView.subviews.count - 1) {
|
955 | self->_prevFirstVisibleFrame = subview.frame;
|
956 | self->_firstVisibleView = subview;
|
957 | break;
|
958 | }
|
959 | }
|
960 | }];
|
961 | [manager addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
962 | if (self->_maintainVisibleContentPosition == nil) {
|
963 | return; // The prop might have changed in the previous UIBlocks, so need to abort here.
|
964 | }
|
965 | NSNumber *autoscrollThreshold = self->_maintainVisibleContentPosition[@"autoscrollToTopThreshold"];
|
966 | // TODO: detect and handle/ignore re-ordering
|
967 | if ([self isHorizontal:self->_scrollView]) {
|
968 | CGFloat deltaX = self->_firstVisibleView.frame.origin.x - self->_prevFirstVisibleFrame.origin.x;
|
969 | if (ABS(deltaX) > 0.1) {
|
970 | self->_scrollView.contentOffset = CGPointMake(
|
971 | self->_scrollView.contentOffset.x + deltaX,
|
972 | self->_scrollView.contentOffset.y
|
973 | );
|
974 | if (autoscrollThreshold != nil) {
|
975 | // If the offset WAS within the threshold of the start, animate to the start.
|
976 | if (self->_scrollView.contentOffset.x - deltaX <= [autoscrollThreshold integerValue]) {
|
977 | [self scrollToOffset:CGPointMake(0, self->_scrollView.contentOffset.y) animated:YES];
|
978 | }
|
979 | }
|
980 | }
|
981 | } else {
|
982 | CGRect newFrame = self->_firstVisibleView.frame;
|
983 | CGFloat deltaY = newFrame.origin.y - self->_prevFirstVisibleFrame.origin.y;
|
984 | if (ABS(deltaY) > 0.1) {
|
985 | self->_scrollView.contentOffset = CGPointMake(
|
986 | self->_scrollView.contentOffset.x,
|
987 | self->_scrollView.contentOffset.y + deltaY
|
988 | );
|
989 | if (autoscrollThreshold != nil) {
|
990 | // If the offset WAS within the threshold of the start, animate to the start.
|
991 | if (self->_scrollView.contentOffset.y - deltaY <= [autoscrollThreshold integerValue]) {
|
992 | [self scrollToOffset:CGPointMake(self->_scrollView.contentOffset.x, 0) animated:YES];
|
993 | }
|
994 | }
|
995 | }
|
996 | }
|
997 | }];
|
998 | }
|
999 |
|
1000 | // Note: setting several properties of UIScrollView has the effect of
|
1001 | // resetting its contentOffset to {0, 0}. To prevent this, we generate
|
1002 | // setters here that will record the contentOffset beforehand, and
|
1003 | // restore it after the property has been set.
|
1004 |
|
1005 | #define RCT_SET_AND_PRESERVE_OFFSET(setter, getter, type) \
|
1006 | - (void)setter:(type)value \
|
1007 | { \
|
1008 | CGPoint contentOffset = _scrollView.contentOffset; \
|
1009 | [_scrollView setter:value]; \
|
1010 | _scrollView.contentOffset = contentOffset; \
|
1011 | } \
|
1012 | - (type)getter \
|
1013 | { \
|
1014 | return [_scrollView getter]; \
|
1015 | }
|
1016 |
|
1017 | RCT_SET_AND_PRESERVE_OFFSET(setAlwaysBounceHorizontal, alwaysBounceHorizontal, BOOL)
|
1018 | RCT_SET_AND_PRESERVE_OFFSET(setAlwaysBounceVertical, alwaysBounceVertical, BOOL)
|
1019 | RCT_SET_AND_PRESERVE_OFFSET(setBounces, bounces, BOOL)
|
1020 | RCT_SET_AND_PRESERVE_OFFSET(setBouncesZoom, bouncesZoom, BOOL)
|
1021 | RCT_SET_AND_PRESERVE_OFFSET(setCanCancelContentTouches, canCancelContentTouches, BOOL)
|
1022 | RCT_SET_AND_PRESERVE_OFFSET(setDecelerationRate, decelerationRate, CGFloat)
|
1023 | RCT_SET_AND_PRESERVE_OFFSET(setDirectionalLockEnabled, isDirectionalLockEnabled, BOOL)
|
1024 | RCT_SET_AND_PRESERVE_OFFSET(setIndicatorStyle, indicatorStyle, UIScrollViewIndicatorStyle)
|
1025 | RCT_SET_AND_PRESERVE_OFFSET(setKeyboardDismissMode, keyboardDismissMode, UIScrollViewKeyboardDismissMode)
|
1026 | RCT_SET_AND_PRESERVE_OFFSET(setMaximumZoomScale, maximumZoomScale, CGFloat)
|
1027 | RCT_SET_AND_PRESERVE_OFFSET(setMinimumZoomScale, minimumZoomScale, CGFloat)
|
1028 | RCT_SET_AND_PRESERVE_OFFSET(setScrollEnabled, isScrollEnabled, BOOL)
|
1029 | #if !TARGET_OS_TV
|
1030 | RCT_SET_AND_PRESERVE_OFFSET(setPagingEnabled, isPagingEnabled, BOOL)
|
1031 | RCT_SET_AND_PRESERVE_OFFSET(setScrollsToTop, scrollsToTop, BOOL)
|
1032 | #endif
|
1033 | RCT_SET_AND_PRESERVE_OFFSET(setShowsHorizontalScrollIndicator, showsHorizontalScrollIndicator, BOOL)
|
1034 | RCT_SET_AND_PRESERVE_OFFSET(setShowsVerticalScrollIndicator, showsVerticalScrollIndicator, BOOL)
|
1035 | RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, zoomScale, CGFloat);
|
1036 | RCT_SET_AND_PRESERVE_OFFSET(setScrollIndicatorInsets, scrollIndicatorInsets, UIEdgeInsets);
|
1037 |
|
1038 | #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
|
1039 | - (void)setContentInsetAdjustmentBehavior:(UIScrollViewContentInsetAdjustmentBehavior)behavior
|
1040 | API_AVAILABLE(ios(11.0)){
|
1041 | // `contentInsetAdjustmentBehavior` is available since iOS 11.
|
1042 | if ([_scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) {
|
1043 | CGPoint contentOffset = _scrollView.contentOffset;
|
1044 | if (@available(iOS 11.0, *)) {
|
1045 | _scrollView.contentInsetAdjustmentBehavior = behavior;
|
1046 | }
|
1047 | _scrollView.contentOffset = contentOffset;
|
1048 | }
|
1049 | }
|
1050 | #endif
|
1051 |
|
1052 | - (void)sendScrollEventWithName:(NSString *)eventName
|
1053 | scrollView:(UIScrollView *)scrollView
|
1054 | userData:(NSDictionary *)userData
|
1055 | {
|
1056 | if (![_lastEmittedEventName isEqualToString:eventName]) {
|
1057 | _coalescingKey++;
|
1058 | _lastEmittedEventName = [eventName copy];
|
1059 | }
|
1060 | RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc] initWithEventName:eventName
|
1061 | reactTag:self.reactTag
|
1062 | scrollViewContentOffset:scrollView.contentOffset
|
1063 | scrollViewContentInset:scrollView.contentInset
|
1064 | scrollViewContentSize:scrollView.contentSize
|
1065 | scrollViewFrame:scrollView.frame
|
1066 | scrollViewZoomScale:scrollView.zoomScale
|
1067 | userData:userData
|
1068 | coalescingKey:_coalescingKey];
|
1069 | [_eventDispatcher sendEvent:scrollEvent];
|
1070 | }
|
1071 |
|
1072 | @end
|
1073 |
|
1074 | @implementation RCTEventDispatcher (RCTScrollView)
|
1075 |
|
1076 | - (void)sendFakeScrollEvent:(NSNumber *)reactTag
|
1077 | {
|
1078 | // Use the selector here in case the onScroll block property is ever renamed
|
1079 | NSString *eventName = NSStringFromSelector(@selector(onScroll));
|
1080 | RCTScrollEvent *fakeScrollEvent = [[RCTScrollEvent alloc] initWithEventName:eventName
|
1081 | reactTag:reactTag
|
1082 | scrollViewContentOffset:CGPointZero
|
1083 | scrollViewContentInset:UIEdgeInsetsZero
|
1084 | scrollViewContentSize:CGSizeZero
|
1085 | scrollViewFrame:CGRectZero
|
1086 | scrollViewZoomScale:0
|
1087 | userData:nil
|
1088 | coalescingKey:0];
|
1089 | [self sendEvent:fakeScrollEvent];
|
1090 | }
|
1091 |
|
1092 | @end
|