react-native-screens 4.10.0-beta.2 → 4.10.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.
Files changed (36) hide show
  1. package/RNScreens.podspec +2 -2
  2. package/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt +3 -8
  3. package/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt +9 -6
  4. package/android/src/fabric/java/com/swmansion/rnscreens/NativeProxy.kt +2 -1
  5. package/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt +111 -15
  6. package/android/src/main/java/com/swmansion/rnscreens/Screen.kt +54 -17
  7. package/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt +1 -1
  8. package/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt +125 -48
  9. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +46 -286
  10. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +94 -54
  11. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigShadowNode.kt +3 -0
  12. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.kt +19 -2
  13. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackViewManager.kt +2 -1
  14. package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt +262 -3
  15. package/android/src/main/java/com/swmansion/rnscreens/ext/ViewExt.kt +2 -0
  16. package/android/src/main/java/com/swmansion/rnscreens/utils/InsetsKt.kt +31 -0
  17. package/android/src/main/java/com/swmansion/rnscreens/utils/PaddingBundle.kt +1 -0
  18. package/android/src/paper/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt +14 -5
  19. package/android/src/versioned/pointerevents/77/com/swmansion/rnscreens/{ScreensCoordinatorLayoutPointerEventsImpl.kt → PointerEventsBoxNoneImpl.kt} +1 -1
  20. package/android/src/versioned/pointerevents/latest/com/swmansion/rnscreens/{ScreensCoordinatorLayoutPointerEventsImpl.kt → PointerEventsBoxNoneImpl.kt} +1 -1
  21. package/common/cpp/react/renderer/components/rnscreens/RNSModalScreenShadowNode.cpp +1 -3
  22. package/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h +7 -3
  23. package/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewComponentDescriptor.h +1 -1
  24. package/cpp/RNSScreenRemovalListener.cpp +3 -1
  25. package/ios/RNSFullWindowOverlay.mm +6 -6
  26. package/ios/RNSScreen.mm +12 -0
  27. package/ios/RNSScreenStackHeaderConfig.h +1 -1
  28. package/ios/RNSScreenStackHeaderConfig.mm +27 -22
  29. package/lib/commonjs/components/Screen.js +13 -4
  30. package/lib/commonjs/components/Screen.js.map +1 -1
  31. package/lib/module/components/Screen.js +13 -4
  32. package/lib/module/components/Screen.js.map +1 -1
  33. package/lib/typescript/components/Screen.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/components/Screen.tsx +23 -13
  36. package/android/src/main/java/com/swmansion/rnscreens/NativeDismissalObserver.kt +0 -12
package/RNScreens.podspec CHANGED
@@ -3,7 +3,7 @@ require "json"
3
3
  package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
4
 
5
5
  new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
6
- platform = new_arch_enabled ? "11.0" : "9.0"
6
+ min_supported_ios_version = new_arch_enabled ? "15.1" : "15.1"
7
7
  source_files = new_arch_enabled ? 'ios/**/*.{h,m,mm,cpp}' : ["ios/**/*.{h,m,mm}", "cpp/RNScreensTurboModule.cpp", "cpp/RNScreensTurboModule.h"]
8
8
 
9
9
  Pod::Spec.new do |s|
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
16
16
  s.homepage = "https://github.com/software-mansion/react-native-screens"
17
17
  s.license = "MIT"
18
18
  s.author = { "author" => "author@domain.cn" }
19
- s.platforms = { :ios => platform, :tvos => "11.0", :visionos => "1.0" }
19
+ s.platforms = { :ios => min_supported_ios_version, :tvos => "11.0", :visionos => "1.0" }
20
20
  s.source = { :git => "https://github.com/software-mansion/react-native-screens.git", :tag => "#{s.version}" }
21
21
  s.source_files = source_files
22
22
  s.project_header_files = "cpp/**/*.h" # Don't expose C++ headers publicly to allow importing framework into Swift files
