From 69ab0531ddc237a8fd30fdc4981778d7e19d3974 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Fri, 13 May 2022 13:52:39 +0200 Subject: [PATCH] chore: add `preventNativeDismiss` prop for Fabric (7) (#1444) * chore: move preventNativeDismiss prop to common section in RNSScreen interface * fix: merge artifact * chore: add preventNativeDismiss prop to codegen * chore: add preventNativeDismiss prop update * fix: add noop for Android * chore: unify business logic between archs (8) (#1446) * chore: unify RNSScreen#vieDidLayoutSubviews * chore: make RNSScreenStack#hitTest:withEvent: method shared https://github.com/software-mansion/react-native-screens/pull/1416#discussion_r856175275 * chore: make RNSScreenStackView#isScrollViewPanGestureRecongnizer & some helper methods shared https://github.com/software-mansion/react-native-screens/pull/1416#discussion_r856301097 * chore: move backButtonInCustomView to shared sectionin RNSScreenStackHeaderConfig.h * chore: move backButtonInCustomView prop to implemented section in JS * chore: update `backButtonInCustomView` for Fabric * chore: unify RNSScreenStackHeaderConfigView#willShowViewController:animated:withConfig * refact: linter suggestion * fix: RNSScreen viewDidLayoutSubviews * chore: copy interfaces to android/src/paper directory * fix: commit address in comment It changed after I rebased --- .../swmansion/rnscreens/ScreenViewManager.kt | 2 + .../RNSScreenManagerDelegate.java | 22 ++++- .../RNSScreenManagerInterface.java | 8 +- ...creenStackHeaderConfigManagerDelegate.java | 6 +- ...reenStackHeaderConfigManagerInterface.java | 2 +- ios/RNSScreen.h | 3 +- ios/RNSScreen.mm | 28 ++++--- ios/RNSScreenStack.mm | 84 +++++++++---------- ios/RNSScreenStackHeaderConfig.h | 2 +- ios/RNSScreenStackHeaderConfig.mm | 7 +- src/fabric/ScreenNativeComponent.js | 1 + .../ScreenStackHeaderConfigNativeComponent.js | 2 +- 12 files changed, 97 insertions(+), 70 deletions(-) diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt index 83708322ad..669b5d08be 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt @@ -153,6 +153,8 @@ class ScreenViewManager : ViewGroupManager(), RNSScreenManagerInterface< override fun setHomeIndicatorHidden(view: Screen?, value: Boolean) = Unit + override fun setPreventNativeDismiss(view: Screen?, value: Boolean) = Unit + override fun getExportedCustomDirectEventTypeConstants(): MutableMap { val map: MutableMap = MapBuilder.of( ScreenDismissedEvent.EVENT_NAME, diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java index abb26946c7..adeabdd83e 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java @@ -12,6 +12,7 @@ import android.view.View; import androidx.annotation.Nullable; import com.facebook.react.bridge.ColorPropConverter; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.BaseViewManagerDelegate; import com.facebook.react.uimanager.BaseViewManagerInterface; @@ -22,9 +23,18 @@ public RNSScreenManagerDelegate(U viewManager) { @Override public void setProperty(T view, String propName, @Nullable Object value) { switch (propName) { + case "customAnimationOnSwipe": + mViewManager.setCustomAnimationOnSwipe(view, value == null ? false : (boolean) value); + break; case "fullScreenSwipeEnabled": mViewManager.setFullScreenSwipeEnabled(view, value == null ? false : (boolean) value); break; + case "homeIndicatorHidden": + mViewManager.setHomeIndicatorHidden(view, value == null ? false : (boolean) value); + break; + case "preventNativeDismiss": + mViewManager.setPreventNativeDismiss(view, value == null ? false : (boolean) value); + break; case "gestureEnabled": mViewManager.setGestureEnabled(view, value == null ? true : (boolean) value); break; @@ -46,6 +56,9 @@ public void setProperty(T view, String propName, @Nullable Object value) { case "statusBarTranslucent": mViewManager.setStatusBarTranslucent(view, value == null ? false : (boolean) value); break; + case "gestureResponseDistance": + mViewManager.setGestureResponseDistance(view, (ReadableMap) value); + break; case "stackPresentation": mViewManager.setStackPresentation(view, (String) value); break; @@ -58,6 +71,12 @@ public void setProperty(T view, String propName, @Nullable Object value) { case "replaceAnimation": mViewManager.setReplaceAnimation(view, (String) value); break; + case "hideKeyboardOnSwipe": + mViewManager.setHideKeyboardOnSwipe(view, value == null ? false : (boolean) value); + break; + case "activityState": + mViewManager.setActivityState(view, value == null ? -1 : ((Double) value).intValue()); + break; case "navigationBarColor": mViewManager.setNavigationBarColor(view, ColorPropConverter.getColor(value, view.getContext())); break; @@ -67,9 +86,6 @@ public void setProperty(T view, String propName, @Nullable Object value) { case "nativeBackButtonDismissalEnabled": mViewManager.setNativeBackButtonDismissalEnabled(view, value == null ? false : (boolean) value); break; - case "activityState": - mViewManager.setActivityState(view, value == null ? -1 : ((Double) value).intValue()); - break; default: super.setProperty(view, propName, value); } diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java index a6cd78d902..a40bea6a14 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java @@ -11,9 +11,13 @@ import android.view.View; import androidx.annotation.Nullable; +import com.facebook.react.bridge.ReadableMap; public interface RNSScreenManagerInterface { + void setCustomAnimationOnSwipe(T view, boolean value); void setFullScreenSwipeEnabled(T view, boolean value); + void setHomeIndicatorHidden(T view, boolean value); + void setPreventNativeDismiss(T view, boolean value); void setGestureEnabled(T view, boolean value); void setStatusBarColor(T view, @Nullable Integer value); void setStatusBarHidden(T view, boolean value); @@ -21,12 +25,14 @@ public interface RNSScreenManagerInterface { void setStatusBarAnimation(T view, @Nullable String value); void setStatusBarStyle(T view, @Nullable String value); void setStatusBarTranslucent(T view, boolean value); + void setGestureResponseDistance(T view, @Nullable ReadableMap value); void setStackPresentation(T view, @Nullable String value); void setStackAnimation(T view, @Nullable String value); void setTransitionDuration(T view, int value); void setReplaceAnimation(T view, @Nullable String value); + void setHideKeyboardOnSwipe(T view, boolean value); + void setActivityState(T view, int value); void setNavigationBarColor(T view, @Nullable Integer value); void setNavigationBarHidden(T view, boolean value); void setNativeBackButtonDismissalEnabled(T view, boolean value); - void setActivityState(T view, int value); } diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenStackHeaderConfigManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenStackHeaderConfigManagerDelegate.java index d5cfbc80af..e02d6882d3 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenStackHeaderConfigManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenStackHeaderConfigManagerDelegate.java @@ -91,12 +91,12 @@ public void setProperty(T view, String propName, @Nullable Object value) { case "hideBackButton": mViewManager.setHideBackButton(view, value == null ? false : (boolean) value); break; - case "topInsetEnabled": - mViewManager.setTopInsetEnabled(view, value == null ? false : (boolean) value); - break; case "backButtonInCustomView": mViewManager.setBackButtonInCustomView(view, value == null ? false : (boolean) value); break; + case "topInsetEnabled": + mViewManager.setTopInsetEnabled(view, value == null ? false : (boolean) value); + break; default: super.setProperty(view, propName, value); } diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenStackHeaderConfigManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenStackHeaderConfigManagerInterface.java index 8e8db08451..2d31a40108 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenStackHeaderConfigManagerInterface.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenStackHeaderConfigManagerInterface.java @@ -36,6 +36,6 @@ public interface RNSScreenStackHeaderConfigManagerInterface { void setTitleColor(T view, @Nullable Integer value); void setDisableBackButtonMenu(T view, boolean value); void setHideBackButton(T view, boolean value); - void setTopInsetEnabled(T view, boolean value); void setBackButtonInCustomView(T view, boolean value); + void setTopInsetEnabled(T view, boolean value); } diff --git a/ios/RNSScreen.h b/ios/RNSScreen.h index 8a1303f0c2..211f4a144a 100644 --- a/ios/RNSScreen.h +++ b/ios/RNSScreen.h @@ -59,6 +59,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) BOOL dismissed; @property (nonatomic) BOOL hideKeyboardOnSwipe; @property (nonatomic) BOOL customAnimationOnSwipe; +@property (nonatomic) BOOL preventNativeDismiss; @property (nonatomic, retain) RNSScreen *controller; @property (nonatomic, copy) NSDictionary *gestureResponseDistance; @property (nonatomic) int activityState; @@ -82,8 +83,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) RCTDirectEventBlock onWillDisappear; @property (nonatomic, copy) RCTDirectEventBlock onNativeDismissCancelled; @property (nonatomic, copy) RCTDirectEventBlock onTransitionProgress; - -@property (nonatomic) BOOL preventNativeDismiss; #endif - (void)notifyFinishTransitioning; diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index 8a8185d831..d62eb5a6c0 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -16,11 +16,11 @@ #import "RNSConvert.h" #else #import -#import "RNSScreenStack.h" #endif #import #import +#import "RNSScreenStack.h" #import "RNSScreenStackHeaderConfig.h" @interface RNSScreenView () @@ -467,6 +467,8 @@ - (void)updateProps:(facebook::react::Props::Shared const &)props setGestureResponseDistance:[RNSConvert gestureResponseDistanceDictFromCppStruct:newScreenProps.gestureResponseDistance]]; + [self setPreventNativeDismiss:newScreenProps.preventNativeDismiss]; + [self setActivityStateOrNil:[NSNumber numberWithInt:newScreenProps.activityState]]; #if !TARGET_OS_TV @@ -609,10 +611,10 @@ - (void)invalidate @implementation RNSScreen { __weak id _previousFirstResponder; + CGRect _lastViewFrame; #ifdef RN_FABRIC_ENABLED RNSScreenView *_initialView; #else - CGRect _lastViewFrame; UIView *_fakeView; CADisplayLink *_animationTimer; CGFloat _currentAlpha; @@ -770,12 +772,7 @@ - (void)viewDidDisappear:(BOOL)animated - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; -#ifdef RN_FABRIC_ENABLED - BOOL isDisplayedWithinUINavController = [self.parentViewController isKindOfClass:[UINavigationController class]]; - if (isDisplayedWithinUINavController) { - [_initialView updateBounds]; - } -#else + // The below code makes the screen view adapt dimensions provided by the system. We take these // into account only when the view is mounted under RNScreensNavigationController in which case system // provides additional padding to account for possible header, and in the case when screen is @@ -784,12 +781,17 @@ - (void)viewDidLayoutSubviews BOOL isDisplayedWithinUINavController = [self.parentViewController isKindOfClass:[RNScreensNavigationController class]]; BOOL isPresentedAsNativeModal = self.parentViewController == nil && self.presentingViewController != nil; - if ((isDisplayedWithinUINavController || isPresentedAsNativeModal) && - !CGRectEqualToRect(_lastViewFrame, self.view.frame)) { - _lastViewFrame = self.view.frame; - [((RNSScreenView *)self.viewIfLoaded) updateBounds]; - } + + if (isDisplayedWithinUINavController || isPresentedAsNativeModal) { +#ifdef RN_FABRIC_ENABLED + [_initialView updateBounds]; +#else + if (!CGRectEqualToRect(_lastViewFrame, self.view.frame)) { + _lastViewFrame = self.view.frame; + [((RNSScreenView *)self.viewIfLoaded) updateBounds]; + } #endif + } } - (void)notifyFinishTransitioning diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm index c9690f229f..39a61fcc01 100644 --- a/ios/RNSScreenStack.mm +++ b/ios/RNSScreenStack.mm @@ -752,7 +752,7 @@ - (BOOL)isInGestureResponseDistance:(UIGestureRecognizer *)gestureRecognizer top x = _controller.view.frame.size.width - x; } - // see: https://github.com/software-mansion/react-native-screens/pull/1442/commits/c0413bddadc1023022d3d390373321632ad3539d + // see: https://github.com/software-mansion/react-native-screens/pull/1442/commits/74d4bae321875d8305ad021b3d448ebf713e7d56 // this prop is always default initialized so we do not expect any nils float start = [gestureResponseDistanceValues[@"start"] floatValue]; float end = [gestureResponseDistanceValues[@"end"] floatValue]; @@ -767,6 +767,47 @@ - (BOOL)isInGestureResponseDistance:(UIGestureRecognizer *)gestureRecognizer top (bottom != -1 && y > bottom)); } +// By default, the header buttons that are not inside the native hit area +// cannot be clicked, so we check it by ourselves +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + if (CGRectContainsPoint(_controller.navigationBar.frame, point)) { + // headerConfig should be the first subview of the topmost screen + UIView *headerConfig = [[_reactSubviews.lastObject reactSubviews] firstObject]; + if ([headerConfig isKindOfClass:[RNSScreenStackHeaderConfig class]]) { + UIView *headerHitTestResult = [headerConfig hitTest:point withEvent:event]; + if (headerHitTestResult != nil) { + return headerHitTestResult; + } + } + } + return [super hitTest:point withEvent:event]; +} + +- (BOOL)isScrollViewPanGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer +{ + // NOTE: This hack is required to restore native behavior of edge swipe (interactive pop gesture) + // without this, on a screen with a scroll view, it's only possible to pop view by panning horizontally + // if even slightly diagonal (or if in motion), scroll view will scroll, and edge swipe will be cancelled + if (![[gestureRecognizer view] isKindOfClass:[UIScrollView class]]) { + return NO; + } + UIScrollView *scrollView = (UIScrollView *)gestureRecognizer.view; + return scrollView.panGestureRecognizer == gestureRecognizer; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer + shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer +{ + return [self isScrollViewPanGestureRecognizer:otherGestureRecognizer]; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer + shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer +{ + return [self isScrollViewPanGestureRecognizer:otherGestureRecognizer]; +} + #ifdef RN_FABRIC_ENABLED #pragma mark - Fabric specific @@ -892,23 +933,6 @@ - (void)didUpdateReactSubviews }); } -// By default, the header buttons that are not inside the native hit area -// cannot be clicked, so we check it by ourselves -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event -{ - if (CGRectContainsPoint(_controller.navigationBar.frame, point)) { - // headerConfig should be the first subview of the topmost screen - UIView *headerConfig = [[_reactSubviews.lastObject reactSubviews] firstObject]; - if ([headerConfig isKindOfClass:[RNSScreenStackHeaderConfig class]]) { - UIView *headerHitTestResult = [headerConfig hitTest:point withEvent:event]; - if (headerHitTestResult != nil) { - return headerHitTestResult; - } - } - } - return [super hitTest:point withEvent:event]; -} - - (void)invalidate { _invalidated = YES; @@ -920,30 +944,6 @@ - (void)invalidate [_controller removeFromParentViewController]; } -- (BOOL)isScrollViewPanGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer -{ - // NOTE: This hack is required to restore native behavior of edge swipe (interactive pop gesture) - // without this, on a screen with a scroll view, it's only possible to pop view by panning horizontally - // if even slightly diagonal (or if in motion), scroll view will scroll, and edge swipe will be cancelled - if (![[gestureRecognizer view] isKindOfClass:[UIScrollView class]]) { - return NO; - } - UIScrollView *scrollView = gestureRecognizer.view; - return scrollView.panGestureRecognizer == gestureRecognizer; -} - -- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer - shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer -{ - return [self isScrollViewPanGestureRecognizer:otherGestureRecognizer]; -} - -- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer - shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer -{ - return [self isScrollViewPanGestureRecognizer:otherGestureRecognizer]; -} - - (id)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController: (id)animationController diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.h index 719e9c8a6b..41cdac5f5c 100644 --- a/ios/RNSScreenStackHeaderConfig.h +++ b/ios/RNSScreenStackHeaderConfig.h @@ -24,7 +24,6 @@ #else @property (nonatomic) UIBlurEffectStyle blurEffect; @property (nonatomic) BOOL hide; -@property (nonatomic) BOOL backButtonInCustomView; #endif @property (nonatomic, retain) NSString *title; @@ -48,6 +47,7 @@ @property (nonatomic) BOOL disableBackButtonMenu; @property (nonatomic) BOOL hideShadow; @property (nonatomic) BOOL translucent; +@property (nonatomic) BOOL backButtonInCustomView; @property (nonatomic) UISemanticContentAttribute direction; + (void)willShowViewController:(UIViewController *)vc diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index 218ec2014e..1cb36b52e2 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -543,12 +543,9 @@ + (void)updateViewController:(UIViewController *)vc for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) { switch (subview.type) { case RNSScreenStackHeaderSubviewTypeLeft: { -#ifdef RN_FABRIC_ENABLED -#else #if !TARGET_OS_TV navitem.leftItemsSupplementBackButton = config.backButtonInCustomView; #endif -#endif // RN_FABRIC_ENABLED UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithCustomView:subview]; navitem.leftBarButtonItem = buttonItem; break; @@ -706,6 +703,10 @@ - (void)updateProps:(facebook::react::Props::Shared const &)props _translucent = newScreenProps.translucent; needsNavigationControllerLayout = YES; } + + if (newScreenProps.backButtonInCustomView != _backButtonInCustomView) { + [self setBackButtonInCustomView:newScreenProps.backButtonInCustomView]; + } _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { diff --git a/src/fabric/ScreenNativeComponent.js b/src/fabric/ScreenNativeComponent.js index abda0ab79b..6db9c95bd2 100644 --- a/src/fabric/ScreenNativeComponent.js +++ b/src/fabric/ScreenNativeComponent.js @@ -58,6 +58,7 @@ export type NativeProps = $ReadOnly<{| customAnimationOnSwipe?: boolean, fullScreenSwipeEnabled?: boolean, homeIndicatorHidden?: boolean, + preventNativeDismiss?: boolean, gestureEnabled?: WithDefault, statusBarColor?: ColorValue, statusBarHidden?: boolean, diff --git a/src/fabric/ScreenStackHeaderConfigNativeComponent.js b/src/fabric/ScreenStackHeaderConfigNativeComponent.js index 2bbc4fd045..1388bada81 100644 --- a/src/fabric/ScreenStackHeaderConfigNativeComponent.js +++ b/src/fabric/ScreenStackHeaderConfigNativeComponent.js @@ -41,9 +41,9 @@ export type NativeProps = $ReadOnly<{| titleColor?: ColorValue, disableBackButtonMenu?: boolean, hideBackButton?: boolean, + backButtonInCustomView?: boolean, // TODO: implement this props on iOS topInsetEnabled?: boolean, - backButtonInCustomView?: boolean, |}>; type ComponentType = HostComponent;