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.
- package/RNScreens.podspec +2 -2
- package/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt +3 -8
- package/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt +9 -6
- package/android/src/fabric/java/com/swmansion/rnscreens/NativeProxy.kt +2 -1
- package/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt +111 -15
- package/android/src/main/java/com/swmansion/rnscreens/Screen.kt +54 -17
- package/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt +1 -1
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt +125 -48
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +46 -286
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +94 -54
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigShadowNode.kt +3 -0
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.kt +19 -2
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackViewManager.kt +2 -1
- package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt +262 -3
- package/android/src/main/java/com/swmansion/rnscreens/ext/ViewExt.kt +2 -0
- package/android/src/main/java/com/swmansion/rnscreens/utils/InsetsKt.kt +31 -0
- package/android/src/main/java/com/swmansion/rnscreens/utils/PaddingBundle.kt +1 -0
- package/android/src/paper/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt +14 -5
- package/android/src/versioned/pointerevents/77/com/swmansion/rnscreens/{ScreensCoordinatorLayoutPointerEventsImpl.kt → PointerEventsBoxNoneImpl.kt} +1 -1
- package/android/src/versioned/pointerevents/latest/com/swmansion/rnscreens/{ScreensCoordinatorLayoutPointerEventsImpl.kt → PointerEventsBoxNoneImpl.kt} +1 -1
- package/common/cpp/react/renderer/components/rnscreens/RNSModalScreenShadowNode.cpp +1 -3
- package/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h +7 -3
- package/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewComponentDescriptor.h +1 -1
- package/cpp/RNSScreenRemovalListener.cpp +3 -1
- package/ios/RNSFullWindowOverlay.mm +6 -6
- package/ios/RNSScreen.mm +12 -0
- package/ios/RNSScreenStackHeaderConfig.h +1 -1
- package/ios/RNSScreenStackHeaderConfig.mm +27 -22
- package/lib/commonjs/components/Screen.js +13 -4
- package/lib/commonjs/components/Screen.js.map +1 -1
- package/lib/module/components/Screen.js +13 -4
- package/lib/module/components/Screen.js.map +1 -1
- package/lib/typescript/components/Screen.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Screen.tsx +23 -13
- 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
|
-
|
|
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 =>
|
|
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
|
package/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt
CHANGED
|
@@ -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
|
|
18
|
-
private var
|
|
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(
|
|
46
|
-
abs(
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
+
hasSizeChanged: Boolean,
|
|
64
149
|
l: Int,
|
|
65
150
|
t: Int,
|
|
66
151
|
r: Int,
|
|
67
152
|
b: Int,
|
|
68
153
|
) {
|
|
69
|
-
super.onLayout(
|
|
154
|
+
super.onLayout(hasSizeChanged, l, t, r, b)
|
|
70
155
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
156
|
+
config.onNativeToolbarLayout(
|
|
157
|
+
this,
|
|
158
|
+
hasSizeChanged || isForceShadowStateUpdateOnLayoutRequested,
|
|
159
|
+
)
|
|
160
|
+
isForceShadowStateUpdateOnLayoutRequested = false
|
|
161
|
+
}
|
|
74
162
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
updateScreenSizeFabric(width, height, t)
|
|
182
|
-
} else {
|
|
183
|
-
updateScreenSizePaper(width, height)
|
|
184
|
-
}
|
|
182
|
+
dispatchShadowStateUpdate(width, height, t)
|
|
185
183
|
|
|
186
|
-
|
|
184
|
+
// FormSheet has no header in current model.
|
|
187
185
|
notifyHeaderHeightChange(t)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
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?.
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
205
|
+
val previousTopScreenRemainsInStack = topScreenWrapper?.let { screenWrappers.contains(it) } == true
|
|
133
206
|
val isPushReplace = newTop.screen.replaceAnimation === Screen.ReplaceAnimation.PUSH
|
|
134
|
-
shouldUseOpenAnimation =
|
|
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 &&
|
|
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.
|
|
164
|
-
|
|
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
|
-
|
|
356
|
+
childDrawingOrderStrategy = null
|
|
280
357
|
}
|
|
281
358
|
previousChildrenCount = drawingOps.size
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
390
|
+
internal inner class DrawingOp {
|
|
314
391
|
var canvas: Canvas? = null
|
|
315
392
|
var child: View? = null
|
|
316
393
|
var drawingTime: Long = 0
|