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