react-native-screens 4.7.0-beta.3 → 4.7.0

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.
@@ -7,12 +7,18 @@ import com.facebook.react.bridge.WritableMap
7
7
  import com.facebook.react.bridge.WritableNativeMap
8
8
  import com.facebook.react.uimanager.PixelUtil
9
9
  import com.facebook.react.uimanager.StateWrapper
10
+ import kotlin.math.abs
10
11
 
11
12
  abstract class FabricEnabledHeaderSubviewViewGroup(
12
13
  context: Context?,
13
14
  ) : ViewGroup(context) {
14
15
  private var mStateWrapper: StateWrapper? = null
15
16
 
17
+ private var lastWidth = 0f
18
+ private var lastHeight = 0f
19
+ private var lastOffsetX = 0f
20
+ private var lastOffsetY = 0f
21
+
16
22
  fun setStateWrapper(wrapper: StateWrapper?) {
17
23
  mStateWrapper = wrapper
18
24
  }
@@ -38,6 +44,21 @@ abstract class FabricEnabledHeaderSubviewViewGroup(
38
44
  val offsetXDip: Float = PixelUtil.toDIPFromPixel(offsetX.toFloat())
39
45
  val offsetYDip: Float = PixelUtil.toDIPFromPixel(offsetY.toFloat())
40
46
 
47
+ // Check incoming state values. If they're already the correct value, return early to prevent
48
+ // infinite UpdateState/SetState loop.
49
+ if (abs(lastWidth - realWidth) < DELTA &&
50
+ abs(lastHeight - realHeight) < DELTA &&
51
+ abs(lastOffsetX - offsetXDip) < DELTA &&
52
+ abs(lastOffsetY - offsetYDip) < DELTA
53
+ ) {
54
+ return
55
+ }
56
+
57
+ lastWidth = realWidth
58
+ lastHeight = realHeight
59
+ lastOffsetX = offsetXDip
60
+ lastOffsetY = offsetYDip
61
+
41
62
  val map: WritableMap =
42
63
  WritableNativeMap().apply {
43
64
  putDouble("frameWidth", realWidth.toDouble())
@@ -48,4 +69,8 @@ abstract class FabricEnabledHeaderSubviewViewGroup(
48
69
 
49
70
  mStateWrapper?.updateState(map)
50
71
  }
72
+
73
+ companion object {
74
+ private const val DELTA = 0.9f
75
+ }
51
76
  }
@@ -45,8 +45,9 @@ import com.swmansion.rnscreens.bottomsheet.useSingleDetent
45
45
  import com.swmansion.rnscreens.bottomsheet.useThreeDetents
46
46
  import com.swmansion.rnscreens.bottomsheet.useTwoDetents
47
47
  import com.swmansion.rnscreens.bottomsheet.usesFormSheetPresentation
48
+ import com.swmansion.rnscreens.events.ScreenAnimationDelegate
48
49
  import com.swmansion.rnscreens.events.ScreenDismissedEvent
49
- import com.swmansion.rnscreens.events.ScreenEventDelegate
50
+ import com.swmansion.rnscreens.events.ScreenEventEmitter
50
51
  import com.swmansion.rnscreens.ext.recycle
51
52
  import com.swmansion.rnscreens.transition.ExternalBoundaryValuesEvaluator
52
53
  import com.swmansion.rnscreens.utils.DeviceUtils
@@ -366,7 +367,14 @@ class ScreenStackFragment :
366
367
  }
367
368
  animatorSet.play(alphaAnimator).with(slideAnimator)
368
369
  }
369
- animatorSet.addListener(ScreenEventDelegate(this))
370
+ animatorSet.addListener(
371
+ ScreenAnimationDelegate(
372
+ this,
373
+ ScreenEventEmitter(this.screen),
374
+ if (enter) ScreenAnimationDelegate.AnimationType.ENTER
375
+ else ScreenAnimationDelegate.AnimationType.EXIT
376
+ )
377
+ )
370
378
  return animatorSet
371
379
  }
372
380
 
