react-native-screens 4.10.0-beta.2 → 4.10.0-beta.3

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.
Files changed (23) hide show
  1. package/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt +3 -8
  2. package/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt +111 -15
  3. package/android/src/main/java/com/swmansion/rnscreens/Screen.kt +11 -2
  4. package/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt +1 -1
  5. package/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt +113 -33
  6. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +31 -285
  7. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +88 -53
  8. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigShadowNode.kt +3 -0
  9. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackViewManager.kt +1 -1
  10. package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt +257 -3
  11. package/android/src/main/java/com/swmansion/rnscreens/ext/ViewExt.kt +2 -0
  12. package/android/src/main/java/com/swmansion/rnscreens/utils/InsetsKt.kt +31 -0
  13. package/android/src/main/java/com/swmansion/rnscreens/utils/PaddingBundle.kt +1 -0
  14. package/android/src/paper/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt +14 -5
  15. package/ios/RNSFullWindowOverlay.mm +6 -6
  16. package/lib/commonjs/components/Screen.js +4 -2
  17. package/lib/commonjs/components/Screen.js.map +1 -1
  18. package/lib/module/components/Screen.js +4 -2
  19. package/lib/module/components/Screen.js.map +1 -1
  20. package/lib/typescript/components/Screen.d.ts.map +1 -1
  21. package/package.json +1 -1
  22. package/src/components/Screen.tsx +6 -4
  23. package/android/src/main/java/com/swmansion/rnscreens/NativeDismissalObserver.kt +0 -12
@@ -23,24 +23,19 @@ abstract class FabricEnabledHeaderConfigViewGroup(
23
23
  mStateWrapper = wrapper
24
24
  }
25
25
 
26
- fun updatePaddings(
27
- paddingStart: Int,
28
- paddingEnd: Int,
29
- ) {
30
- // Do nothing on Fabric. This method is used only on Paper.
31
- }
32
-
33
26
  fun updateHeaderConfigState(
34
27
  width: Int,
35
28
  height: Int,
36
29
  paddingStart: Int,
37
30
  paddingEnd: Int,
38
31
  ) {
32
+ // Implementation of this method differs between Fabric & Paper!
39
33
  updateState(width, height, paddingStart, paddingEnd)
40
34
  }
41
35
 
36
+ // Implementation of this method differs between Fabric & Paper!
42
37
  @UiThread
