RCTScrollView.m 42 KB

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