From 83964f103c464b79d564ce688eb58ede9e4d76a7 Mon Sep 17 00:00:00 2001 From: kazhoang <59641890+kazhoang@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:30:51 +0700 Subject: [PATCH 1/4] Fix: Navigation gesture gestureResponseDistance failure --- ios/Fabric/RNCPagerViewComponentView.h | 1 + ios/Fabric/RNCPagerViewComponentView.mm | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/ios/Fabric/RNCPagerViewComponentView.h b/ios/Fabric/RNCPagerViewComponentView.h index 3d6b9a64..c9444e8a 100644 --- a/ios/Fabric/RNCPagerViewComponentView.h +++ b/ios/Fabric/RNCPagerViewComponentView.h @@ -17,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic) NSInteger destinationIndex; @property(nonatomic) NSString* layoutDirection; @property(nonatomic) BOOL overdrag; +@property(nonatomic) BOOL isDragging; - (void)setPage:(NSInteger)number; - (void)setPageWithoutAnimation:(NSInteger)number; diff --git a/ios/Fabric/RNCPagerViewComponentView.mm b/ios/Fabric/RNCPagerViewComponentView.mm index 067f92ee..0523c107 100644 --- a/ios/Fabric/RNCPagerViewComponentView.mm +++ b/ios/Fabric/RNCPagerViewComponentView.mm @@ -71,6 +71,7 @@ - (instancetype)initWithFrame:(CGRect)frame _destinationIndex = -1; _layoutDirection = @"ltr"; _overdrag = NO; + _isDragging = NO; } return self; @@ -83,6 +84,27 @@ - (void)willMoveToSuperview:(UIView *)newSuperview { } } +-(void) didMoveToWindow { + UIPanGestureRecognizer* gesture = [UIPanGestureRecognizer new]; + gesture.delegate = self; + [self addGestureRecognizer: gesture]; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + if (otherGestureRecognizer == self->scrollView.panGestureRecognizer) { + UIPanGestureRecognizer* p = (UIPanGestureRecognizer*) gestureRecognizer; + CGPoint velocity = [p velocityInView:self]; + if (!_isDragging && self.currentIndex == 0 && velocity.x > 0) { + self->scrollView.panGestureRecognizer.enabled = false; + return NO; + } else { + self->scrollView.panGestureRecognizer.enabled = self->scrollView.scrollEnabled; + } + } else { + self->scrollView.panGestureRecognizer.enabled = self->scrollView.scrollEnabled; + } + return YES; +} #pragma mark - React API @@ -270,6 +292,7 @@ - (UIViewController *)currentlyDisplayed { #pragma mark - UIScrollViewDelegate - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + _isDragging = YES; const auto strongEventEmitter = *std::dynamic_pointer_cast(_eventEmitter); strongEventEmitter.onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Dragging }); } @@ -298,6 +321,7 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoi } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + _isDragging = NO; const auto strongEventEmitter = *std::dynamic_pointer_cast(_eventEmitter); strongEventEmitter.onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Idle }); } From ee3acd541fb4a2dddaca8b88a940322fc60e03ec Mon Sep 17 00:00:00 2001 From: kazhoang <59641890+kazhoang@users.noreply.github.com> Date: Fri, 1 Aug 2025 11:47:29 +0700 Subject: [PATCH 2/4] feat: add allowNavFullscreenGesture prop to enable swipe-back gestures in PagerView --- README.md | 1 + .../PagerViewViewManager.kt | 5 ++ ios/RNCPagerViewComponentView.mm | 48 ++++++++++++++++++- src/PagerViewNativeComponent.ts | 1 + 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e15bbf7..4dc2c21d 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ For advanced usage please take a look into our [example project](https://github. | `offscreenPageLimit: number` | Set the number of pages that should be retained to either side of the currently visible page(s). Pages beyond this limit will be recreated from the adapter when needed. Defaults to RecyclerView's caching strategy. The given value must either be larger than 0. | Android | | `overdrag: boolean` | Allows for overscrolling after reaching the end or very beginning or pages. Defaults to `false` | iOS | | `layoutDirection: ('ltr' / 'rtl' / 'locale')` | Specifies layout direction. Use `ltr` or `rtl` to set explicitly or `locale` to deduce from the default language script of a locale. Defaults to `locale` | both | +| `allowNavFullscreenGesture: boolean` | Allows navigation swipe-back gestures to pass through when PagerView is at index 0. If `overdrag` is set to `true`, it will have higher priority. Defaults to `false` | iOS | | Method | Description | Platform | | ------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------: | diff --git a/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt b/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt index 8ec286a7..7f90b526 100644 --- a/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt +++ b/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt @@ -163,6 +163,11 @@ class PagerViewViewManager : ViewGroupManager(), RNCViewPa } } + @ReactProp(name = "allowNavFullscreenGesture") + override fun setAllowNavFullscreenGesture(view: NestedScrollableHost?, value: Boolean) { + return + } + @ReactProp(name = "overdrag") override fun setOverdrag(view: NestedScrollableHost?, value: Boolean) { return diff --git a/ios/RNCPagerViewComponentView.mm b/ios/RNCPagerViewComponentView.mm index c6a150b0..caadd5b3 100644 --- a/ios/RNCPagerViewComponentView.mm +++ b/ios/RNCPagerViewComponentView.mm @@ -12,7 +12,7 @@ using namespace facebook::react; -@interface RNCPagerViewComponentView () +@interface RNCPagerViewComponentView () @property(nonatomic, strong) UIPageViewController *nativePageViewController; @property(nonatomic, strong) NSMutableArray *nativeChildrenViewControllers; @@ -28,6 +28,9 @@ @implementation RNCPagerViewComponentView { NSInteger _destinationIndex; BOOL _overdrag; NSString *_layoutDirection; + BOOL _isDragging; + BOOL _allowNavFullscreenGesture; + UIPanGestureRecognizer *_navGestureRecognizer; } // Needed because of this: https://github.com/facebook/react-native/pull/37274 @@ -76,6 +79,9 @@ - (instancetype)initWithFrame:(CGRect)frame _destinationIndex = -1; _layoutDirection = @"ltr"; _overdrag = NO; + _isDragging = NO; + _allowNavFullscreenGesture = NO; + _navGestureRecognizer = nil; } return self; @@ -88,6 +94,23 @@ - (void)willMoveToSuperview:(UIView *)newSuperview { } } +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + if (otherGestureRecognizer == self->scrollView.panGestureRecognizer) { + UIPanGestureRecognizer* p = (UIPanGestureRecognizer*) gestureRecognizer; + CGPoint velocity = [p velocityInView:self]; + BOOL shouldPagerGestureFail = !_overdrag && !_isDragging && _currentIndex == 0 && velocity.x > 0; + if (shouldPagerGestureFail) { + self->scrollView.panGestureRecognizer.enabled = false; + return NO; + } else { + self->scrollView.panGestureRecognizer.enabled = self->scrollView.scrollEnabled; + } + } else { + self->scrollView.panGestureRecognizer.enabled = self->scrollView.scrollEnabled; + } + + return YES; +} #pragma mark - React API @@ -126,6 +149,11 @@ -(void)prepareForRecycle { [super prepareForRecycle]; _nativePageViewController = nil; _currentIndex = -1; + _allowNavFullscreenGesture = NO; + if (_navGestureRecognizer) { + [self removeGestureRecognizer:_navGestureRecognizer]; + _navGestureRecognizer = nil; + } } - (void)shouldDismissKeyboard:(RNCViewPagerKeyboardDismissMode)dismissKeyboard { @@ -173,6 +201,22 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props oldProps:(cons _overdrag = newScreenProps.overdrag; } + if (newScreenProps.allowNavFullscreenGesture != _allowNavFullscreenGesture) { + _allowNavFullscreenGesture = newScreenProps.allowNavFullscreenGesture; + + if (_allowNavFullscreenGesture) { + _navGestureRecognizer = [[UIPanGestureRecognizer alloc] init]; + _navGestureRecognizer.delegate = self; + [self addGestureRecognizer:_navGestureRecognizer]; + _isDragging = NO; + } else { + if (_navGestureRecognizer) { + [self removeGestureRecognizer:_navGestureRecognizer]; + _navGestureRecognizer = nil; + } + } + } + [super updateProps:props oldProps:oldProps]; } @@ -264,6 +308,7 @@ - (UIViewController *)currentlyDisplayed { #pragma mark - UIScrollViewDelegate - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + _isDragging = YES; const auto eventEmitter = [self pagerEventEmitter]; eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Dragging }); } @@ -290,6 +335,7 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoi } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + _isDragging = NO; const auto eventEmitter = [self pagerEventEmitter]; eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Idle }); } diff --git a/src/PagerViewNativeComponent.ts b/src/PagerViewNativeComponent.ts index 19d935ec..40049763 100644 --- a/src/PagerViewNativeComponent.ts +++ b/src/PagerViewNativeComponent.ts @@ -33,6 +33,7 @@ export interface NativeProps extends ViewProps { overScrollMode?: WithDefault<'auto' | 'always' | 'never', 'auto'>; overdrag?: WithDefault; keyboardDismissMode?: WithDefault<'none' | 'on-drag', 'none'>; + allowNavFullscreenGesture?: WithDefault; onPageScroll?: DirectEventHandler; onPageSelected?: DirectEventHandler; onPageScrollStateChanged?: DirectEventHandler; From 8f064863553de0b4c1dbba49e5b3a19e475297a4 Mon Sep 17 00:00:00 2001 From: KAz Date: Sun, 3 Aug 2025 07:47:56 +0700 Subject: [PATCH 3/4] fix: guard clause if allowNavFullscreenGesture property is enabled --- ios/RNCPagerViewComponentView.mm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ios/RNCPagerViewComponentView.mm b/ios/RNCPagerViewComponentView.mm index caadd5b3..a4c975ee 100644 --- a/ios/RNCPagerViewComponentView.mm +++ b/ios/RNCPagerViewComponentView.mm @@ -95,6 +95,10 @@ - (void)willMoveToSuperview:(UIView *)newSuperview { } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + if (!_allowNavFullscreenGesture) { + return NO; + } + if (otherGestureRecognizer == self->scrollView.panGestureRecognizer) { UIPanGestureRecognizer* p = (UIPanGestureRecognizer*) gestureRecognizer; CGPoint velocity = [p velocityInView:self]; From 0094584faba2a592c01abb24426c59447e6c344b Mon Sep 17 00:00:00 2001 From: KAz Date: Fri, 8 Aug 2025 12:06:20 +0700 Subject: [PATCH 4/4] refactor: rename allowNavFullscreenGesture to allowNavigationBackGesture for clarity and consistency and remove unnecessary properties --- README.md | 7 +-- .../PagerViewViewManager.kt | 4 +- ios/RNCPagerViewComponentView.mm | 45 +++++++++---------- src/PagerViewNativeComponent.ts | 2 +- 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 4dc2c21d..a9b42f67 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ For advanced usage please take a look into our [example project](https://github. | `offscreenPageLimit: number` | Set the number of pages that should be retained to either side of the currently visible page(s). Pages beyond this limit will be recreated from the adapter when needed. Defaults to RecyclerView's caching strategy. The given value must either be larger than 0. | Android | | `overdrag: boolean` | Allows for overscrolling after reaching the end or very beginning or pages. Defaults to `false` | iOS | | `layoutDirection: ('ltr' / 'rtl' / 'locale')` | Specifies layout direction. Use `ltr` or `rtl` to set explicitly or `locale` to deduce from the default language script of a locale. Defaults to `locale` | both | -| `allowNavFullscreenGesture: boolean` | Allows navigation swipe-back gestures to pass through when PagerView is at index 0. If `overdrag` is set to `true`, it will have higher priority. Defaults to `false` | iOS | +| `allowNavigationBackGesture: boolean` | Allows navigation swipe-back gestures to pass through when PagerView is at index 0. If `overdrag` is set to `true`, it will have higher priority. Defaults to `false` | iOS | | Method | Description | Platform | | ------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------: | @@ -243,6 +243,7 @@ const pageScrollHandler = usePageScrollHandler({ ``` ## usePagerView Hook Usage + The `usePagerView` hook is a convenient way to manage the state and control the behavior of the `` component. It provides functions and variables to interact with the pager, such as navigating between pages and enabling/disabling scrolling. Below is an example of how to use the usePager hook: @@ -295,7 +296,8 @@ export function PagerHookExample() { ); } ``` -### How the Example Works: + +### How the Example Works - **Pager View Setup**: The `AnimatedPagerView` component wraps `PagerView` in React Native's animation capabilities. It accepts multiple props from the `usePager` hook, such as `overdragEnabled`, `scrollEnabled`, `onPageScroll`, `onPageSelected`, and others to manage pager behavior. @@ -305,7 +307,6 @@ export function PagerHookExample() { The `usePager` hook makes it easy to handle pagination with dynamic views. This example demonstrates how to set up a simple paginated interface where users can scroll through pages, interact with page elements, and control the pager with external navigation. - ## License MIT diff --git a/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt b/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt index 7f90b526..76b540e0 100644 --- a/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt +++ b/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt @@ -163,8 +163,8 @@ class PagerViewViewManager : ViewGroupManager(), RNCViewPa } } - @ReactProp(name = "allowNavFullscreenGesture") - override fun setAllowNavFullscreenGesture(view: NestedScrollableHost?, value: Boolean) { + @ReactProp(name = "allowNavigationBackGesture") + override fun setAllowNavigationBackGesture(view: NestedScrollableHost?, value: Boolean) { return } diff --git a/ios/RNCPagerViewComponentView.mm b/ios/RNCPagerViewComponentView.mm index a4c975ee..047167ea 100644 --- a/ios/RNCPagerViewComponentView.mm +++ b/ios/RNCPagerViewComponentView.mm @@ -28,8 +28,7 @@ @implementation RNCPagerViewComponentView { NSInteger _destinationIndex; BOOL _overdrag; NSString *_layoutDirection; - BOOL _isDragging; - BOOL _allowNavFullscreenGesture; + BOOL _allowNavigationBackGesture; UIPanGestureRecognizer *_navGestureRecognizer; } @@ -79,8 +78,7 @@ - (instancetype)initWithFrame:(CGRect)frame _destinationIndex = -1; _layoutDirection = @"ltr"; _overdrag = NO; - _isDragging = NO; - _allowNavFullscreenGesture = NO; + _allowNavigationBackGesture = NO; _navGestureRecognizer = nil; } @@ -95,16 +93,16 @@ - (void)willMoveToSuperview:(UIView *)newSuperview { } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { - if (!_allowNavFullscreenGesture) { + if (!_allowNavigationBackGesture) { return NO; } if (otherGestureRecognizer == self->scrollView.panGestureRecognizer) { - UIPanGestureRecognizer* p = (UIPanGestureRecognizer*) gestureRecognizer; - CGPoint velocity = [p velocityInView:self]; - BOOL shouldPagerGestureFail = !_overdrag && !_isDragging && _currentIndex == 0 && velocity.x > 0; + UIPanGestureRecognizer* panGestureRecognizer = (UIPanGestureRecognizer*) gestureRecognizer; + CGPoint velocity = [panGestureRecognizer velocityInView:self]; + BOOL shouldPagerGestureFail = !_overdrag && !scrollView.isDecelerating && _currentIndex == 0 && velocity.x > 0; if (shouldPagerGestureFail) { - self->scrollView.panGestureRecognizer.enabled = false; + self->scrollView.panGestureRecognizer.enabled = NO; return NO; } else { self->scrollView.panGestureRecognizer.enabled = self->scrollView.scrollEnabled; @@ -148,16 +146,12 @@ -(void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics]; } - -(void)prepareForRecycle { [super prepareForRecycle]; _nativePageViewController = nil; _currentIndex = -1; - _allowNavFullscreenGesture = NO; - if (_navGestureRecognizer) { - [self removeGestureRecognizer:_navGestureRecognizer]; - _navGestureRecognizer = nil; - } + _allowNavigationBackGesture = NO; + [self cleanupGestureRecognizer]; } - (void)shouldDismissKeyboard:(RNCViewPagerKeyboardDismissMode)dismissKeyboard { @@ -205,19 +199,15 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props oldProps:(cons _overdrag = newScreenProps.overdrag; } - if (newScreenProps.allowNavFullscreenGesture != _allowNavFullscreenGesture) { - _allowNavFullscreenGesture = newScreenProps.allowNavFullscreenGesture; + if (newScreenProps.allowNavigationBackGesture != _allowNavigationBackGesture) { + _allowNavigationBackGesture = newScreenProps.allowNavigationBackGesture; - if (_allowNavFullscreenGesture) { + if (_allowNavigationBackGesture) { _navGestureRecognizer = [[UIPanGestureRecognizer alloc] init]; _navGestureRecognizer.delegate = self; [self addGestureRecognizer:_navGestureRecognizer]; - _isDragging = NO; } else { - if (_navGestureRecognizer) { - [self removeGestureRecognizer:_navGestureRecognizer]; - _navGestureRecognizer = nil; - } + [self cleanupGestureRecognizer]; } } @@ -312,7 +302,6 @@ - (UIViewController *)currentlyDisplayed { #pragma mark - UIScrollViewDelegate - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { - _isDragging = YES; const auto eventEmitter = [self pagerEventEmitter]; eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Dragging }); } @@ -339,7 +328,6 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoi } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { - _isDragging = NO; const auto eventEmitter = [self pagerEventEmitter]; eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Idle }); } @@ -457,6 +445,13 @@ - (BOOL)isLtrLayout { return [_layoutDirection isEqualToString: @"ltr"]; } +- (void)cleanupGestureRecognizer { + if (_navGestureRecognizer) { + [self removeGestureRecognizer:_navGestureRecognizer]; + _navGestureRecognizer = nil; + } +} + - (std::shared_ptr)pagerEventEmitter { if (!_eventEmitter) { diff --git a/src/PagerViewNativeComponent.ts b/src/PagerViewNativeComponent.ts index 40049763..57c9186b 100644 --- a/src/PagerViewNativeComponent.ts +++ b/src/PagerViewNativeComponent.ts @@ -33,7 +33,7 @@ export interface NativeProps extends ViewProps { overScrollMode?: WithDefault<'auto' | 'always' | 'never', 'auto'>; overdrag?: WithDefault; keyboardDismissMode?: WithDefault<'none' | 'on-drag', 'none'>; - allowNavFullscreenGesture?: WithDefault; + allowNavigationBackGesture?: WithDefault; onPageScroll?: DirectEventHandler; onPageSelected?: DirectEventHandler; onPageScrollStateChanged?: DirectEventHandler;