43
- fun updateState(
38
+ private fun updateState(
44
39
  width: Int,
45
40
  height: Int,
46
41
  paddingStart: Int,
@@ -3,18 +3,40 @@ package com.swmansion.rnscreens
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
5
  import android.os.Build
6
+ import android.view.WindowInsets
6
7
  import android.view.WindowManager
7
8
  import androidx.appcompat.widget.Toolbar
9
+ import androidx.core.view.WindowInsetsCompat
8
10
  import com.facebook.react.modules.core.ChoreographerCompat
9
11
  import com.facebook.react.modules.core.ReactChoreographer
10
12
  import com.facebook.react.uimanager.ThemedReactContext
13
+ import com.swmansion.rnscreens.utils.InsetsCompat
14
+ import com.swmansion.rnscreens.utils.resolveInsetsOrZero
15
+ import kotlin.math.max
11
16
 
12
- // This class is used to store config closer to search bar
17
+ /**
18
+ * Main toolbar class representing the native header.
19
+ *
20
+ * This class is used to store config closer to search bar.
21
+ * It also handles inset/padding related logic in coordination with header config.
22
+ */
13
23
  @SuppressLint("ViewConstructor") // Only we construct this view, it is never inflated.
14
24
  open class CustomToolbar(
15
25
  context: Context,
16
26
  val config: ScreenStackHeaderConfig,
17
27
  ) : Toolbar(context) {
28
+ // Switch this flag to enable/disable display cutout avoidance.
29
+ // Currently this is controlled by isTopInsetEnabled prop.
30
+ private val shouldAvoidDisplayCutout
31
+ get() = config.isTopInsetEnabled
32
+
33
+ private val shouldApplyTopInset
34
+ get() = config.isTopInsetEnabled
35
+
36
+ private var lastInsets = InsetsCompat.NONE
37
+
38
+ private var isForceShadowStateUpdateOnLayoutRequested = false
39
+
18
40
  private var isLayoutEnqueued = false
19
41
  private val layoutCallback: ChoreographerCompat.FrameCallback =
20
42
  object : ChoreographerCompat.FrameCallback() {
@@ -59,27 +81,101 @@ open class CustomToolbar(
59
81
  }
60
82
  }
61
83
 
84
+ override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets? {
85
+ val unhandledInsets = super.onApplyWindowInsets(insets)
86
+
87
+ // There are few UI modes we could be running in
88
+ //
89
+ // 1. legacy non edge-to-edge mode,
90
+ // 2. edge-to-edge with gesture navigation,
91
+ // 3. edge-to-edge with translucent navigation buttons bar.
92
+ //
93
+ // Additionally we need to gracefully handle possible display cutouts.
94
+
95
+ // We use rootWindowInsets in lieu of insets or unhandledInsets here,
96
+ // because cutout sometimes (only in certain scenarios, e.g. with headerLeft view present)
97
+ // happen to be Insets.ZERO and is not reliable.
98
+ val rootWindowInsets = rootWindowInsets
99
+ val cutoutInsets =
100
+ resolveInsetsOrZero(WindowInsetsCompat.Type.displayCutout(), rootWindowInsets)
101
+ val systemBarInsets =
102
+ resolveInsetsOrZero(WindowInsetsCompat.Type.systemBars(), rootWindowInsets)
103
+ val statusBarInsetsStable =
104
+ resolveInsetsOrZero(
105
+ WindowInsetsCompat.Type.systemBars(),
106
+ rootWindowInsets,
107
+ ignoreVisibility = true,
108
+ )
109
+
110
+ // This seems to work fine in all tested configurations, because cutout & system bars overlap
111
+ // only in portrait mode & top inset is controlled separately, therefore we don't count
112
+ // any insets twice.
113
+ val horizontalInsets =
114
+ InsetsCompat.of(
115
+ cutoutInsets.left + systemBarInsets.left,
116
+ 0,
117
+ cutoutInsets.right + systemBarInsets.right,
118
+ 0,
119
+ )
120
+
121
+ // We want to handle display cutout always, no matter the HeaderConfig prop values.
122
+ // If there are no cutout displays, we want to apply the additional padding to
123
+ // respect the status bar.
124
+ val verticalInsets =
125
+ InsetsCompat.of(
126
+ 0,
127
+ max(cutoutInsets.top, if (shouldApplyTopInset) statusBarInsetsStable.top else 0),
128
+ 0,
129
+ max(cutoutInsets.bottom, 0),
130
+ )
131
+
132
+ val newInsets = InsetsCompat.add(horizontalInsets, verticalInsets)
133
+
134
+ if (lastInsets != newInsets) {
135
+ lastInsets = newInsets
136
+ applyExactPadding(
137
+ lastInsets.left,
138
+ lastInsets.top,
139
+ lastInsets.right,
140
+ lastInsets.bottom,
141
+ )
142
+ }
143
+
144
+ return unhandledInsets
145
+ }
146
+
62
147
  override fun onLayout(
63
- changed: Boolean,
148
+ hasSizeChanged: Boolean,
64
149
  l: Int,
65
150
  t: Int,
66
151
  r: Int,
67
152
  b: Int,
68
153
  ) {
69
- super.onLayout(changed, l, t, r, b)
154
+ super.onLayout(hasSizeChanged, l, t, r, b)
70
155
 
71
- if (!changed) {
72
- return
73
- }
156
+ config.onNativeToolbarLayout(
157
+ this,
158
+ hasSizeChanged || isForceShadowStateUpdateOnLayoutRequested,
159
+ )
160
+ isForceShadowStateUpdateOnLayoutRequested = false
161
+ }
74
162
 
75
- val contentInsetStart = if (navigationIcon != null) contentInsetStartWithNavigation else contentInsetStart
76
- if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
77
- val width = r - l
78
- val height = b - t
79
- config.updateHeaderConfigState(width, height, contentInsetStart, contentInsetEnd)
80
- } else {
81
- // our children are already laid out
82
- config.updatePaddings(contentInsetStart, contentInsetEnd)
83
- }
163
+ fun updateContentInsets() {
164
+ contentInsetStartWithNavigation = config.preferredContentInsetStartWithNavigation
165
+ setContentInsetsRelative(config.preferredContentInsetStart, config.preferredContentInsetEnd)
166
+ }
167
+
168
+ private fun applyExactPadding(
169
+ left: Int,
170
+ top: Int,
171
+ right: Int,
172
+ bottom: Int,
173
+ ) {
174
+ requestForceShadowStateUpdateOnLayout()
175
+ setPadding(left, top, right, bottom)
176
+ }
177
+
178
+ private fun requestForceShadowStateUpdateOnLayout() {
179
+ isForceShadowStateUpdateOnLayoutRequested = shouldAvoidDisplayCutout
84
180
  }
85
181
  }
@@ -275,7 +275,7 @@ class Screen(
275
275
  throw IllegalStateException("[RNScreens] activityState can only progress in NativeStack")
276
276
  }
277
277
  this.activityState = activityState
278
- container?.notifyChildUpdate()
278
+ container?.onChildUpdate()
279
279
  }