@@ -0,0 +1,88 @@
1
+ package com.swmansion.rnscreens.events
2
+
3
+ import android.animation.Animator
4
+ import android.util.Log
5
+ import com.swmansion.rnscreens.ScreenStackFragmentWrapper
6
+
7
+ // The goal is to make this universal delegate for handling animation progress related logic.
8
+ // At this moment this class works only with form sheet presentation.
9
+ class ScreenAnimationDelegate(
10
+ private val wrapper: ScreenStackFragmentWrapper,
11
+ private val eventEmitter: ScreenEventEmitter?,
12
+ private val animationType: AnimationType,
13
+ ) : Animator.AnimatorListener {
14
+ enum class AnimationType {
15
+ ENTER,
16
+ EXIT
17
+ }
18
+
19
+ private var currentState: LifecycleState = LifecycleState.INITIALIZED
20
+
21
+ private fun progressState() {
22
+ currentState =
23
+ when (currentState) {
24
+ LifecycleState.INITIALIZED -> LifecycleState.START_DISPATCHED
25
+ LifecycleState.START_DISPATCHED -> LifecycleState.END_DISPATCHED
26
+ LifecycleState.END_DISPATCHED -> LifecycleState.END_DISPATCHED
27
+ }
28
+ }
29
+
30
+ override fun onAnimationStart(animation: Animator) {
31
+ if (currentState === LifecycleState.INITIALIZED) {
32
+ progressState()
33
+
34
+ // These callbacks do not work as expected from this call site, TODO: investigate it.
35
+ // To fix it quickly we emit required events manually
36
+ // wrapper.onViewAnimationStart()
37
+
38
+ when (animationType) {
39
+ AnimationType.ENTER -> eventEmitter?.dispatchOnWillAppear()
40
+ AnimationType.EXIT -> eventEmitter?.dispatchOnWillDisappear()
41
+ }
42
+
43
+ val isExitAnimation = animationType === AnimationType.EXIT
44
+ eventEmitter?.dispatchTransitionProgress(
45
+ 0.0f,
46
+ isExitAnimation,
47
+ isExitAnimation,
48
+ )
49
+ }
50
+ }
51
+
52
+ override fun onAnimationEnd(animation: Animator) {
53
+ if (currentState === LifecycleState.START_DISPATCHED) {
54
+ progressState()
55
+ animation.removeListener(this)
56
+
57
+ // wrapper.onViewAnimationEnd()
58
+
59
+ when (animationType) {
60
+ AnimationType.ENTER -> eventEmitter?.dispatchOnAppear()
61
+ AnimationType.EXIT -> eventEmitter?.dispatchOnDisappear()
62
+ }
63
+
64
+ val isExitAnimation = animationType === AnimationType.EXIT
65
+ eventEmitter?.dispatchTransitionProgress(
66
+ 1.0f,
67
+ isExitAnimation,
68
+ isExitAnimation,
69
+ )
70
+
71
+ wrapper.screen.endRemovalTransition()
72
+ }
73
+ }
74
+
75
+ override fun onAnimationCancel(animation: Animator) = Unit
76
+
77
+ override fun onAnimationRepeat(animation: Animator) = Unit
78
+
79
+ private enum class LifecycleState {
80
+ INITIALIZED,
81
+ START_DISPATCHED,
82
+ END_DISPATCHED,
83
+ }
84
+
85
+ companion object {
86
+ const val TAG = "ScreenEventDelegate"
87
+ }
88
+ }
@@ -0,0 +1,36 @@
1
+ package com.swmansion.rnscreens.events
2
+
3
+ import com.facebook.react.uimanager.UIManagerHelper
4
+ import com.swmansion.rnscreens.Screen
5
+ import com.swmansion.rnscreens.ScreenFragment
6
+
7
+ // TODO: Consider taking weak ref here or accepting screen as argument in every method
8
+ // to avoid reference cycle.
9
+ class ScreenEventEmitter(val screen: Screen) {
10
+ val reactEventDispatcher
11
+ get() = screen.reactEventDispatcher
12
+
13
+ val reactSurfaceId
14
+ get() = UIManagerHelper.getSurfaceId(screen)
15
+
16
+ fun dispatchOnWillAppear() =
17
+ reactEventDispatcher?.dispatchEvent(ScreenWillAppearEvent(reactSurfaceId, screen.id))
18
+
19
+ fun dispatchOnAppear() =
20
+ reactEventDispatcher?.dispatchEvent(ScreenAppearEvent(reactSurfaceId, screen.id))
21
+
22
+ fun dispatchOnWillDisappear() =
23
+ reactEventDispatcher?.dispatchEvent(ScreenWillDisappearEvent(reactSurfaceId, screen.id))
24
+
25
+ fun dispatchOnDisappear() =
26
+ reactEventDispatcher?.dispatchEvent(ScreenDisappearEvent(reactSurfaceId, screen.id))
27
+
28
+ fun dispatchOnDismissed() =
29
+ reactEventDispatcher?.dispatchEvent(ScreenDismissedEvent(reactSurfaceId, screen.id))
30
+
31
+ fun dispatchTransitionProgress(progress: Float, isExitAnimation: Boolean, isGoingForward: Boolean) {
32
+ val sanitizedProgress = progress.coerceIn(0.0f, 1.0f)
33
+ val coalescingKey = ScreenFragment.getCoalescingKey(sanitizedProgress)
34
+ reactEventDispatcher?.dispatchEvent(ScreenTransitionProgressEvent(reactSurfaceId, screen.id, sanitizedProgress, isExitAnimation, isGoingForward, coalescingKey))
35
+ }
36
+ }
@@ -35,8 +35,7 @@ class RNSScreenStackHeaderConfigComponentDescriptor final
35
35
  auto stateData = state->getData();
