Skip to content

Commit e70c315

Browse files
authored
Add overdrag property on iOS (#107)
1 parent 73a07f6 commit e70c315

File tree

8 files changed

+79
-28
lines changed

8 files changed

+79
-28
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const styles = StyleSheet.create({
125125
|`orientation: Orientation`|Set `horizontal` or `vertical` scrolling orientation (it does **not** work dynamically)|both
126126
|`transitionStyle: TransitionStyle`|Use `scroll` or `curl` to change transition style (it does **not** work dynamically)|iOS
127127
|`showPageIndicator: boolean`|Shows the dots indicator at the bottom of the view|iOS
128+
|`overdrag: boolean`|Allows for overscrolling after reaching the end or very beginning or pages|iOS
128129

129130
## Preview
130131

example/ViewPagerExample.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type State = {
3636
pages: Array<CreatePage>,
3737
scrollState: PageScrollState,
3838
dotsVisible: boolean,
39+
overdragEnabled: boolean,
3940
};
4041

4142
export default class ViewPagerExample extends React.Component<*, State> {
@@ -60,6 +61,7 @@ export default class ViewPagerExample extends React.Component<*, State> {
6061
pages: pages,
6162
scrollState: 'idle',
6263
dotsVisible: false,
64+
overdragEnabled: true,
6365
};
6466
this.viewPager = React.createRef();
6567
}
@@ -117,7 +119,13 @@ export default class ViewPagerExample extends React.Component<*, State> {
117119
};
118120

119121
render() {
120-
const {page, pages, animationsAreEnabled, dotsVisible} = this.state;
122+
const {
123+
page,
124+
pages,
125+
animationsAreEnabled,
126+
dotsVisible,
127+
overdragEnabled,
128+
} = this.state;
121129
return (
122130
<SafeAreaView style={styles.container}>
123131
<ViewPager
@@ -133,7 +141,8 @@ export default class ViewPagerExample extends React.Component<*, State> {
133141
// Lib does not support dynamically transitionStyle change
134142
transitionStyle="scroll"
135143
showPageIndicator={dotsVisible}
136-
ref={this.viewPager}>
144+
ref={this.viewPager}
145+
overdrag={overdragEnabled}>
137146
{pages.map(p => this.renderPage(p))}
138147
</ViewPager>
139148
<View style={styles.buttons}>
@@ -146,6 +155,19 @@ export default class ViewPagerExample extends React.Component<*, State> {
146155
this.setState({scrollEnabled: !this.state.scrollEnabled})
147156
}
148157
/>
158+
<Button
159+
enabled={true}
160+
text={
161+
this.state.overdragEnabled
162+
? 'Overdrag Enabled'
163+
: 'Overdrag Disabled'
164+
}
165+
onPress={() =>
166+
this.setState(state => {
167+
overdragEnabled: !state.overdragEnabled;
168+
})
169+
}
170+
/>
149171
<Button
150172
enabled={true}
151173
text={dotsVisible ? 'Hide dots' : 'Show dots'}

example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ PODS:
182182
- React-cxxreact (= 0.61.4)
183183
- React-jsi (= 0.61.4)
184184
- React-jsinspector (0.61.4)
185-
- react-native-viewpager (2.0.2):
185+
- react-native-viewpager (3.1.0):
186186
- React
187187
- React-RCTActionSheet (0.61.4):
188188
- React-Core/RCTActionSheetHeaders (= 0.61.4)
@@ -326,7 +326,7 @@ SPEC CHECKSUMS:
326326
React-jsi: ca921f4041505f9d5197139b2d09eeb020bb12e8
327327
React-jsiexecutor: 8dfb73b987afa9324e4009bdce62a18ce23d983c
328328
React-jsinspector: d15478d0a8ada19864aa4d1cc1c697b41b3fa92f
329-
react-native-viewpager: 88f4269c19d626d2ccd5f7c7f5f7f76422ddb9e3
329+
react-native-viewpager: 66c02e8a18d2d11eef08c84ed9a406b149791751
330330
React-RCTActionSheet: 7369b7c85f99b6299491333affd9f01f5a130c22
331331
React-RCTAnimation: d07be15b2bd1d06d89417eb0343f98ffd2b099a7
332332
React-RCTBlob: 8e0b23d95c9baa98f6b0e127e07666aaafd96c34

ios/ReactNativePageView.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ NS_ASSUME_NONNULL_BEGIN
1212
@property(strong, nonatomic, readonly) UIPageViewController *reactPageViewController;
1313
@property(strong, nonatomic, readonly) UIPageControl *reactPageIndicatorView;
1414
@property(nonatomic, readonly) RCTEventDispatcher *eventDispatcher;
15+
@property(nonatomic) BOOL overdrag;
1516

1617
@property(nonatomic, strong) NSMutableArray<UIViewController *> *childrenViewControllers;
1718
@property(nonatomic) NSInteger initialPage;

ios/ReactNativePageView.m

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ - (instancetype) initWithReactTag:(NSNumber *)reactTag
3232
coalescingKey:(uint16_t)coalescingKey;
3333
{
3434
RCTAssertParam(reactTag);
35-
35+
3636
if ((self = [super init])) {
3737
_viewTag = reactTag;
3838
_position = position;
@@ -99,7 +99,7 @@ - (instancetype) initWithReactTag:(NSNumber *)reactTag
9999
coalescingKey:(uint16_t)coalescingKey;
100100
{
101101
RCTAssertParam(reactTag);
102-
102+
103103
if ((self = [super init])) {
104104
_viewTag = reactTag;
105105
_state = state;
@@ -165,7 +165,7 @@ - (instancetype) initWithReactTag:(NSNumber *)reactTag
165165
coalescingKey:(uint16_t)coalescingKey;
166166
{
167167
RCTAssertParam(reactTag);
168-
168+
169169
if ((self = [super init])) {
170170
_viewTag = reactTag;
171171
_position = position;
@@ -222,6 +222,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher {
222222
_dismissKeyboard = UIScrollViewKeyboardDismissModeNone;
223223
_coalescingKey = 0;
224224
_eventDispatcher = eventDispatcher;
225+
_overdrag = YES;
225226
}
226227
return self;
227228
}
@@ -259,7 +260,7 @@ - (void)addPages {
259260
_childrenViewControllers = tempChildrenViewControllers;
260261
_reactPageIndicatorView.numberOfPages = _childrenViewControllers.count;
261262
[self goTo:[NSNumber numberWithInteger:_currentIndex] animated:NO];
262-
263+
263264
} else {
264265
RCTLog(@"getParentViewController returns nil");
265266
}
@@ -271,36 +272,36 @@ - (void)embed {
271272
dictionaryWithObjectsAndKeys:
272273
[NSNumber numberWithLong:_pageMargin],
273274
UIPageViewControllerOptionInterPageSpacingKey, nil];
274-
275+
275276
UIPageViewController *reactPageViewController =
276277
[[UIPageViewController alloc]
277278
initWithTransitionStyle:_transitionStyle
278279
navigationOrientation:_orientation
279280
options:options];
280-
281+
281282
_reactPageViewController = reactPageViewController;
282283
_reactPageViewController.delegate = self;
283284
_reactPageViewController.dataSource = self;
284-
285+
285286
for (UIView *subview in _reactPageViewController.view.subviews) {
286287
if([subview isKindOfClass:UIScrollView.class]){
287288
((UIScrollView *)subview).delegate = self;
288289
((UIScrollView *)subview).keyboardDismissMode = _dismissKeyboard;
289290
}
290291
}
291-
292+
292293
[self renderChildrenViewControllers];
293294
_reactPageIndicatorView = [self createPageIndicator:self];
294295
_reactPageIndicatorView.hidden = !_showPageIndicator;
295-
296+
296297
[[self reactViewController] addChildViewController:_reactPageViewController];
297298
[reactPageViewController.view addSubview:_reactPageIndicatorView];
298299
[self addSubview:reactPageViewController.view];
299300
_reactPageViewController.view.frame = [self bounds];
300-
301+
301302
[_reactPageViewController didMoveToParentViewController:[self reactViewController]];
302303
[self shouldScroll:_scrollEnabled];
303-
304+
304305
// Add the page view controller's gesture recognizers to the view controller's view so that the gestures are started more easily.
305306
self.gestureRecognizers = _reactPageViewController.gestureRecognizers;
306307
_reactPageIndicatorView.translatesAutoresizingMaskIntoConstraints = NO;
@@ -341,7 +342,7 @@ - (void)renderChildrenViewControllers {
341342
[vc.view removeFromSuperview];
342343
}
343344
[_childrenViewControllers removeAllObjects];
344-
345+
345346
for (UIView *view in [self reactSubviews]) {
346347
[view removeFromSuperview];
347348
UIViewController *pageViewController = [self createChildViewController:view];
@@ -372,7 +373,7 @@ - (void)setReactViewControllers:(NSInteger)index
372373
if (weakSelf.eventDispatcher) {
373374
[weakSelf.eventDispatcher sendEvent:[[RCTOnPageSelected alloc] initWithReactTag:weakSelf.reactTag position:[NSNumber numberWithInteger:index] coalescingKey:coalescingKey]];
374375
}
375-
376+
376377
}];
377378
}
378379

@@ -385,20 +386,20 @@ - (UIViewController *)createChildViewController:(UIView *)view {
385386
- (void)goTo:(NSNumber *)index animated:(BOOL)animated {
386387
if (_currentIndex >= 0 &&
387388
index.integerValue < _childrenViewControllers.count) {
388-
389+
389390
_reactPageIndicatorView.currentPage = index.integerValue;
390391
UIPageViewControllerNavigationDirection direction =
391392
(index.integerValue > _currentIndex)
392393
? UIPageViewControllerNavigationDirectionForward
393394
: UIPageViewControllerNavigationDirectionReverse;
394-
395+
395396
UIViewController *viewController =
396397
[_childrenViewControllers objectAtIndex:index.integerValue];
397398
[self setReactViewControllers:index.integerValue
398399
with:viewController
399400
direction:direction
400401
animated:animated];
401-
402+
402403
}
403404
}
404405

@@ -424,7 +425,7 @@ - (void)pageViewController:(UIPageViewController *)pageViewController
424425
UIViewController* currentVC = pageViewController.viewControllers[0];
425426
_currentIndex = [_childrenViewControllers indexOfObject:currentVC];
426427
[_eventDispatcher sendEvent:[[RCTOnPageSelected alloc] initWithReactTag:self.reactTag position:[NSNumber numberWithInteger:_currentIndex] coalescingKey:_coalescingKey++]];
427-
428+
428429
[_eventDispatcher sendEvent:[[RCTOnPageScrollEvent alloc] initWithReactTag:self.reactTag position:[NSNumber numberWithInteger:_currentIndex] offset:[NSNumber numberWithFloat:0] coalescingKey:_coalescingKey++]];
429430
_reactPageIndicatorView.currentPage = _currentIndex;
430431
}
@@ -435,13 +436,13 @@ - (UIViewController *)pageViewController:
435436
(UIPageViewController *)pageViewController
436437
viewControllerAfterViewController:(UIViewController *)viewController {
437438
NSUInteger index = [_childrenViewControllers indexOfObject:viewController];
438-
439+
439440
if (index == NSNotFound) {
440441
return nil;
441442
}
442-
443+
443444
index++;
444-
445+
445446
if (index == [_childrenViewControllers count]) {
446447
return nil;
447448
}
@@ -454,15 +455,15 @@ - (UIViewController *)pageViewController:
454455
(UIPageViewController *)pageViewController
455456
viewControllerBeforeViewController:(UIViewController *)viewController {
456457
NSUInteger index = [_childrenViewControllers indexOfObject:viewController];
457-
458+
458459
if (index == NSNotFound) {
459460
return nil;
460461
}
461-
462+
462463
if (index == 0) {
463464
return nil;
464465
}
465-
466+
466467
index--;
467468
return [_childrenViewControllers objectAtIndex:index];
468469
}
@@ -504,6 +505,13 @@ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
504505
}
505506

506507
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
508+
if (!_overdrag) {
509+
if (_currentIndex == 0 && scrollView.contentOffset.x <= scrollView.bounds.size.width) {
510+
*targetContentOffset = CGPointMake(scrollView.bounds.size.width, 0);
511+
} else if (_currentIndex == _reactPageIndicatorView.numberOfPages-1 && scrollView.contentOffset.x >= scrollView.bounds.size.width) {
512+
*targetContentOffset = CGPointMake(scrollView.bounds.size.width, 0);
513+
}
514+
}
507515
[_eventDispatcher sendEvent:[[RCTOnPageScrollStateChanged alloc] initWithReactTag:self.reactTag state:@"settling" coalescingKey:_coalescingKey++]];
508516
}
509517

@@ -512,6 +520,14 @@ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
512520
}
513521

514522
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
523+
if (!_overdrag) {
524+
if (_currentIndex == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width) {
525+
scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
526+
} else if (_currentIndex == _reactPageIndicatorView.numberOfPages-1 && scrollView.contentOffset.x > scrollView.bounds.size.width) {
527+
scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
528+
}
529+
}
530+
515531
CGPoint point = scrollView.contentOffset;
516532
float offset = (point.x - self.frame.size.width)/self.frame.size.width;
517533
if(fabs(offset) > 1) {

ios/ReactViewPagerManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ @implementation ReactViewPagerManager
1212

1313
RCT_EXPORT_VIEW_PROPERTY(transitionStyle, UIPageViewControllerTransitionStyle)
1414
RCT_EXPORT_VIEW_PROPERTY(orientation, UIPageViewControllerNavigationOrientation)
15+
RCT_EXPORT_VIEW_PROPERTY(overdrag, BOOL)
1516
RCT_EXPORT_VIEW_PROPERTY(onPageSelected, RCTDirectEventBlock)
1617
RCT_EXPORT_VIEW_PROPERTY(onPageScroll, RCTDirectEventBlock)
1718
RCT_EXPORT_VIEW_PROPERTY(onPageScrollStateChanged, RCTDirectEventBlock)

js/types.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,9 @@ export type ViewPagerProps = $ReadOnly<{|
101101
orientation?: Orientation,
102102
transitionStyle?: TransitionStyle,
103103
showPageIndicator?: boolean,
104+
/**
105+
* Determines whether it's possible to overscroll a bit
106+
* after reaching end or very beginning of pages.
107+
*/
108+
overdrag?: boolean,
104109
|}>;

typings/index.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,18 @@ export interface ViewPagerProps extends ReactNative.ViewProps {
6464
* edge-to-edge.
6565
*/
6666
pageMargin?: number;
67-
67+
6868
/**
6969
* iOS only
7070
*/
7171
orientation?: 'horizontal' | 'vertical',
7272
transitionStyle?: 'scroll' | 'curl',
7373
showPageIndicator?: boolean,
74+
/**
75+
* Determines whether it's possible to overscroll a bit
76+
* after reaching end or very beginning of pages.
77+
*/
78+
overdrag?: boolean,
7479
}
7580

7681
declare class ViewPagerComponent extends React.Component<ViewPagerProps> {}

0 commit comments

Comments
 (0)