@onekeyfe/react-native-pager-view 3.0.158 → 3.0.160
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -4
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +379 -31
- package/android/src/main/java/com/reactnativepagerview/NativeScrollCoordinatorContributions.kt +15 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerPendingRestore.kt +26 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +41 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +600 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +145 -0
- package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
- package/android/src/main/jni/CMakeLists.txt +19 -0
- package/android/src/main/jni/pagerview.h +11 -0
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +5 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollCoordinatorContributionsTest.kt +26 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerPendingRestoreTest.kt +31 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +75 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerComponentDescriptor.h +8 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.cpp +15 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.h +23 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerState.h +33 -0
- package/docs/SPEC.md +450 -0
- package/ios/RNCCollapsiblePagerReleasedStatePolicy.h +8 -0
- package/ios/RNCCollapsiblePagerReleasedStatePolicy.m +13 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +414 -1
- package/ios/RNCNativeScrollerComponentView.h +17 -0
- package/ios/RNCNativeScrollerComponentView.mm +479 -0
- package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +80 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +788 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationWiringTests.mm +109 -0
- package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +218 -0
- package/ios/tests/RNCCollapsiblePagerReleasedStatePolicyTests.mm +23 -0
- package/ios/tests/RNCNativeScrollerTests.mm +252 -0
- package/lib/module/CollapsiblePagerView.js +10 -1
- package/lib/module/NativeScroller.js +171 -11
- package/lib/module/NativeScrollerNativeComponent.ts +101 -0
- package/lib/typescript/src/CollapsiblePagerView.d.ts +3 -3
- package/lib/typescript/src/NativeScroller.d.ts +15 -7
- package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +79 -0
- package/package.json +8 -4
- package/react-native-pager-view.podspec +2 -1
- package/react-native.config.js +15 -0
- package/src/CollapsiblePagerView.tsx +14 -4
- package/src/NativeScroller.tsx +291 -14
- package/src/NativeScrollerNativeComponent.ts +101 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
#import "RNCNativeScrollerComponentView.h"
|
|
2
|
+
|
|
3
|
+
#import <React/RCTConversions.h>
|
|
4
|
+
#import <react/renderer/components/pagerview/RNCNativeScrollerComponentDescriptor.h>
|
|
5
|
+
#import <react/renderer/components/pagerview/EventEmitters.h>
|
|
6
|
+
#import <react/renderer/components/pagerview/Props.h>
|
|
7
|
+
#import <react/renderer/components/pagerview/RCTComponentViewHelpers.h>
|
|
8
|
+
|
|
9
|
+
using namespace facebook::react;
|
|
10
|
+
|
|
11
|
+
@interface RNCNativeVerticalScrollView : UIScrollView
|
|
12
|
+
@end
|
|
13
|
+
@implementation RNCNativeVerticalScrollView
|
|
14
|
+
- (BOOL)touchesShouldCancelInContentView:(UIView *)view { return YES; }
|
|
15
|
+
@end
|
|
16
|
+
|
|
17
|
+
template <typename Event>
|
|
18
|
+
static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint target)
|
|
19
|
+
{
|
|
20
|
+
UIEdgeInsets inset = scroll.contentInset;
|
|
21
|
+
return {
|
|
22
|
+
.contentInset = {.top = inset.top, .right = inset.right, .bottom = inset.bottom, .left = inset.left},
|
|
23
|
+
.contentOffset = {.x = scroll.contentOffset.x, .y = scroll.contentOffset.y},
|
|
24
|
+
.contentSize = {.width = scroll.contentSize.width, .height = scroll.contentSize.height},
|
|
25
|
+
.layoutMeasurement = {.width = scroll.bounds.size.width, .height = scroll.bounds.size.height},
|
|
26
|
+
.velocity = {.x = velocity.x, .y = velocity.y},
|
|
27
|
+
.targetContentOffset = {.x = target.x, .y = target.y},
|
|
28
|
+
.zoomScale = 1,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@interface RNCNativeScrollerComponentView () <UIScrollViewDelegate, RCTRNCNativeScrollerViewProtocol>
|
|
33
|
+
@end
|
|
34
|
+
|
|
35
|
+
@implementation RNCNativeScrollerComponentView {
|
|
36
|
+
RNCNativeScrollerShadowNode::ConcreteState::Shared _state;
|
|
37
|
+
RNCNativeVerticalScrollView *_scrollView;
|
|
38
|
+
UIView<RCTComponentViewProtocol> *_reactContent;
|
|
39
|
+
UIRefreshControl *_refreshControl;
|
|
40
|
+
UIEdgeInsets _callerInset;
|
|
41
|
+
BOOL _dragActive;
|
|
42
|
+
BOOL _momentumActive;
|
|
43
|
+
BOOL _refreshEventSentForDrag;
|
|
44
|
+
BOOL _needsContentClamp;
|
|
45
|
+
BOOL _recycled;
|
|
46
|
+
BOOL _stopping;
|
|
47
|
+
NSUInteger _generation;
|
|
48
|
+
CFTimeInterval _lastScrollEvent;
|
|
49
|
+
NSTimeInterval _scrollEventThrottle;
|
|
50
|
+
NSMapTable<id, NSNumber *> *_pagerStickyHeights;
|
|
51
|
+
NSNumber *_lastContentViewportHeight;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
55
|
+
{
|
|
56
|
+
return concreteComponentDescriptorProvider<RNCNativeScrollerComponentDescriptor>();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
60
|
+
{
|
|
61
|
+
if ((self = [super initWithFrame:frame])) {
|
|
62
|
+
static const auto defaultProps = std::make_shared<const RNCNativeScrollerProps>();
|
|
63
|
+
_props = defaultProps;
|
|
64
|
+
_pagerStickyHeights = [NSMapTable weakToStrongObjectsMapTable];
|
|
65
|
+
_scrollView = [[RNCNativeVerticalScrollView alloc] initWithFrame:self.bounds];
|
|
66
|
+
_scrollView.delegate = self;
|
|
67
|
+
_scrollView.alwaysBounceVertical = YES;
|
|
68
|
+
_scrollView.alwaysBounceHorizontal = NO;
|
|
69
|
+
_scrollView.showsHorizontalScrollIndicator = NO;
|
|
70
|
+
_scrollView.directionalLockEnabled = YES;
|
|
71
|
+
_scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
|
72
|
+
self.contentView = _scrollView;
|
|
73
|
+
}
|
|
74
|
+
return self;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
- (UIScrollView *)scrollView { return _scrollView; }
|
|
78
|
+
|
|
79
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)child index:(NSInteger)index
|
|
80
|
+
{
|
|
81
|
+
// The wrapper supplies one Yoga-owned content view. Pager-owned shared headers
|
|
82
|
+
// are separate UIScrollView children and must never be removed or repositioned.
|
|
83
|
+
if (_reactContent != nil && _reactContent != child) {
|
|
84
|
+
[self stopScrolling];
|
|
85
|
+
[_reactContent removeFromSuperview];
|
|
86
|
+
}
|
|
87
|
+
_reactContent = child;
|
|
88
|
+
[_scrollView insertSubview:child atIndex:0];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)child index:(NSInteger)index
|
|
92
|
+
{
|
|
93
|
+
if (child != _reactContent) return;
|
|
94
|
+
[self stopScrolling];
|
|
95
|
+
[child removeFromSuperview];
|
|
96
|
+
_reactContent = nil;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
100
|
+
{
|
|
101
|
+
const auto &next = *std::static_pointer_cast<const RNCNativeScrollerProps>(props);
|
|
102
|
+
const auto &previous = *std::static_pointer_cast<const RNCNativeScrollerProps>(_props);
|
|
103
|
+
BOOL wasRecycled = _recycled;
|
|
104
|
+
_recycled = NO;
|
|
105
|
+
_scrollView.delegate = self;
|
|
106
|
+
if (!next.scrollEnabled && _scrollView.scrollEnabled) [self stopScrolling];
|
|
107
|
+
_scrollView.scrollEnabled = next.scrollEnabled;
|
|
108
|
+
_scrollView.showsVerticalScrollIndicator = next.showsVerticalScrollIndicator;
|
|
109
|
+
_scrollView.showsHorizontalScrollIndicator = next.showsHorizontalScrollIndicator;
|
|
110
|
+
_scrollView.bounces = next.bounces;
|
|
111
|
+
if (wasRecycled || previous.alwaysBounceVertical != next.alwaysBounceVertical) {
|
|
112
|
+
_scrollView.alwaysBounceVertical = next.alwaysBounceVertical;
|
|
113
|
+
}
|
|
114
|
+
_scrollView.decelerationRate = isfinite(next.decelerationRate)
|
|
115
|
+
? MIN(1, MAX(0, next.decelerationRate)) : UIScrollViewDecelerationRateNormal;
|
|
116
|
+
switch (next.keyboardDismissMode) {
|
|
117
|
+
case RNCNativeScrollerKeyboardDismissMode::OnDrag:
|
|
118
|
+
_scrollView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; break;
|
|
119
|
+
case RNCNativeScrollerKeyboardDismissMode::Interactive:
|
|
120
|
+
_scrollView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; break;
|
|
121
|
+
default: _scrollView.keyboardDismissMode = UIScrollViewKeyboardDismissModeNone; break;
|
|
122
|
+
}
|
|
123
|
+
_scrollEventThrottle = isfinite(next.scrollEventThrottle)
|
|
124
|
+
? MAX(0, next.scrollEventThrottle) / 1000.0 : 0;
|
|
125
|
+
if (_scrollEventThrottle <= 0.016) _scrollEventThrottle = 0;
|
|
126
|
+
|
|
127
|
+
UIEdgeInsets inset = UIEdgeInsetsMake(
|
|
128
|
+
isfinite(next.contentInsetTop) ? next.contentInsetTop : 0,
|
|
129
|
+
isfinite(next.contentInsetLeft) ? next.contentInsetLeft : 0,
|
|
130
|
+
isfinite(next.contentInsetBottom) ? next.contentInsetBottom : 0,
|
|
131
|
+
isfinite(next.contentInsetRight) ? next.contentInsetRight : 0);
|
|
132
|
+
if (!UIEdgeInsetsEqualToEdgeInsets(inset, _callerInset)) {
|
|
133
|
+
UIEdgeInsets oldInset = _callerInset;
|
|
134
|
+
UIEdgeInsets current = _scrollView.contentInset;
|
|
135
|
+
_callerInset = inset;
|
|
136
|
+
_scrollView.contentInset = UIEdgeInsetsMake(
|
|
137
|
+
current.top + inset.top - oldInset.top, current.left + inset.left - oldInset.left,
|
|
138
|
+
current.bottom + inset.bottom - oldInset.bottom, current.right + inset.right - oldInset.right);
|
|
139
|
+
for (UIView *owner = self.superview; owner != nil; owner = owner.superview) {
|
|
140
|
+
if ([owner conformsToProtocol:@protocol(RNCNativeScrollerInsetCoordinator)]) {
|
|
141
|
+
[(id<RNCNativeScrollerInsetCoordinator>)owner nativeScroller:self
|
|
142
|
+
callerInsetChangedFrom:oldInset to:inset];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
BOOL createdRefreshControl = next.refreshEnabled && _refreshControl == nil;
|
|
148
|
+
if (createdRefreshControl) {
|
|
149
|
+
_refreshControl = [UIRefreshControl new];
|
|
150
|
+
[_refreshControl addTarget:self action:@selector(refreshTriggered:)
|
|
151
|
+
forControlEvents:UIControlEventValueChanged];
|
|
152
|
+
_scrollView.refreshControl = _refreshControl;
|
|
153
|
+
} else if (!next.refreshEnabled && _refreshControl != nil) {
|
|
154
|
+
[_refreshControl endRefreshing];
|
|
155
|
+
_scrollView.refreshControl = nil;
|
|
156
|
+
_refreshControl = nil;
|
|
157
|
+
}
|
|
158
|
+
if (_refreshControl != nil) {
|
|
159
|
+
_refreshControl.tintColor = next.refreshTintColor ? RCTUIColorFromSharedColor(next.refreshTintColor) : nil;
|
|
160
|
+
NSString *title = RCTNSStringFromString(next.refreshTitle);
|
|
161
|
+
UIColor *titleColor = next.refreshTitleColor ? RCTUIColorFromSharedColor(next.refreshTitleColor) : nil;
|
|
162
|
+
_refreshControl.attributedTitle = title.length == 0 ? nil : [[NSAttributedString alloc]
|
|
163
|
+
initWithString:title attributes:titleColor == nil ? @{} : @{NSForegroundColorAttributeName: titleColor}];
|
|
164
|
+
if (previous.refreshProgressViewOffset != next.refreshProgressViewOffset || createdRefreshControl) {
|
|
165
|
+
CGRect bounds = _refreshControl.bounds;
|
|
166
|
+
bounds.origin.y = isfinite(next.refreshProgressViewOffset) ? -next.refreshProgressViewOffset : 0;
|
|
167
|
+
_refreshControl.bounds = bounds;
|
|
168
|
+
}
|
|
169
|
+
if (next.refreshing && !_refreshControl.refreshing) {
|
|
170
|
+
[_refreshControl beginRefreshing];
|
|
171
|
+
} else if (!next.refreshing && (previous.refreshing || createdRefreshControl)) {
|
|
172
|
+
[_refreshControl endRefreshing];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
CGSize size = CGSizeMake(isfinite(next.contentWidth) ? MAX(0, next.contentWidth) : 0,
|
|
177
|
+
isfinite(next.contentHeight) ? MAX(0, next.contentHeight) : 0);
|
|
178
|
+
if (!CGSizeEqualToSize(size, _scrollView.contentSize)) {
|
|
179
|
+
_needsContentClamp |= size.height < _scrollView.contentSize.height;
|
|
180
|
+
CGPoint offset = _scrollView.contentOffset;
|
|
181
|
+
BOOL wasPulling = offset.y < -_scrollView.adjustedContentInset.top;
|
|
182
|
+
_scrollView.contentSize = size;
|
|
183
|
+
// UIKit can synchronously discard a pull when content becomes empty.
|
|
184
|
+
// Upper-range shrink correction remains owned by the deferred clamp.
|
|
185
|
+
if (wasPulling && _scrollView.contentOffset.y != offset.y) {
|
|
186
|
+
[_scrollView setContentOffset:CGPointMake(_scrollView.contentOffset.x, offset.y) animated:NO];
|
|
187
|
+
}
|
|
188
|
+
[self scheduleContentClamp];
|
|
189
|
+
}
|
|
190
|
+
BOOL refreshEnded = previous.refreshing && !next.refreshing;
|
|
191
|
+
[super updateProps:props oldProps:oldProps];
|
|
192
|
+
if (refreshEnded) [self scheduleContentClamp];
|
|
193
|
+
[self updateFabricContentOrigin];
|
|
194
|
+
[self emitContentViewportChangeIfNeeded];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
|
|
198
|
+
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
|
|
199
|
+
{
|
|
200
|
+
CGSize previous = _scrollView.bounds.size;
|
|
201
|
+
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
202
|
+
[self updateFabricContentOrigin];
|
|
203
|
+
[self emitContentViewportChangeIfNeeded];
|
|
204
|
+
if (!CGSizeEqualToSize(previous, _scrollView.bounds.size)) {
|
|
205
|
+
_needsContentClamp = YES;
|
|
206
|
+
[self scheduleContentClamp];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
- (CGFloat)contentViewportHeight
|
|
211
|
+
{
|
|
212
|
+
CGFloat stickyHeight = 0;
|
|
213
|
+
for (NSNumber *height in _pagerStickyHeights.objectEnumerator) stickyHeight += height.doubleValue;
|
|
214
|
+
return MAX(0, _scrollView.bounds.size.height - _callerInset.top - _callerInset.bottom - stickyHeight);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
- (void)setPagerStickyHeight:(CGFloat)height forOwner:(id)owner
|
|
218
|
+
{
|
|
219
|
+
if (owner == nil) return;
|
|
220
|
+
CGFloat next = isfinite(height) ? MAX(0, height) : 0;
|
|
221
|
+
if (next == 0) [_pagerStickyHeights removeObjectForKey:owner];
|
|
222
|
+
else [_pagerStickyHeights setObject:@(next) forKey:owner];
|
|
223
|
+
[self emitContentViewportChangeIfNeeded];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
- (void)emitContentViewportChangeIfNeeded
|
|
227
|
+
{
|
|
228
|
+
if (_recycled || !_eventEmitter) return;
|
|
229
|
+
CGFloat height = [self contentViewportHeight];
|
|
230
|
+
if (_lastContentViewportHeight != nil && _lastContentViewportHeight.doubleValue == height) return;
|
|
231
|
+
_lastContentViewportHeight = @(height);
|
|
232
|
+
std::static_pointer_cast<const RNCNativeScrollerEventEmitter>(_eventEmitter)
|
|
233
|
+
->onContentViewportChange({.height = height});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
- (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
|
|
237
|
+
{
|
|
238
|
+
if (_eventEmitter != eventEmitter) _lastContentViewportHeight = nil;
|
|
239
|
+
[super updateEventEmitter:eventEmitter];
|
|
240
|
+
[self emitContentViewportChangeIfNeeded];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
|
|
244
|
+
{
|
|
245
|
+
_state = std::static_pointer_cast<const RNCNativeScrollerShadowNode::ConcreteState>(state);
|
|
246
|
+
[self updateFabricContentOrigin];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
- (void)updateFabricContentOrigin
|
|
250
|
+
{
|
|
251
|
+
if (!_state || _recycled) return;
|
|
252
|
+
facebook::react::Point origin = {-static_cast<Float>(_scrollView.contentOffset.x),
|
|
253
|
+
-static_cast<Float>(_scrollView.contentOffset.y)};
|
|
254
|
+
_state->updateState([origin](const RNCNativeScrollerShadowNode::ConcreteState::Data &oldData)
|
|
255
|
+
-> RNCNativeScrollerShadowNode::ConcreteState::SharedData {
|
|
256
|
+
if (oldData.contentOrigin == origin) return nullptr;
|
|
257
|
+
auto next = oldData;
|
|
258
|
+
next.contentOrigin = origin;
|
|
259
|
+
return std::make_shared<const RNCNativeScrollerShadowNode::ConcreteState::Data>(next);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
- (void)scheduleContentClamp
|
|
264
|
+
{
|
|
265
|
+
if (!_needsContentClamp) return;
|
|
266
|
+
NSUInteger generation = _generation;
|
|
267
|
+
__weak __typeof__(self) weakSelf = self;
|
|
268
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
269
|
+
__strong __typeof__(self) self = weakSelf;
|
|
270
|
+
if (self == nil || self->_generation != generation || self->_recycled) return;
|
|
271
|
+
[self clampContentOffsetIfNeeded];
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
- (void)clampContentOffsetIfNeeded
|
|
276
|
+
{
|
|
277
|
+
if (!_needsContentClamp || _scrollView.dragging || _scrollView.decelerating ||
|
|
278
|
+
_refreshControl.refreshing || _scrollView.bounds.size.height <= 0) return;
|
|
279
|
+
_needsContentClamp = NO;
|
|
280
|
+
CGFloat maximum = MAX(-_scrollView.adjustedContentInset.top,
|
|
281
|
+
_scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.adjustedContentInset.bottom);
|
|
282
|
+
if (_scrollView.contentOffset.y > maximum) {
|
|
283
|
+
[_scrollView setContentOffset:CGPointMake(_scrollView.contentOffset.x, maximum) animated:NO];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
- (void)refreshTriggered:(UIRefreshControl *)control
|
|
288
|
+
{
|
|
289
|
+
if (_recycled || _refreshEventSentForDrag || control != _refreshControl) return;
|
|
290
|
+
const auto &props = *std::static_pointer_cast<const RNCNativeScrollerProps>(_props);
|
|
291
|
+
if (!props.refreshEnabled || props.refreshing) return;
|
|
292
|
+
_refreshEventSentForDrag = YES;
|
|
293
|
+
if (_eventEmitter) {
|
|
294
|
+
std::static_pointer_cast<const RNCNativeScrollerEventEmitter>(_eventEmitter)->onRefresh({});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Every delegate path and imperative interruption uses the same terminal owner.
|
|
299
|
+
- (void)emitScrollEvent:(NSInteger)kind velocity:(CGPoint)velocity target:(CGPoint)target
|
|
300
|
+
{
|
|
301
|
+
if (_recycled || !_eventEmitter) return;
|
|
302
|
+
const auto emitter = std::static_pointer_cast<const RNCNativeScrollerEventEmitter>(_eventEmitter);
|
|
303
|
+
switch (kind) {
|
|
304
|
+
case 0: emitter->onScroll(RNCScrollerMetrics<RNCNativeScrollerEventEmitter::OnScroll>(_scrollView, velocity, target)); break;
|
|
305
|
+
case 1: emitter->onScrollBeginDrag(RNCScrollerMetrics<RNCNativeScrollerEventEmitter::OnScrollBeginDrag>(_scrollView, velocity, target)); break;
|
|
306
|
+
case 2: emitter->onScrollEndDrag(RNCScrollerMetrics<RNCNativeScrollerEventEmitter::OnScrollEndDrag>(_scrollView, velocity, target)); break;
|
|
307
|
+
case 3: emitter->onMomentumScrollBegin(RNCScrollerMetrics<RNCNativeScrollerEventEmitter::OnMomentumScrollBegin>(_scrollView, velocity, target)); break;
|
|
308
|
+
case 4: emitter->onMomentumScrollEnd(RNCScrollerMetrics<RNCNativeScrollerEventEmitter::OnMomentumScrollEnd>(_scrollView, velocity, target)); break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
- (void)finishMomentum
|
|
313
|
+
{
|
|
314
|
+
if (!_momentumActive) return;
|
|
315
|
+
_momentumActive = NO;
|
|
316
|
+
[self emitScrollEvent:4 velocity:CGPointZero target:_scrollView.contentOffset];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
- (void)stopScrolling
|
|
320
|
+
{
|
|
321
|
+
if (_stopping) return;
|
|
322
|
+
_stopping = YES;
|
|
323
|
+
[_scrollView setContentOffset:_scrollView.contentOffset animated:NO];
|
|
324
|
+
if (_dragActive) {
|
|
325
|
+
_dragActive = NO;
|
|
326
|
+
[self emitScrollEvent:2 velocity:CGPointZero target:_scrollView.contentOffset];
|
|
327
|
+
BOOL enabled = _scrollView.panGestureRecognizer.enabled;
|
|
328
|
+
_scrollView.panGestureRecognizer.enabled = NO;
|
|
329
|
+
_scrollView.panGestureRecognizer.enabled = enabled;
|
|
330
|
+
}
|
|
331
|
+
[self finishMomentum];
|
|
332
|
+
_stopping = NO;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
|
|
336
|
+
{
|
|
337
|
+
if (_recycled) return;
|
|
338
|
+
[self finishMomentum];
|
|
339
|
+
_dragActive = YES;
|
|
340
|
+
_refreshEventSentForDrag = NO;
|
|
341
|
+
_lastScrollEvent = 0;
|
|
342
|
+
[self emitScrollEvent:1 velocity:CGPointZero target:scrollView.contentOffset];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
|
346
|
+
{
|
|
347
|
+
if (_recycled) return;
|
|
348
|
+
[self updateFabricContentOrigin];
|
|
349
|
+
CFTimeInterval now = CACurrentMediaTime();
|
|
350
|
+
if (_lastScrollEvent != 0 && now - _lastScrollEvent < _scrollEventThrottle) return;
|
|
351
|
+
_lastScrollEvent = now;
|
|
352
|
+
[self emitScrollEvent:0 velocity:CGPointZero target:scrollView.contentOffset];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
|
|
356
|
+
withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)target
|
|
357
|
+
{
|
|
358
|
+
if (_recycled || !_dragActive) return;
|
|
359
|
+
_dragActive = NO;
|
|
360
|
+
[self emitScrollEvent:2 velocity:velocity target:*target];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
|
|
364
|
+
{
|
|
365
|
+
if (_recycled) return;
|
|
366
|
+
if (_dragActive) {
|
|
367
|
+
_dragActive = NO;
|
|
368
|
+
[self emitScrollEvent:2 velocity:CGPointZero target:scrollView.contentOffset];
|
|
369
|
+
}
|
|
370
|
+
if (!decelerate) [self scheduleContentClamp];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
|
|
374
|
+
{
|
|
375
|
+
if (_recycled || _stopping || _momentumActive) return;
|
|
376
|
+
_momentumActive = YES;
|
|
377
|
+
[self emitScrollEvent:3 velocity:CGPointZero target:scrollView.contentOffset];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
|
|
381
|
+
{
|
|
382
|
+
if (_recycled) return;
|
|
383
|
+
[self finishMomentum];
|
|
384
|
+
_lastScrollEvent = 0;
|
|
385
|
+
[self scrollViewDidScroll:scrollView];
|
|
386
|
+
[self scheduleContentClamp];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
|
|
390
|
+
{
|
|
391
|
+
[self scrollViewDidEndDecelerating:scrollView];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
- (void)scrollTo:(double)x y:(double)y animated:(BOOL)animated
|
|
395
|
+
{
|
|
396
|
+
if (_recycled || !isfinite(x) || !isfinite(y)) return;
|
|
397
|
+
[self stopScrolling];
|
|
398
|
+
UIEdgeInsets inset = _scrollView.adjustedContentInset;
|
|
399
|
+
CGPoint target = CGPointMake(MAX(-inset.left, MIN(x,
|
|
400
|
+
MAX(-inset.left, _scrollView.contentSize.width - _scrollView.bounds.size.width + inset.right))),
|
|
401
|
+
MAX(-inset.top, MIN(y,
|
|
402
|
+
MAX(-inset.top, _scrollView.contentSize.height - _scrollView.bounds.size.height + inset.bottom))));
|
|
403
|
+
if (CGPointEqualToPoint(target, _scrollView.contentOffset)) return;
|
|
404
|
+
if (animated) {
|
|
405
|
+
_momentumActive = YES;
|
|
406
|
+
[self emitScrollEvent:3 velocity:CGPointZero target:target];
|
|
407
|
+
}
|
|
408
|
+
[_scrollView setContentOffset:target animated:animated];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
- (void)scrollToEnd:(BOOL)animated
|
|
412
|
+
{
|
|
413
|
+
[self scrollTo:_scrollView.contentOffset.x
|
|
414
|
+
y:_scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.adjustedContentInset.bottom
|
|
415
|
+
animated:animated];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
- (void)setRefreshState:(BOOL)refreshing
|
|
419
|
+
{
|
|
420
|
+
[self setRefreshing:refreshing];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
- (void)setRefreshing:(BOOL)refreshing
|
|
424
|
+
{
|
|
425
|
+
if (_recycled || _refreshControl == nil) return;
|
|
426
|
+
if (refreshing) {
|
|
427
|
+
if (!_refreshControl.refreshing) [_refreshControl beginRefreshing];
|
|
428
|
+
} else {
|
|
429
|
+
[_refreshControl endRefreshing];
|
|
430
|
+
[self scheduleContentClamp];
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
- (void)flashScrollIndicators { [_scrollView flashScrollIndicators]; }
|
|
435
|
+
|
|
436
|
+
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
|
|
437
|
+
{
|
|
438
|
+
RCTRNCNativeScrollerHandleCommand(self, commandName, args);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
- (void)didMoveToWindow
|
|
442
|
+
{
|
|
443
|
+
[super didMoveToWindow];
|
|
444
|
+
if (self.window == nil) {
|
|
445
|
+
[self stopScrolling];
|
|
446
|
+
[_pagerStickyHeights removeAllObjects];
|
|
447
|
+
[self emitContentViewportChangeIfNeeded];
|
|
448
|
+
} else {
|
|
449
|
+
const auto &props = *std::static_pointer_cast<const RNCNativeScrollerProps>(_props);
|
|
450
|
+
if (props.refreshing) [_refreshControl beginRefreshing];
|
|
451
|
+
[self scheduleContentClamp];
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
- (void)prepareForRecycle
|
|
456
|
+
{
|
|
457
|
+
[self stopScrolling];
|
|
458
|
+
_recycled = YES;
|
|
459
|
+
_scrollView.delegate = nil;
|
|
460
|
+
++_generation;
|
|
461
|
+
_state.reset();
|
|
462
|
+
[_pagerStickyHeights removeAllObjects];
|
|
463
|
+
_lastContentViewportHeight = nil;
|
|
464
|
+
[_refreshControl endRefreshing];
|
|
465
|
+
_scrollView.refreshControl = nil;
|
|
466
|
+
_refreshControl = nil;
|
|
467
|
+
[_reactContent removeFromSuperview];
|
|
468
|
+
_reactContent = nil;
|
|
469
|
+
_scrollView.contentInset = UIEdgeInsetsZero;
|
|
470
|
+
_scrollView.contentSize = CGSizeZero;
|
|
471
|
+
_scrollView.contentOffset = CGPointZero;
|
|
472
|
+
_callerInset = UIEdgeInsetsZero;
|
|
473
|
+
_needsContentClamp = NO;
|
|
474
|
+
_refreshEventSentForDrag = NO;
|
|
475
|
+
_lastScrollEvent = 0;
|
|
476
|
+
[super prepareForRecycle];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
@end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#import <XCTest/XCTest.h>
|
|
2
|
+
|
|
3
|
+
#import "RNCCollapsiblePagerViewComponentView.h"
|
|
4
|
+
#import <react/renderer/components/pagerview/Props.h>
|
|
5
|
+
|
|
6
|
+
using namespace facebook::react;
|
|
7
|
+
|
|
8
|
+
@interface RNCCollapsiblePagerHitTestTests : XCTestCase
|
|
9
|
+
@property (nonatomic, strong) UIView *root;
|
|
10
|
+
@property (nonatomic, strong) UIButton *toolbar;
|
|
11
|
+
@property (nonatomic, strong) RNCCollapsiblePagerViewComponentView *pager;
|
|
12
|
+
@property (nonatomic, strong) UIButton *sticky;
|
|
13
|
+
@end
|
|
14
|
+
|
|
15
|
+
@implementation RNCCollapsiblePagerHitTestTests
|
|
16
|
+
|
|
17
|
+
- (void)setPointerEvents:(PointerEventsMode)mode
|
|
18
|
+
{
|
|
19
|
+
auto props = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
20
|
+
props->nativeSmoothHeaderScrollEnabled = true;
|
|
21
|
+
props->pointerEvents = mode;
|
|
22
|
+
[self.pager updateProps:props oldProps:nullptr];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
- (void)setUp
|
|
26
|
+
{
|
|
27
|
+
[super setUp];
|
|
28
|
+
self.root = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
|
|
29
|
+
self.toolbar = [[UIButton alloc] initWithFrame:CGRectMake(0, 36, 320, 64)];
|
|
30
|
+
[self.root addSubview:self.toolbar];
|
|
31
|
+
self.pager = [[RNCCollapsiblePagerViewComponentView alloc] init];
|
|
32
|
+
[self setPointerEvents:PointerEventsMode::Auto];
|
|
33
|
+
self.pager.frame = CGRectMake(0, 100, 320, 500);
|
|
34
|
+
[self.root addSubview:self.pager];
|
|
35
|
+
|
|
36
|
+
// Model smooth mode's header reparenting and its collapsed, clipped content.
|
|
37
|
+
UIView *scroll = [[UIView alloc] initWithFrame:self.pager.bounds];
|
|
38
|
+
[self.pager addSubview:scroll];
|
|
39
|
+
UIView *header = [self.pager valueForKey:@"sharedHeaderHostView"];
|
|
40
|
+
for (UIView *child in header.subviews.copy) [child removeFromSuperview];
|
|
41
|
+
[scroll addSubview:header];
|
|
42
|
+
header.frame = CGRectMake(0, -64, 320, 108);
|
|
43
|
+
[header addSubview:[[UIButton alloc] initWithFrame:CGRectMake(0, 0, 320, 64)]];
|
|
44
|
+
self.sticky = [[UIButton alloc] initWithFrame:CGRectMake(0, 140, 320, 44)];
|
|
45
|
+
[header addSubview:self.sticky];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (void)testCollapsedHeaderDoesNotInterceptSiblingToolbar
|
|
49
|
+
{
|
|
50
|
+
XCTAssertEqual([self.root hitTest:CGPointMake(20, 80) withEvent:nil], self.toolbar);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (void)testPinnedChildOutsideHeaderHostStillReceivesViewportTouches
|
|
54
|
+
{
|
|
55
|
+
XCTAssertEqual([self.pager hitTest:CGPointMake(20, 90) withEvent:nil], self.sticky);
|
|
56
|
+
[self setPointerEvents:PointerEventsMode::BoxNone];
|
|
57
|
+
XCTAssertEqual([self.pager hitTest:CGPointMake(20, 90) withEvent:nil], self.sticky);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
- (void)testFabricPointerEventsAreNotBypassed
|
|
61
|
+
{
|
|
62
|
+
[self setPointerEvents:PointerEventsMode::None];
|
|
63
|
+
XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
|
|
64
|
+
[self setPointerEvents:PointerEventsMode::BoxOnly];
|
|
65
|
+
XCTAssertEqual([self.pager hitTest:CGPointMake(20, 90) withEvent:nil], self.pager);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
- (void)testNoninteractivePagerDoesNotForwardHeaderTouches
|
|
69
|
+
{
|
|
70
|
+
self.pager.hidden = YES;
|
|
71
|
+
XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
|
|
72
|
+
self.pager.hidden = NO;
|
|
73
|
+
self.pager.userInteractionEnabled = NO;
|
|
74
|
+
XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
|
|
75
|
+
self.pager.userInteractionEnabled = YES;
|
|
76
|
+
self.pager.alpha = 0.005;
|
|
77
|
+
XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@end
|