36
36
 
37
37
  if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
38
- layoutableShadowNode.setSize(
39
- {stateData.frameSize.width, stateData.frameSize.height});
38
+ layoutableShadowNode.setSize(stateData.frameSize);
40
39
  #ifdef ANDROID
41
40
  layoutableShadowNode.setPadding({
42
41
  stateData.paddingStart,
@@ -8,6 +8,7 @@
8
8
  #include <react/renderer/components/rnscreens/utils/RectUtil.h>
9
9
  #include <react/renderer/core/ConcreteComponentDescriptor.h>
10
10
  #include "RNSScreenStackHeaderSubviewShadowNode.h"
11
+ #include "utils/RectUtil.h"
11
12
 
12
13
  namespace facebook::react {
13
14
 
@@ -32,12 +33,11 @@ class RNSScreenStackHeaderSubviewComponentDescriptor final
32
33
 
33
34
  auto state = std::static_pointer_cast<
34
35
  const RNSScreenStackHeaderSubviewShadowNode::ConcreteState>(
35
- shadowNode.getState());
36
+ shadowNode.getMostRecentState());
36
37
  auto stateData = state->getData();
37
38
 
38
- if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
39
- layoutableShadowNode.setSize(
40
- Size{stateData.frameSize.width, stateData.frameSize.height});
39
+ if (!isSizeEmpty(stateData.frameSize)) {
40
+ layoutableShadowNode.setSize(stateData.frameSize);
41
41
  }
42
42
 
43
43
  ConcreteComponentDescriptor::adopt(shadowNode);
@@ -44,7 +44,7 @@ class JSI_EXPORT RNSScreenStackHeaderSubviewState final {
44
44
 
45
45
  #endif // ANDROID
46
46
 
47
- const Size frameSize{};
47
+ const Size frameSize{-1.f, -1.f};
48
48
  Point contentOffset{};
49
49
 
50
50
  #pragma mark - Getters
@@ -33,4 +33,11 @@ inline constexpr bool checkFrameSizesEqualWithEps(
33
33
  equalWithRespectToEps(first.height, second.height, eps);
34
34
  }
35
35
 
36
+ /**
37
+ * @return false if any component value is less than 0
38
+ */
39
+ inline constexpr bool isSizeEmpty(const react::Size &size) {
40
+ return size.width < 0 || size.height < 0;
41
+ }
42
+
36
43
  } // namespace rnscreens
@@ -73,15 +73,30 @@ namespace react = facebook::react;
73
73
 
74
74
  - (void)attachToAncestorScreenView
75
75
  {
76
- if (![self.reactSuperview isKindOfClass:RNSScreenView.class]) {
77
- RCTLogError(@"Expected reactSuperview to be a RNSScreenView. Found %@", self.reactSuperview);
76
+ RNSScreen *_Nullable screen =
77
+ static_cast<RNSScreen *_Nullable>([[self findFirstScreenViewAncestor] reactViewController]);
78
+ if (screen == nil) {
79
+ RCTLogError(@"Failed to find parent screen controller from %@.", self);
78
80
  return;
79
81
  }
80
-
81
- RNSScreen *screen = (RNSScreen *)[self.reactSuperview reactViewController];
82
82
  [self attachToAncestorScreenViewStartingFrom:screen];
83
83
  }
84
84
 
85
+ - (nullable RNSScreenView *)findFirstScreenViewAncestor
86
+ {
87
+ UIView *currentView = self;
88
+
89
+ // In standard scenario this should do only a single iteration.
90
+ // Haven't got repro, but we got reports that there are scenarios
91
+ // when there are intermediate views between screen view & the content wrapper.
92
+ // https://github.com/software-mansion/react-native-screens/pull/2683
93
+ do {
94
+ currentView = currentView.reactSuperview;
95
+ } while (currentView != nil && ![currentView isKindOfClass:RNSScreenView.class]);
96
+
97
+ return static_cast<RNSScreenView *_Nullable>(currentView);
98
+ }
99
+
85
100
  #ifdef RCT_NEW_ARCH_ENABLED
86
101
 
87
102
  #pragma mark - RCTComponentViewProtocol
@@ -235,7 +235,7 @@ export interface ScreenProps extends ViewProps {
235
235
  * while **Android is limited to three**.
236
236
  *
237
237
  * There is also possibility to specify `fitToContents` literal, which intents to set the sheet height
238
- * to the height of its contents.
238
+ * to the height of its contents. On iOS `fitToContents` currently also includes small padding accounting for bottom inset.
239
239
  *
240
240
  * Please note that the array **must** be sorted in ascending order. This invariant is verified only in developement mode,
241
241
  * where violation results in error.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-screens",
3
- "version": "4.7.0-beta.3",
3
+ "version": "4.7.0",
4
4
  "description": "Native navigation primitives for your React Native app.",
5
5
  "scripts": {
6
6
  "submodules": "git submodule update --init --recursive && (cd react-navigation && yarn && yarn build && cd ../)",
package/src/types.tsx CHANGED
@@ -303,7 +303,7 @@ export interface ScreenProps extends ViewProps {
303
303
  * while **Android is limited to three**.
304
304
  *
305
305
  * There is also possibility to specify `fitToContents` literal, which intents to set the sheet height
306
- * to the height of its contents.
306
+ * to the height of its contents. On iOS `fitToContents` currently also includes small padding accounting for bottom inset.
307
307
  *
308
308
  * Please note that the array **must** be sorted in ascending order. This invariant is verified only in developement mode,
309
309
  * where violation results in error.
@@ -1,48 +0,0 @@
1
- package com.swmansion.rnscreens.events
2
-
3
- import android.animation.Animator
4
- import com.swmansion.rnscreens.ScreenFragmentWrapper
5
-
6
- class ScreenEventDelegate(
7
- private val wrapper: ScreenFragmentWrapper,
8
- ) : Animator.AnimatorListener {
9
- private var currentState: LifecycleState = LifecycleState.INITIALIZED
10
-
11
- private fun progressState() {
12
- currentState =
13
- when (currentState) {
14
- LifecycleState.INITIALIZED -> LifecycleState.START_DISPATCHED
15
- LifecycleState.START_DISPATCHED -> LifecycleState.END_DISPATCHED
16
- LifecycleState.END_DISPATCHED -> LifecycleState.END_DISPATCHED
17
- }
18
- }
19
-
20
- override fun onAnimationStart(animation: Animator) {
21
- if (currentState === LifecycleState.INITIALIZED) {
22
- progressState()
23
- wrapper.onViewAnimationStart()
24
- }
25
- }
26
-
27
- override fun onAnimationEnd(animation: Animator) {
28
- if (currentState === LifecycleState.START_DISPATCHED) {
29
- progressState()
30
- animation.removeListener(this)
31
- wrapper.onViewAnimationEnd()
32
- }
33
- }
34
-
35
- override fun onAnimationCancel(animation: Animator) = Unit
36
-
37
- override fun onAnimationRepeat(animation: Animator) = Unit
38
-
39
- private enum class LifecycleState {
40
- INITIALIZED,
41
- START_DISPATCHED,
42
- END_DISPATCHED,
43
- }
44
-
45
- companion object {
46
- const val TAG = "ScreenEventDelegate"
47
- }
48
- }