@@ -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,
@@ -14,8 +14,9 @@ abstract class FabricEnabledViewGroup(
14
14
  ) : ViewGroup(context) {
15
15
  private var mStateWrapper: StateWrapper? = null
16
16
 
17
- private var lastSetWidth = 0f
18
- private var lastSetHeight = 0f
17
+ private var lastWidth = 0f
18
+ private var lastHeight = 0f
19
+ private var lastHeaderHeight = 0f
19
20
 
20
21
  fun setStateWrapper(wrapper: StateWrapper?) {
21
22
  mStateWrapper = wrapper
@@ -42,14 +43,16 @@ abstract class FabricEnabledViewGroup(
42
43
  // Check incoming state values. If they're already the correct value, return early to prevent
43
44
  // infinite UpdateState/SetState loop.
44
45
  val delta = 0.9f
45
- if (abs(lastSetWidth - realWidth) < delta &&
46
- abs(lastSetHeight - realHeight) < delta
46
+ if (abs(lastWidth - realWidth) < delta &&
47
+ abs(lastHeight - realHeight) < delta &&
48
+ abs(lastHeaderHeight - realHeaderHeight) < delta
47
49
  ) {
48
50
  return
49
51
  }
50
52
 
51
- lastSetWidth = realWidth
52
- lastSetHeight = realHeight
53
+ lastWidth = realWidth
54
+ lastHeight = realHeight
55
+ lastHeaderHeight = realHeaderHeight
53
56
  val map: WritableMap =
54
57
  WritableNativeMap().apply {
55
58
  putDouble("frameWidth", realWidth.toDouble())
@@ -41,7 +41,8 @@ class NativeProxy {
41
41
  }
42
42
  }
43
43
 
44
- // Called from native
44
+ // Called from native. Currently this method is called from MountingCoordinator thread,
45
+ // which usually is not UI thread.
45
46
  @DoNotStrip
46
47
  public fun notifyScreenRemoved(screenTag: Int) {
47
48
  // Since RN 0.78 the screenTag we receive as argument here might not belong to a screen
@@ -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
  }
@@ -27,6 +27,7 @@ import com.google.android.material.shape.CornerFamily
27
27
  import com.google.android.material.shape.MaterialShapeDrawable
28
28
  import com.google.android.material.shape.ShapeAppearanceModel
29
29
  import com.swmansion.rnscreens.bottomsheet.isSheetFitToContents
30
+ import com.swmansion.rnscreens.bottomsheet.useSingleDetent
30
31
  import com.swmansion.rnscreens.bottomsheet.usesFormSheetPresentation
31
32
  import com.swmansion.rnscreens.events.HeaderHeightChangeEvent
32
33
  import com.swmansion.rnscreens.events.SheetDetentChangedEvent
@@ -103,6 +104,9 @@ class Screen(
103
104
  field = value
104
105
  }
105
106
 
107
+ private val isNativeStackScreen: Boolean
108
+ get() = container is ScreenStack
109
+
106
110
  init {
107
111
  // we set layout params as WindowManager.LayoutParams to workaround the issue with TextInputs
108
112
  // not displaying modal menus (e.g., copy/paste or selection). The missing menus are due to the
@@ -132,11 +136,7 @@ class Screen(
132
136
  val height = bottom - top
133
137
 
134
138
  if (isSheetFitToContents()) {
135
- sheetBehavior?.let {
136
- if (it.maxHeight != height) {
137
- it.maxHeight = height
138
- }
139
- }
139
+ sheetBehavior?.useSingleDetent(height)
140
140
 
141
141
  if (!BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
142
142
  // On old architecture we delay enter transition in order to wait for initial frame.
@@ -173,23 +173,31 @@ class Screen(
173
173
  r: Int,
174
174
  b: Int,
175
175
  ) {
176
- if (container is ScreenStack && changed) {
176
+ // In case of form sheet we get layout notification a bit later, in `onBottomSheetBehaviorDidLayout`
177
+ // after the attached behaviour laid out this view.
178
+ if (changed && isNativeStackScreen && !usesFormSheetPresentation()) {
177
179
  val width = r - l
178
180
  val height = b - t
179
181
 
180
- if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
181
- updateScreenSizeFabric(width, height, t)
182
- } else {
183
- updateScreenSizePaper(width, height)
184
- }
182
+ dispatchShadowStateUpdate(width, height, t)
185
183
 
186
- footer?.onParentLayout(changed, l, t, r, b, container!!.height)
184
+ // FormSheet has no header in current model.
187
185
  notifyHeaderHeightChange(t)
186
+ }
187
+ }
188
188
 
189
- if (!BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
190
- maybeTriggerPostponedTransition()
191
- }
189
+ internal fun onBottomSheetBehaviorDidLayout(coordinatorLayoutDidChange: Boolean) {
190
+ if (!usesFormSheetPresentation()) {
191
+ return
192
+ }
193
+ if (coordinatorLayoutDidChange && isNativeStackScreen) {
194
+ dispatchShadowStateUpdate(width, height, top)
195
+ }
196
+ if (!BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
197
+ maybeTriggerPostponedTransition()
192
198
  }
199
+
200
+ footer?.onParentLayout(coordinatorLayoutDidChange, left, top, right, bottom, container!!.height)
193
201
  }
194
202
 
195
203
  private fun maybeTriggerPostponedTransition() {
@@ -214,6 +222,21 @@ class Screen(
214
222
  )
215
223
  }
216
224
 
225
+ /**
226
+ * @param offsetY ignored on old architecture
227
+ */
228
+ private fun dispatchShadowStateUpdate(
229
+ width: Int,
230
+ height: Int,
231
+ offsetY: Int,
232
+ ) {
233
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
234
+ updateScreenSizeFabric(width, height, offsetY)
235
+ } else {
236
+ updateScreenSizePaper(width, height)
237
+ }
238
+ }
239
+
217
240
  val headerConfig: ScreenStackHeaderConfig?
218
241
  get() = children.find { it is ScreenStackHeaderConfig } as? ScreenStackHeaderConfig
219
242
 
@@ -275,7 +298,7 @@ class Screen(
275
298
  throw IllegalStateException("[RNScreens] activityState can only progress in NativeStack")
276
299
  }
277
300
  this.activityState = activityState
278
- container?.notifyChildUpdate()
301
+ container?.onChildUpdate()
279
302
  }
280
303
 
281
304
  fun setScreenOrientation(screenOrientation: String?) {
@@ -482,7 +505,19 @@ class Screen(
482
505
  ?.dispatchEvent(HeaderHeightChangeEvent(surfaceId, id, headerHeight))
483
506
  }
484
507
 
485
- internal fun notifySheetDetentChange(
508
+ internal fun onSheetDetentChanged(
509
+ detentIndex: Int,
510
+ isStable: Boolean,
511
+ ) {
512
+ dispatchSheetDetentChanged(detentIndex, isStable)
513
+ // There is no need to update shadow state for transient sheet states -
514
+ // we are unsure of the exact sheet position anyway.
515
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED && isStable) {
516
+ updateScreenSizeFabric(width, height, top)
517
+ }
518
+ }
519
+
520
+ private fun dispatchSheetDetentChanged(
486
521
  detentIndex: Int,
487
522
  isStable: Boolean,
488
523
  ) {
@@ -571,3 +606,5 @@ class Screen(
571
606
  const val SHEET_FIT_TO_CONTENTS = -1.0
572
607
  }
573
608
  }
609
+
610
+ 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,30 +167,34 @@ 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
- // Scope function to limit the scope of locals.
107
- run {
108
- val notDismissedWrappers =
109
- screenWrappers
110
- .asReversed()
111
- .asSequence()
112
- .filter { !dismissedWrappers.contains(it) && it.screen.activityState !== Screen.ActivityState.INACTIVE }
113
-
114
- newTop = notDismissedWrappers.firstOrNull()
115
- visibleBottom =
116
- notDismissedWrappers
117
- .dropWhile { it.screen.isTransparent() }
118
- .firstOrNull()
119
- ?.takeUnless { it === newTop }
120
- }
178
+ val notDismissedWrappers =
179
+ screenWrappers
180
+ .asReversed()
181
+ .asSequence()
182
+ .filter { !dismissedWrappers.contains(it) && it.screen.activityState !== Screen.ActivityState.INACTIVE }
183
+
184
+ newTop = notDismissedWrappers.firstOrNull()
185
+ visibleBottom =
186
+ notDismissedWrappers
187
+ .dropWhile { it.screen.isTransparent() }
188
+ .firstOrNull()
189
+ ?.takeUnless { it === newTop }
121
190
 
122
191
  var shouldUseOpenAnimation = true
123
192
  var stackAnimation: StackAnimation? = null
124
- if (newTop != null && !stack.contains(newTop)) {
193
+
194
+ val newTopAlreadyInStack = stack.contains(newTop)
195
+ val topScreenWillChange = newTop !== topScreenWrapper
196
+
197
+ if (newTop != null && !newTopAlreadyInStack) {
125
198
  // if new top screen wasn't on stack we do "open animation" so long it is not the very first
126
199
  // screen on stack
127
200
  if (topScreenWrapper != null) {
@@ -129,9 +202,9 @@ class ScreenStack(
129
202
  // if the previous top screen does not exist anymore and the new top was not on the stack
130
203
  // before, probably replace or reset was called, so we play the "close animation".
131
204
  // Otherwise it's open animation
132
- val containsTopScreen = topScreenWrapper?.let { screenWrappers.contains(it) } == true
205
+ val previousTopScreenRemainsInStack = topScreenWrapper?.let { screenWrappers.contains(it) } == true
133
206
  val isPushReplace = newTop.screen.replaceAnimation === Screen.ReplaceAnimation.PUSH
134
- shouldUseOpenAnimation = containsTopScreen || isPushReplace
207
+ shouldUseOpenAnimation = previousTopScreenRemainsInStack || isPushReplace
135
208
  // if the replace animation is `push`, the new top screen provides the animation, otherwise the previous one
136
209
  stackAnimation = if (shouldUseOpenAnimation) newTop.screen.stackAnimation else topScreenWrapper?.screen?.stackAnimation
137
210
  } else {
@@ -140,7 +213,7 @@ class ScreenStack(
140
213
  stackAnimation = StackAnimation.NONE
141
214
  goingForward = true
142
215
  }
143
- } else if (newTop != null && topScreenWrapper != null && topScreenWrapper !== newTop) {
216
+ } else if (newTop != null && topScreenWrapper != null && topScreenWillChange) {
144
217
  // otherwise if we are performing top screen change we do "close animation"
145
218
  shouldUseOpenAnimation = false
146
219
  stackAnimation = topScreenWrapper?.screen?.stackAnimation
@@ -160,8 +233,29 @@ class ScreenStack(
160
233
  // appears on top of the previous one. You can read more about in the comment
161
234
  // for the code we use to change that behavior:
162
235
  // 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
236
+ // Note: This should not be set in case there is only a single screen in stack or animation `none` is used.
237
+ // Atm needsDrawReordering implementation guards that assuming that first screen on stack uses `NONE` animation.
238
+ childDrawingOrderStrategy = SwapLastTwo()
239
+ } else if (newTop != null &&
240
+ newTopAlreadyInStack &&
241
+ topScreenWrapper?.screen?.isTransparent() == true &&
242
+ newTop.screen.isTransparent() == false
243
+ ) {
244
+ // In case where we dismiss multiple transparent views we want to ensure
245
+ // that they are drawn in correct order - Android swaps them by default,
246
+ // so we need to swap the swap to unswap :D
247
+ val dismissedTransparentScreenApproxCount =
248
+ stack
249
+ .asReversed()
250
+ .asSequence()
251
+ .takeWhile {
252
+ it !== newTop &&
253
+ it.screen.isTransparent()
254
+ }.count()
255
+ if (dismissedTransparentScreenApproxCount > 1) {
256
+ childDrawingOrderStrategy =
257
+ ReverseOrderInRange(max(stack.lastIndex - dismissedTransparentScreenApproxCount + 1, 0)..stack.lastIndex)
258
+ }
165
259
  }
166
260
 
167
261
  createTransaction().let { transaction ->
@@ -241,23 +335,6 @@ class ScreenStack(
241
335
  stack.forEach { it.onContainerUpdate() }
242
336
  }
243
337
 
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
338
  private fun drawAndRelease() {
262
339
  // We make a copy of the drawingOps and use it to dispatch draws in order to be sure
263
340
  // that we do not modify the original list. There are cases when `op.draw` can call
@@ -276,12 +353,12 @@ class ScreenStack(
276
353
 
277
354
  // check the view removal is completed (by comparing the previous children count)
278
355
  if (drawingOps.size < previousChildrenCount) {
279
- reverseLastTwoChildren = false
356
+ childDrawingOrderStrategy = null
280
357
  }
281
358
  previousChildrenCount = drawingOps.size
282
- if (reverseLastTwoChildren && drawingOps.size >= 2) {
283
- Collections.swap(drawingOps, drawingOps.size - 1, drawingOps.size - 2)
284
- }
359
+
360
+ childDrawingOrderStrategy?.apply(drawingOps)
361
+
285
362
  drawAndRelease()
286
363
  }
287
364
 
@@ -310,7 +387,7 @@ class ScreenStack(
310
387
  // See: https://developer.android.com/about/versions/15/behavior-changes-15?hl=en#openjdk-api-changes
311
388
  private fun obtainDrawingOp(): DrawingOp = if (drawingOpPool.isEmpty()) DrawingOp() else drawingOpPool.removeAt(drawingOpPool.lastIndex)
312
389
 
313
- private inner class DrawingOp {
390
+ internal inner class DrawingOp {
314
391
  var canvas: Canvas? = null
315
392
  var child: View? = null
316
393
  var drawingTime: Long = 0