280
280
 
281
281
  fun setScreenOrientation(screenOrientation: String?) {
@@ -482,7 +482,14 @@ class Screen(
482
482
  ?.dispatchEvent(HeaderHeightChangeEvent(surfaceId, id, headerHeight))
483
483
  }
484
484
 
485
- internal fun notifySheetDetentChange(
485
+ internal fun onSheetDetentChanged(
486
+ detentIndex: Int,
487
+ isStable: Boolean,
488
+ ) {
489
+ dispatchSheetDetentChanged(detentIndex, isStable)
490
+ }
491
+
492
+ private fun dispatchSheetDetentChanged(
486
493
  detentIndex: Int,
487
494
  isStable: Boolean,
488
495
  ) {
@@ -571,3 +578,5 @@ class Screen(
571
578
  const val SHEET_FIT_TO_CONTENTS = -1.0
572
579
  }
573
580
  }
581
+
582
+ internal fun View.asScreen() = this as Screen
@@ -91,7 +91,7 @@ open class ScreenContainer(
91
91
  val isNested: Boolean
92
92
  get() = parentScreenWrapper != null
93
93
 
94
- fun notifyChildUpdate() {
94
+ fun onChildUpdate() {
95
95
  performUpdatesNow()
96
96
  }
97
97
 
@@ -12,6 +12,72 @@ import com.swmansion.rnscreens.events.StackFinishTransitioningEvent
12
12
  import com.swmansion.rnscreens.utils.setTweenAnimations
13
13
  import java.util.Collections
14
14
  import kotlin.collections.ArrayList
15
+ import kotlin.math.max
16
+
17
+ internal interface ChildDrawingOrderStrategy {
18
+ /**
19
+ * Mutates the list of draw operations **in-place**.
20
+ */
21
+ fun apply(drawingOperations: MutableList<ScreenStack.DrawingOp>)
22
+
23
+ /**
24
+ * Enables the given strategy. When enabled - the strategy **might** mutate the operations
25
+ * list passed to `apply` method.
26
+ */
27
+ fun enable()
28
+
29
+ /**
30
+ * Disables the given strategy - even when `apply` is called it **must not** produce
31
+ * any side effect (it must not manipulate the drawing operations list passed to `apply` method).
32
+ */
33
+ fun disable()
34
+
35
+ fun isEnabled(): Boolean
36
+ }
37
+
38
+ internal abstract class ChildDrawingOrderStrategyBase(
39
+ var enabled: Boolean = false,
40
+ ) : ChildDrawingOrderStrategy {
41
+ override fun enable() {
42
+ enabled = true
43
+ }
44
+
45
+ override fun disable() {
46
+ enabled = false
47
+ }
48
+
49
+ override fun isEnabled() = enabled
50
+ }
51
+
52
+ internal class SwapLastTwo : ChildDrawingOrderStrategyBase() {
53
+ override fun apply(drawingOperations: MutableList<ScreenStack.DrawingOp>) {
54
+ if (!isEnabled()) {
55
+ return
56
+ }
57
+ if (drawingOperations.size >= 2) {
58
+ Collections.swap(drawingOperations, drawingOperations.lastIndex, drawingOperations.lastIndex - 1)
59
+ }
60
+ }
61
+ }
62
+
63
+ internal class ReverseOrderInRange(
64
+ val range: IntRange,
65
+ ) : ChildDrawingOrderStrategyBase() {
66
+ override fun apply(drawingOperations: MutableList<ScreenStack.DrawingOp>) {
67
+ if (!isEnabled()) {
68
+ return
69
+ }
70
+
71
+ var startRange = range.start
72
+ var endRange = range.endInclusive
73
+
74
+ while (startRange < endRange) {
75
+ Collections.swap(drawingOperations, startRange, endRange)
76
+ startRange += 1
77
+ endRange -= 1
78
+ }
79
+ }
80
+ }
15
81
 
16
82
  class ScreenStack(
17
83
  context: Context?,
@@ -22,9 +88,10 @@ class ScreenStack(
22
88
  private var drawingOps: MutableList<DrawingOp> = ArrayList()
23
89
  private var topScreenWrapper: ScreenStackFragmentWrapper? = null
24
90
  private var removalTransitionStarted = false
25
- private var isDetachingCurrentScreen = false
26
- private var reverseLastTwoChildren = false
27
91
  private var previousChildrenCount = 0
92
+
93
+ private var childDrawingOrderStrategy: ChildDrawingOrderStrategy? = null
94
+
28
95
  var goingForward = false
29
96
 
30
97
  /**
@@ -56,11 +123,13 @@ class ScreenStack(
56
123
 
57
124
  override fun startViewTransition(view: View) {
58
125
  super.startViewTransition(view)
126
+ childDrawingOrderStrategy?.enable()
59
127
  removalTransitionStarted = true
60
128
  }
61
129
 
62
130
  override fun endViewTransition(view: View) {
63
131
  super.endViewTransition(view)
132
+ childDrawingOrderStrategy?.disable()
64
133
  if (removalTransitionStarted) {
65
134
  removalTransitionStarted = false
66
135
  dispatchOnFinishTransitioning()
@@ -98,9 +167,12 @@ class ScreenStack(
98
167
  // when all screens are dismissed and no screen is to be displayed on top. We need to gracefully
99
168
  // handle the case of newTop being NULL, which happens in several places below
100
169
  var newTop: ScreenFragmentWrapper? = null // newTop is nullable, see the above comment ^
101
- var visibleBottom: ScreenFragmentWrapper? =
102
- null // this is only set if newTop has one of transparent presentation modes
103
- isDetachingCurrentScreen = false // we reset it so the previous value is not used by mistake
170
+
171
+ // this is only set if newTop has one of transparent presentation modes
172
+ var visibleBottom: ScreenFragmentWrapper? = null
173
+
174
+ // reset, to not use previously set strategy by mistake
175
+ childDrawingOrderStrategy = null
104
176
 
105
177
  // Determine new first & last visible screens.
106
178
  // Scope function to limit the scope of locals.
@@ -121,7 +193,11 @@ class ScreenStack(
121
193
 
122
194
  var shouldUseOpenAnimation = true
123
195
  var stackAnimation: StackAnimation? = null
124
- if (newTop != null && !stack.contains(newTop)) {
196
+
197
+ val newTopAlreadyInStack = stack.contains(newTop)
198
+ val topScreenWillChange = newTop !== topScreenWrapper
199
+
200
+ if (newTop != null && !newTopAlreadyInStack) {
125
201
  // if new top screen wasn't on stack we do "open animation" so long it is not the very first
126
202
  // screen on stack
127
203
  if (topScreenWrapper != null) {
@@ -129,9 +205,9 @@ class ScreenStack(
129
205
  // if the previous top screen does not exist anymore and the new top was not on the stack
130
206
  // before, probably replace or reset was called, so we play the "close animation".
131
207
  // Otherwise it's open animation
132
- val containsTopScreen = topScreenWrapper?.let { screenWrappers.contains(it) } == true
208
+ val previousTopScreenRemainsInStack = topScreenWrapper?.let { screenWrappers.contains(it) } == true
133
209
  val isPushReplace = newTop.screen.replaceAnimation === Screen.ReplaceAnimation.PUSH
134
- shouldUseOpenAnimation = containsTopScreen || isPushReplace
210
+ shouldUseOpenAnimation = previousTopScreenRemainsInStack || isPushReplace
135
211
  // if the replace animation is `push`, the new top screen provides the animation, otherwise the previous one
136
212
  stackAnimation = if (shouldUseOpenAnimation) newTop.screen.stackAnimation else topScreenWrapper?.screen?.stackAnimation
137
213
  } else {
@@ -140,7 +216,7 @@ class ScreenStack(
140
216
  stackAnimation = StackAnimation.NONE
141
217
  goingForward = true
142
218
  }
143
- } else if (newTop != null && topScreenWrapper != null && topScreenWrapper !== newTop) {
219
+ } else if (newTop != null && topScreenWrapper != null && topScreenWillChange) {
144
220
  // otherwise if we are performing top screen change we do "close animation"
145
221
  shouldUseOpenAnimation = false
146
222
  stackAnimation = topScreenWrapper?.screen?.stackAnimation
@@ -160,8 +236,29 @@ class ScreenStack(
160
236
  // appears on top of the previous one. You can read more about in the comment
161
237
  // for the code we use to change that behavior:
162
238
  // https://github.com/airbnb/native-navigation/blob/9cf50bf9b751b40778f473f3b19fcfe2c4d40599/lib/android/src/main/java/com/airbnb/android/react/navigation/ScreenCoordinatorLayout.java#L18
163
- // Note: This should not be set in case there is only a single screen in stack or animation `none` is used. Atm needsDrawReordering implementation guards that assuming that first screen on stack uses `NONE` animation.
164
- isDetachingCurrentScreen = true
239
+ // Note: This should not be set in case there is only a single screen in stack or animation `none` is used.
240
+ // Atm needsDrawReordering implementation guards that assuming that first screen on stack uses `NONE` animation.
241
+ childDrawingOrderStrategy = SwapLastTwo()
242
+ } else if (newTop != null &&
243
+ newTopAlreadyInStack &&
244
+ topScreenWrapper?.screen?.isTransparent() == true &&
245
+ newTop.screen.isTransparent() == false
246
+ ) {
247
+ // In case where we dismiss multiple transparent views we want to ensure
248
+ // that they are drawn in correct order - Android swaps them by default,
249
+ // so we need to swap the swap to unswap :D
250
+ val dismissedTransparentScreenApproxCount =
251
+ stack
252
+ .asReversed()
253
+ .asSequence()
254
+ .takeWhile {
255
+ it !== newTop &&
256
+ it.screen.isTransparent()
257
+ }.count()
258
+ if (dismissedTransparentScreenApproxCount > 1) {
259
+ childDrawingOrderStrategy =
260
+ ReverseOrderInRange(max(stack.lastIndex - dismissedTransparentScreenApproxCount + 1, 0)..stack.lastIndex)
261
+ }
165
262
  }
166
263
 
167
264
  createTransaction().let { transaction ->
@@ -241,23 +338,6 @@ class ScreenStack(
241
338
  stack.forEach { it.onContainerUpdate() }
242
339
  }
243
340
 
244
- // below methods are taken from
245
- // https://github.com/airbnb/native-navigation/blob/9cf50bf9b751b40778f473f3b19fcfe2c4d40599/lib/android/src/main/java/com/airbnb/android/react/navigation/ScreenCoordinatorLayout.java#L43
246
- // and are used to swap the order of drawing views when navigating forward with the transitions
247
- // that are making transitioning fragments appear one on another. See more info in the comment to
248
- // the linked class.
249
- override fun removeView(view: View) {
250
- // we set this property to reverse the order of drawing views
251
- // when we want to push new fragment on top of the previous one and their animations collide.
252
- // More information in:
253
- // https://github.com/airbnb/native-navigation/blob/9cf50bf9b751b40778f473f3b19fcfe2c4d40599/lib/android/src/main/java/com/airbnb/android/react/navigation/ScreenCoordinatorLayout.java#L17
254
- if (isDetachingCurrentScreen) {
255
- isDetachingCurrentScreen = false
256
- reverseLastTwoChildren = true
257
- }
258
- super.removeView(view)
259
- }
260
-
261
341
  private fun drawAndRelease() {
262
342
  // We make a copy of the drawingOps and use it to dispatch draws in order to be sure
263
343
  // that we do not modify the original list. There are cases when `op.draw` can call
@@ -276,12 +356,12 @@ class ScreenStack(
276
356
 
277
357
  // check the view removal is completed (by comparing the previous children count)
278
358
  if (drawingOps.size < previousChildrenCount) {
279
- reverseLastTwoChildren = false
359
+ childDrawingOrderStrategy = null
280
360
  }
281
361
  previousChildrenCount = drawingOps.size
282
- if (reverseLastTwoChildren && drawingOps.size >= 2) {
283
- Collections.swap(drawingOps, drawingOps.size - 1, drawingOps.size - 2)
284
- }
362
+
363
+ childDrawingOrderStrategy?.apply(drawingOps)
364
+
285
365
  drawAndRelease()
286
366
  }
287
367
 
@@ -310,7 +390,7 @@ class ScreenStack(
310
390
  // See: https://developer.android.com/about/versions/15/behavior-changes-15?hl=en#openjdk-api-changes
311
391
  private fun obtainDrawingOp(): DrawingOp = if (drawingOpPool.isEmpty()) DrawingOp() else drawingOpPool.removeAt(drawingOpPool.lastIndex)
312
392
 
313
- private inner class DrawingOp {
393
+ internal inner class DrawingOp {
314
394
  var canvas: Canvas? = null
315
395
  var child: View? = null
316
396
  var drawingTime: Long = 0