react-native-screens 4.25.0 → 4.25.1
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/android/src/main/java/com/swmansion/rnscreens/Screen.kt +5 -115
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +12 -0
- package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetAnimationCoordinator.kt +364 -0
- package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt +8 -157
- package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetUtils.kt +16 -0
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt +44 -0
- package/package.json +1 -1
|
@@ -26,9 +26,11 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
|
26
26
|
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
|
+
import com.swmansion.rnscreens.bottomsheet.SheetAnimationCoordinator
|
|
29
30
|
import com.swmansion.rnscreens.bottomsheet.SheetDetents
|
|
30
31
|
import com.swmansion.rnscreens.bottomsheet.fitToContentsSheetHeight
|
|
31
32
|
import com.swmansion.rnscreens.bottomsheet.isSheetFitToContents
|
|
33
|
+
import com.swmansion.rnscreens.bottomsheet.resolveClampedHeight
|
|
32
34
|
import com.swmansion.rnscreens.bottomsheet.updateMetrics
|
|
33
35
|
import com.swmansion.rnscreens.bottomsheet.useSingleDetent
|
|
34
36
|
import com.swmansion.rnscreens.bottomsheet.usesFormSheetPresentation
|
|
@@ -50,6 +52,8 @@ class Screen(
|
|
|
50
52
|
val sheetBehavior: BottomSheetBehavior<Screen>?
|
|
51
53
|
get() = (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior as? BottomSheetBehavior<Screen>
|
|
52
54
|
|
|
55
|
+
internal val sheetAnimationCoordinator: SheetAnimationCoordinator by lazy { SheetAnimationCoordinator(this) }
|
|
56
|
+
|
|
53
57
|
val reactEventDispatcher: EventDispatcher?
|
|
54
58
|
get() = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
|
|
55
59
|
|
|
@@ -155,7 +159,7 @@ class Screen(
|
|
|
155
159
|
if (isInitial) {
|
|
156
160
|
setupInitialSheetContentHeight(sheetBehavior, height)
|
|
157
161
|
} else if (sheetDefaultResizeAnimationEnabled) {
|
|
158
|
-
updateSheetContentHeightWithAnimation(sheetBehavior, oldHeight, height)
|
|
162
|
+
sheetAnimationCoordinator.updateSheetContentHeightWithAnimation(sheetBehavior, oldHeight, height)
|
|
159
163
|
} else {
|
|
160
164
|
updateSheetContentHeightWithoutAnimation(sheetBehavior, height)
|
|
161
165
|
}
|
|
@@ -171,104 +175,6 @@ class Screen(
|
|
|
171
175
|
updateState(width, height, headerHeight)
|
|
172
176
|
}
|
|
173
177
|
|
|
174
|
-
/**
|
|
175
|
-
* This should be used only with sheet in `fitToContents` mode.
|
|
176
|
-
*/
|
|
177
|
-
private fun updateSheetContentHeightWithAnimation(
|
|
178
|
-
behavior: BottomSheetBehavior<Screen>,
|
|
179
|
-
oldHeight: Int,
|
|
180
|
-
newHeight: Int,
|
|
181
|
-
) {
|
|
182
|
-
val currentTranslationY = this.translationY
|
|
183
|
-
|
|
184
|
-
/*
|
|
185
|
-
* WHY OVERFLOW MATTERS:
|
|
186
|
-
* BottomSheetBehavior has a physical limit (maxHeight) defined by the parent container.
|
|
187
|
-
* If the new content height exceeds this limit (by its size or keyboard offset), simply
|
|
188
|
-
* animating translationY back to 'currentTranslationY' would attempt to render the sheet
|
|
189
|
-
* larger than the screen.
|
|
190
|
-
*
|
|
191
|
-
* We need to have constraint height inside the container's bounds.
|
|
192
|
-
* By including this overflow to our animation, we ensure the sheet stops
|
|
193
|
-
* expanding exactly at the maxHeight, preventing from being pushed
|
|
194
|
-
* off-screen or causing layout synchronization issues with the CoordinatorLayout.
|
|
195
|
-
*/
|
|
196
|
-
val clampedOldHeight = resolveClampedHeight(oldHeight, currentTranslationY)
|
|
197
|
-
val clampedNewHeight = resolveClampedHeight(newHeight, currentTranslationY)
|
|
198
|
-
val visibleDelta = (clampedNewHeight - clampedOldHeight).toFloat()
|
|
199
|
-
|
|
200
|
-
if (visibleDelta == 0f) return
|
|
201
|
-
|
|
202
|
-
val isContentExpanding = visibleDelta > 0
|
|
203
|
-
|
|
204
|
-
if (isContentExpanding) {
|
|
205
|
-
/*
|
|
206
|
-
* Expanding content animation:
|
|
207
|
-
*
|
|
208
|
-
* Before animation, we're updating the SheetBehavior - the maximum height is the new
|
|
209
|
-
* content height, then we're forcing a layout pass. This ensures the view calculates
|
|
210
|
-
* with its new bounds when the animation starts.
|
|
211
|
-
*
|
|
212
|
-
* In the animation, we're translating the Screen back to it's (newly calculated) origin
|
|
213
|
-
* position, providing an impression that FormSheet expands. It already has the final size,
|
|
214
|
-
* but some content is not yet visible on the screen.
|
|
215
|
-
*
|
|
216
|
-
* After animation, we just need to send a notification that ShadowTree state should be updated,
|
|
217
|
-
* as the positioning of pressables has changed due to the Y translation manipulation.
|
|
218
|
-
*/
|
|
219
|
-
this.translationY += visibleDelta
|
|
220
|
-
this
|
|
221
|
-
.animate()
|
|
222
|
-
.translationY(currentTranslationY)
|
|
223
|
-
.withStartAction {
|
|
224
|
-
behavior.updateMetrics(clampedNewHeight)
|
|
225
|
-
layout(this.left, this.bottom - clampedNewHeight, this.right, this.bottom)
|
|
226
|
-
}.withEndAction {
|
|
227
|
-
// Force a layout pass on the CoordinatorLayout to synchronize BottomSheetBehavior's
|
|
228
|
-
// internal offsets with the new maxHeight. This prevents the sheet from snapping back
|
|
229
|
-
// to its old position when the user starts a gesture.
|
|
230
|
-
parent.requestLayout()
|
|
231
|
-
onSheetYTranslationChanged()
|
|
232
|
-
}.start()
|
|
233
|
-
} else {
|
|
234
|
-
/*
|
|
235
|
-
* Shrinking content animation:
|
|
236
|
-
*
|
|
237
|
-
* Before the animation, our Screen translationY is 0 - because its actual layout and visual position are equal.
|
|
238
|
-
*
|
|
239
|
-
* Before the animation, I'm updating sheet metrics to the target value - it won't update until the next layout pass,
|
|
240
|
-
* which is controlled by end action. This is done deliberately, to allow catching the case when quick combination
|
|
241
|
-
* of shrink & expand animation is detected.
|
|
242
|
-
*
|
|
243
|
-
* In the animation, we're translating the Screen down by the calculated height delta to the position (which will
|
|
244
|
-
* be new absolute 0 for the Screen, after ending the transition), providing an impression that FormSheet shrinks.
|
|
245
|
-
* FormSheet's size remains unchanged during the whole animation, therefore there is no view clipping.
|
|
246
|
-
*
|
|
247
|
-
* After animation, we can update the layout: the maximum FormSheet height is updated and we're forcing
|
|
248
|
-
* another layout pass. Additionally, since the actual layout and the target position are equal,
|
|
249
|
-
* we can reset translationY to 0.
|
|
250
|
-
*
|
|
251
|
-
* After animation, we need to send a notification that ShadowTree state should be updated,
|
|
252
|
-
* as the FormSheet size has changed and the positioning of pressables has changed due to the Y translation manipulation.
|
|
253
|
-
*/
|
|
254
|
-
val targetTranslationY = currentTranslationY - visibleDelta
|
|
255
|
-
this
|
|
256
|
-
.animate()
|
|
257
|
-
.translationY(targetTranslationY)
|
|
258
|
-
.withStartAction {
|
|
259
|
-
behavior.updateMetrics(clampedNewHeight)
|
|
260
|
-
}.withEndAction {
|
|
261
|
-
layout(this.left, this.bottom - clampedNewHeight, this.right, this.bottom)
|
|
262
|
-
this.translationY = currentTranslationY
|
|
263
|
-
// Force a layout pass on the CoordinatorLayout to synchronize BottomSheetBehavior's
|
|
264
|
-
// internal offsets with the new maxHeight. This prevents the sheet from snapping back
|
|
265
|
-
// to its old position when the user starts a gesture.
|
|
266
|
-
parent.requestLayout()
|
|
267
|
-
onSheetYTranslationChanged()
|
|
268
|
-
}.start()
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
178
|
private fun updateSheetContentHeightWithoutAnimation(
|
|
273
179
|
behavior: BottomSheetBehavior<Screen>,
|
|
274
180
|
height: Int,
|
|
@@ -299,22 +205,6 @@ class Screen(
|
|
|
299
205
|
requestLayout()
|
|
300
206
|
}
|
|
301
207
|
|
|
302
|
-
private fun resolveClampedHeight(
|
|
303
|
-
targetHeight: Int,
|
|
304
|
-
currentTranslationY: Float,
|
|
305
|
-
): Int {
|
|
306
|
-
val maxAvailableVerticalSpace =
|
|
307
|
-
this.fragment
|
|
308
|
-
?.asScreenStackFragment()
|
|
309
|
-
?.sheetDelegate
|
|
310
|
-
?.tryResolveMaxFormSheetHeight() ?: return targetHeight
|
|
311
|
-
|
|
312
|
-
// Please note that currentTranslationY is rather < 0 here.
|
|
313
|
-
// The translation is included in constraining the available space, because the FormSheet can have some offset, e.g. to
|
|
314
|
-
// avoid the keyboard.
|
|
315
|
-
return targetHeight.coerceAtMost((maxAvailableVerticalSpace + currentTranslationY).toInt())
|
|
316
|
-
}
|
|
317
|
-
|
|
318
208
|
fun registerLayoutCallbackForWrapper(wrapper: ScreenContentWrapper) {
|
|
319
209
|
wrapper.delegate = this
|
|
320
210
|
}
|
|
@@ -270,6 +270,14 @@ class ScreenStackFragment :
|
|
|
270
270
|
object : WindowInsetsAnimationCompat.Callback(
|
|
271
271
|
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
|
|
272
272
|
) {
|
|
273
|
+
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
|
|
274
|
+
super.onPrepare(animation)
|
|
275
|
+
|
|
276
|
+
if ((animation.typeMask and WindowInsetsCompat.Type.ime()) != 0) {
|
|
277
|
+
sheetDelegate.notifyKeyboardAnimationStart()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
273
281
|
// Replace InsetsAnimationCallback created by BottomSheetBehavior
|
|
274
282
|
// to avoid interfering with custom animations.
|
|
275
283
|
// See: https://github.com/software-mansion/react-native-screens/pull/2909
|
|
@@ -288,6 +296,10 @@ class ScreenStackFragment :
|
|
|
288
296
|
override fun onEnd(animation: WindowInsetsAnimationCompat) {
|
|
289
297
|
super.onEnd(animation)
|
|
290
298
|
|
|
299
|
+
if ((animation.typeMask and WindowInsetsCompat.Type.ime()) != 0) {
|
|
300
|
+
sheetDelegate.notifyKeyboardAnimationEnd()
|
|
301
|
+
}
|
|
302
|
+
|
|
291
303
|
screen.onSheetYTranslationChanged()
|
|
292
304
|
}
|
|
293
305
|
}
|
package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetAnimationCoordinator.kt
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
package com.swmansion.rnscreens.bottomsheet
|
|
2
|
+
|
|
3
|
+
import android.animation.Animator
|
|
4
|
+
import android.animation.AnimatorListenerAdapter
|
|
5
|
+
import android.animation.AnimatorSet
|
|
6
|
+
import android.animation.ValueAnimator
|
|
7
|
+
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
8
|
+
import androidx.core.view.WindowInsetsCompat
|
|
9
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
10
|
+
import com.swmansion.rnscreens.Screen
|
|
11
|
+
import com.swmansion.rnscreens.ScreenStackFragment
|
|
12
|
+
import com.swmansion.rnscreens.events.ScreenAnimationDelegate
|
|
13
|
+
import com.swmansion.rnscreens.events.ScreenEventEmitter
|
|
14
|
+
import com.swmansion.rnscreens.ext.asScreenStackFragment
|
|
15
|
+
import com.swmansion.rnscreens.transition.ExternalBoundaryValuesEvaluator
|
|
16
|
+
import java.lang.ref.WeakReference
|
|
17
|
+
|
|
18
|
+
internal class SheetAnimationCoordinator(
|
|
19
|
+
screen: Screen,
|
|
20
|
+
) {
|
|
21
|
+
private val screenRef = WeakReference(screen)
|
|
22
|
+
private val screen: Screen get() =
|
|
23
|
+
checkNotNull(screenRef.get()) {
|
|
24
|
+
"[RNScreens] Screen has been destroyed and shouldn't be the subject of any animations"
|
|
25
|
+
}
|
|
26
|
+
private var activeKeyboardAnimationsCount: Int = 0
|
|
27
|
+
private val isKeyboardAnimationInProgress: Boolean
|
|
28
|
+
get() = activeKeyboardAnimationsCount > 0
|
|
29
|
+
private var isSheetAnimationInProgress: Boolean = false
|
|
30
|
+
private var currentContentAnimator: ValueAnimator? = null
|
|
31
|
+
|
|
32
|
+
private var lastKeyboardBottomOffset: Int = 0
|
|
33
|
+
|
|
34
|
+
internal fun createSheetEnterAnimator(sheetAnimationContext: SheetDelegate.SheetAnimationContext): Animator {
|
|
35
|
+
val animatorSet = AnimatorSet()
|
|
36
|
+
|
|
37
|
+
val dimmingDelegate = sheetAnimationContext.dimmingDelegate
|
|
38
|
+
val screenStackFragment = sheetAnimationContext.fragment
|
|
39
|
+
|
|
40
|
+
val alphaAnimator = createDimmingViewAlphaAnimator(0f, dimmingDelegate.maxAlpha, dimmingDelegate)
|
|
41
|
+
val slideAnimator = createSheetSlideInAnimator()
|
|
42
|
+
|
|
43
|
+
animatorSet
|
|
44
|
+
.play(slideAnimator)
|
|
45
|
+
.takeIf {
|
|
46
|
+
dimmingDelegate.willDimForDetentIndex(screen, screen.sheetInitialDetentIndex)
|
|
47
|
+
}?.with(alphaAnimator)
|
|
48
|
+
|
|
49
|
+
attachCommonListeners(animatorSet, isEnter = true, screenStackFragment)
|
|
50
|
+
|
|
51
|
+
return animatorSet
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
internal fun createSheetExitAnimator(sheetAnimationContext: SheetDelegate.SheetAnimationContext): Animator {
|
|
55
|
+
val animatorSet = AnimatorSet()
|
|
56
|
+
|
|
57
|
+
val coordinatorLayout = sheetAnimationContext.coordinatorLayout
|
|
58
|
+
val dimmingDelegate = sheetAnimationContext.dimmingDelegate
|
|
59
|
+
val screenStackFragment = sheetAnimationContext.fragment
|
|
60
|
+
|
|
61
|
+
val alphaAnimator =
|
|
62
|
+
createDimmingViewAlphaAnimator(dimmingDelegate.dimmingView.alpha, 0f, dimmingDelegate)
|
|
63
|
+
val slideAnimator = createSheetSlideOutAnimator(coordinatorLayout)
|
|
64
|
+
|
|
65
|
+
animatorSet.play(alphaAnimator).with(slideAnimator)
|
|
66
|
+
|
|
67
|
+
attachCommonListeners(animatorSet, isEnter = false, screenStackFragment)
|
|
68
|
+
|
|
69
|
+
return animatorSet
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* This should be used only with sheet in `fitToContents` mode.
|
|
74
|
+
*
|
|
75
|
+
* If an entry/exit animation is already in progress, we silently update the
|
|
76
|
+
* behavior metrics and view layout without starting a competing translationY
|
|
77
|
+
* animation - this is the fix for the race condition between the slide-in
|
|
78
|
+
* ValueAnimator and externally triggered layout pass (e.g. applying padding from SAV insets).
|
|
79
|
+
*/
|
|
80
|
+
internal fun updateSheetContentHeightWithAnimation(
|
|
81
|
+
behavior: BottomSheetBehavior<Screen>,
|
|
82
|
+
oldHeight: Int,
|
|
83
|
+
newHeight: Int,
|
|
84
|
+
) {
|
|
85
|
+
val currentTranslationY = screen.translationY
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* WHY OVERFLOW MATTERS:
|
|
89
|
+
* BottomSheetBehavior has a physical limit (maxHeight) defined by the parent container.
|
|
90
|
+
* If the new content height exceeds this limit (by its size or keyboard offset), simply
|
|
91
|
+
* animating translationY back to 'currentTranslationY' would attempt to render the sheet
|
|
92
|
+
* larger than the screen.
|
|
93
|
+
*
|
|
94
|
+
* We need to constrain the height within the container's bounds.
|
|
95
|
+
* By including this overflow to our animation, we ensure the sheet stops
|
|
96
|
+
* expanding exactly at the maxHeight, preventing from being pushed
|
|
97
|
+
* off-screen or causing layout synchronization issues with the CoordinatorLayout.
|
|
98
|
+
*/
|
|
99
|
+
val clampedOldHeight = screen.resolveClampedHeight(oldHeight, currentTranslationY)
|
|
100
|
+
val clampedNewHeight = screen.resolveClampedHeight(newHeight, currentTranslationY)
|
|
101
|
+
|
|
102
|
+
// If an entry/exit animation or a keyboard animation is in progress - it owns
|
|
103
|
+
// translationY writes. Then when the content size is changing, we silently
|
|
104
|
+
// update behavior metrics and re-layout so the ongoing slide animation
|
|
105
|
+
// lands at the correct final geometry, without firing a competing animation.
|
|
106
|
+
if (isSheetAnimationInProgress || isKeyboardAnimationInProgress) {
|
|
107
|
+
behavior.updateMetrics(clampedNewHeight)
|
|
108
|
+
screen.layoutBottomSheetAtHeight(clampedNewHeight)
|
|
109
|
+
screen.finalizeBottomSheetLayoutUpdates()
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
val visibleDelta = (clampedNewHeight - clampedOldHeight).toFloat()
|
|
114
|
+
if (visibleDelta == 0f) return
|
|
115
|
+
|
|
116
|
+
if (visibleDelta > 0) {
|
|
117
|
+
animateContentExpanding(behavior, clampedNewHeight, currentTranslationY, visibleDelta)
|
|
118
|
+
} else {
|
|
119
|
+
animateContentShrinking(behavior, clampedNewHeight, currentTranslationY, visibleDelta)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/*
|
|
124
|
+
* Expanding content animation:
|
|
125
|
+
*
|
|
126
|
+
* Before animation, we're updating the SheetBehavior - the maximum height is the new
|
|
127
|
+
* content height, then we're forcing a layout pass. This ensures the view calculates
|
|
128
|
+
* with its new bounds when the animation starts.
|
|
129
|
+
*
|
|
130
|
+
* In the animation, we're translating the Screen back to its (newly calculated) origin
|
|
131
|
+
* position, providing an impression that FormSheet expands. It already has the final size,
|
|
132
|
+
* but some content is not yet visible on the screen.
|
|
133
|
+
*
|
|
134
|
+
* After animation, we just need to send a notification that ShadowTree state should be updated,
|
|
135
|
+
* as the positioning of pressables has changed due to the Y translation manipulation.
|
|
136
|
+
*/
|
|
137
|
+
private fun animateContentExpanding(
|
|
138
|
+
behavior: BottomSheetBehavior<Screen>,
|
|
139
|
+
clampedNewHeight: Int,
|
|
140
|
+
currentTranslationY: Float,
|
|
141
|
+
visibleDelta: Float,
|
|
142
|
+
) {
|
|
143
|
+
screen.translationY += visibleDelta
|
|
144
|
+
cancelCurrentContentAnimation()
|
|
145
|
+
currentContentAnimator =
|
|
146
|
+
ValueAnimator.ofFloat(screen.translationY, currentTranslationY).apply {
|
|
147
|
+
addListener(
|
|
148
|
+
object : AnimatorListenerAdapter() {
|
|
149
|
+
override fun onAnimationStart(animation: Animator) {
|
|
150
|
+
behavior.updateMetrics(clampedNewHeight)
|
|
151
|
+
screen.layoutBottomSheetAtHeight(clampedNewHeight)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
155
|
+
currentContentAnimator = null
|
|
156
|
+
screen.finalizeBottomSheetLayoutUpdates()
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
addUpdateListener { screen.translationY = it.animatedValue as Float }
|
|
161
|
+
start()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/*
|
|
166
|
+
* Shrinking content animation:
|
|
167
|
+
*
|
|
168
|
+
* Before the animation, our Screen translationY is 0 - because its actual layout and visual position are equal.
|
|
169
|
+
*
|
|
170
|
+
* Before the animation, I'm updating sheet metrics to the target value - it won't update until the next layout pass,
|
|
171
|
+
* which is controlled by end action. This is done deliberately, to allow catching the case when quick combination
|
|
172
|
+
* of shrink & expand animation is detected.
|
|
173
|
+
*
|
|
174
|
+
* In the animation, we're translating the Screen down by the calculated height delta to the position (which will
|
|
175
|
+
* be new absolute 0 for the Screen, after ending the transition), providing an impression that FormSheet shrinks.
|
|
176
|
+
* FormSheet's size remains unchanged during the whole animation, therefore there is no view clipping.
|
|
177
|
+
*
|
|
178
|
+
* After animation, we can update the layout: the maximum FormSheet height is updated and we're forcing
|
|
179
|
+
* another layout pass. Additionally, since the actual layout and the target position are equal,
|
|
180
|
+
* we can reset translationY to 0.
|
|
181
|
+
*
|
|
182
|
+
* After animation, we need to send a notification that ShadowTree state should be updated,
|
|
183
|
+
* as the FormSheet size has changed and the positioning of pressables has changed due to the Y translation manipulation.
|
|
184
|
+
*/
|
|
185
|
+
private fun animateContentShrinking(
|
|
186
|
+
behavior: BottomSheetBehavior<Screen>,
|
|
187
|
+
clampedNewHeight: Int,
|
|
188
|
+
currentTranslationY: Float,
|
|
189
|
+
visibleDelta: Float,
|
|
190
|
+
) {
|
|
191
|
+
val targetTranslationY = currentTranslationY - visibleDelta
|
|
192
|
+
cancelCurrentContentAnimation()
|
|
193
|
+
currentContentAnimator =
|
|
194
|
+
ValueAnimator.ofFloat(currentTranslationY, targetTranslationY).apply {
|
|
195
|
+
addListener(
|
|
196
|
+
object : AnimatorListenerAdapter() {
|
|
197
|
+
override fun onAnimationStart(animation: Animator) {
|
|
198
|
+
behavior.updateMetrics(clampedNewHeight)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
202
|
+
currentContentAnimator = null
|
|
203
|
+
screen.layoutBottomSheetAtHeight(clampedNewHeight)
|
|
204
|
+
screen.translationY = currentTranslationY
|
|
205
|
+
screen.finalizeBottomSheetLayoutUpdates()
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
addUpdateListener { screen.translationY = it.animatedValue as Float }
|
|
210
|
+
start()
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
internal fun notifyKeyboardAnimationStart() {
|
|
215
|
+
activeKeyboardAnimationsCount++
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
internal fun notifyKeyboardAnimationEnd() {
|
|
219
|
+
activeKeyboardAnimationsCount = maxOf(0, activeKeyboardAnimationsCount - 1)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
internal fun handleKeyboardInsetsProgress(insets: WindowInsetsCompat) {
|
|
223
|
+
lastKeyboardBottomOffset = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
224
|
+
// Prioritize enter/exit animations over direct keyboard inset reactions.
|
|
225
|
+
// We store the latest keyboard offset in `lastKeyboardBottomOffset`
|
|
226
|
+
// so that it can always be respected when applying translations in `updateSheetTranslationY`.
|
|
227
|
+
//
|
|
228
|
+
// This approach allows screen translation to be triggered from two sources, but without messing them together:
|
|
229
|
+
// - During enter/exit animations, while accounting for the keyboard height.
|
|
230
|
+
// - While interacting with a TextInput inside the bottom sheet, to handle keyboard show/hide events.
|
|
231
|
+
if (!isSheetAnimationInProgress) {
|
|
232
|
+
updateSheetTranslationY(0f)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// This function calculates the Y offset to which the FormSheet should animate
|
|
237
|
+
// when appearing (entering) or disappearing (exiting) with the on-screen keyboard (IME) present.
|
|
238
|
+
// Its purpose is to ensure the FormSheet does not exceed the top edge of the screen.
|
|
239
|
+
// It tries to display the FormSheet fully above the keyboard when there's enough space.
|
|
240
|
+
// Otherwise, it shifts the sheet as high as possible, even if it means part of its content
|
|
241
|
+
// will remain hidden behind the keyboard.
|
|
242
|
+
private fun computeSheetOffsetYWithIMEPresent(keyboardHeight: Int): Int {
|
|
243
|
+
val containerHeight =
|
|
244
|
+
screen.fragment
|
|
245
|
+
?.asScreenStackFragment()
|
|
246
|
+
?.sheetDelegate
|
|
247
|
+
?.tryResolveMaxFormSheetHeight()
|
|
248
|
+
check(containerHeight != null) {
|
|
249
|
+
"[RNScreens] Failed to find window height during bottom sheet behaviour configuration"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (screen.isSheetFitToContents()) {
|
|
253
|
+
val contentHeight = screen.contentWrapper?.height ?: 0
|
|
254
|
+
val offsetFromTop = maxOf(containerHeight - contentHeight, 0)
|
|
255
|
+
// If the content is higher than the Screen, offsetFromTop becomes negative.
|
|
256
|
+
// In such cases, we return 0 because a negative translation would shift the Screen
|
|
257
|
+
// to the bottom, which is not intended.
|
|
258
|
+
return minOf(offsetFromTop, keyboardHeight)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
val detents = screen.sheetDetents
|
|
262
|
+
|
|
263
|
+
val detentValue = detents.highest().coerceIn(0.0, 1.0)
|
|
264
|
+
val sheetHeight = (detentValue * containerHeight).toInt()
|
|
265
|
+
val offsetFromTop = containerHeight - sheetHeight
|
|
266
|
+
|
|
267
|
+
return minOf(offsetFromTop, keyboardHeight)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private fun updateSheetTranslationY(baseTranslationY: Float) {
|
|
271
|
+
val keyboardCorrection = lastKeyboardBottomOffset
|
|
272
|
+
val bottomOffset = computeSheetOffsetYWithIMEPresent(keyboardCorrection).toFloat()
|
|
273
|
+
|
|
274
|
+
screen.translationY = baseTranslationY - bottomOffset
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private fun createSheetSlideInAnimator(): ValueAnimator {
|
|
278
|
+
val startValueCallback = { _: Number? -> screen.height.toFloat() }
|
|
279
|
+
val evaluator = ExternalBoundaryValuesEvaluator(startValueCallback, { 0f })
|
|
280
|
+
|
|
281
|
+
return ValueAnimator.ofObject(evaluator, screen.height.toFloat(), 0f).apply {
|
|
282
|
+
addUpdateListener { updateSheetTranslationY(it.animatedValue as Float) }
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private fun createSheetSlideOutAnimator(coordinatorLayout: CoordinatorLayout): ValueAnimator {
|
|
287
|
+
val endValue = (coordinatorLayout.bottom - screen.top - screen.translationY)
|
|
288
|
+
|
|
289
|
+
return ValueAnimator.ofFloat(0f, endValue).apply {
|
|
290
|
+
addUpdateListener {
|
|
291
|
+
updateSheetTranslationY(it.animatedValue as Float)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private fun createDimmingViewAlphaAnimator(
|
|
297
|
+
from: Float,
|
|
298
|
+
to: Float,
|
|
299
|
+
dimmingDelegate: DimmingViewManager,
|
|
300
|
+
): ValueAnimator =
|
|
301
|
+
ValueAnimator.ofFloat(from, to).apply {
|
|
302
|
+
addUpdateListener { animator ->
|
|
303
|
+
(animator.animatedValue as? Float)?.let {
|
|
304
|
+
dimmingDelegate.dimmingView.alpha = it
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private fun cancelCurrentContentAnimation() {
|
|
310
|
+
currentContentAnimator?.removeAllListeners()
|
|
311
|
+
currentContentAnimator?.removeAllUpdateListeners()
|
|
312
|
+
currentContentAnimator?.cancel()
|
|
313
|
+
currentContentAnimator = null
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private fun Screen.layoutBottomSheetAtHeight(height: Int) = layout(left, bottom - height, right, bottom)
|
|
317
|
+
|
|
318
|
+
private fun Screen.finalizeBottomSheetLayoutUpdates() {
|
|
319
|
+
// Force a layout pass on the CoordinatorLayout to synchronize BottomSheetBehavior's
|
|
320
|
+
// internal offsets with the new maxHeight. This prevents the sheet from snapping back
|
|
321
|
+
// to its old position when the user starts a gesture.
|
|
322
|
+
parent.requestLayout()
|
|
323
|
+
|
|
324
|
+
// Notify that ShadowTree state should be updated, as the positioning of pressables
|
|
325
|
+
// has changed due to the Y translation manipulation.
|
|
326
|
+
onSheetYTranslationChanged()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private fun attachCommonListeners(
|
|
330
|
+
animatorSet: AnimatorSet,
|
|
331
|
+
isEnter: Boolean,
|
|
332
|
+
screenStackFragment: ScreenStackFragment,
|
|
333
|
+
) {
|
|
334
|
+
animatorSet.addListener(
|
|
335
|
+
ScreenAnimationDelegate(
|
|
336
|
+
screenStackFragment,
|
|
337
|
+
ScreenEventEmitter(screen),
|
|
338
|
+
if (isEnter) {
|
|
339
|
+
ScreenAnimationDelegate.AnimationType.ENTER
|
|
340
|
+
} else {
|
|
341
|
+
ScreenAnimationDelegate.AnimationType.EXIT
|
|
342
|
+
},
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
animatorSet.addListener(
|
|
347
|
+
object : AnimatorListenerAdapter() {
|
|
348
|
+
override fun onAnimationStart(animation: Animator) {
|
|
349
|
+
isSheetAnimationInProgress = true
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
override fun onAnimationCancel(animation: Animator) {
|
|
353
|
+
isSheetAnimationInProgress = false
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
357
|
+
isSheetAnimationInProgress = false
|
|
358
|
+
|
|
359
|
+
screen.onSheetYTranslationChanged()
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
package com.swmansion.rnscreens.bottomsheet
|
|
2
2
|
|
|
3
3
|
import android.animation.Animator
|
|
4
|
-
import android.animation.AnimatorListenerAdapter
|
|
5
|
-
import android.animation.AnimatorSet
|
|
6
|
-
import android.animation.ValueAnimator
|
|
7
4
|
import android.content.Context
|
|
8
5
|
import android.os.Build
|
|
9
6
|
import android.view.View
|
|
@@ -24,9 +21,6 @@ import com.swmansion.rnscreens.KeyboardState
|
|
|
24
21
|
import com.swmansion.rnscreens.KeyboardVisible
|
|
25
22
|
import com.swmansion.rnscreens.Screen
|
|
26
23
|
import com.swmansion.rnscreens.ScreenStackFragment
|
|
27
|
-
import com.swmansion.rnscreens.events.ScreenAnimationDelegate
|
|
28
|
-
import com.swmansion.rnscreens.events.ScreenEventEmitter
|
|
29
|
-
import com.swmansion.rnscreens.transition.ExternalBoundaryValuesEvaluator
|
|
30
24
|
import com.swmansion.rnscreens.utils.isSoftKeyboardVisibleOrNull
|
|
31
25
|
|
|
32
26
|
class SheetDelegate(
|
|
@@ -36,10 +30,7 @@ class SheetDelegate(
|
|
|
36
30
|
private var isKeyboardVisible: Boolean = false
|
|
37
31
|
private var keyboardState: KeyboardState = KeyboardNotVisible
|
|
38
32
|
|
|
39
|
-
private var isSheetAnimationInProgress: Boolean = false
|
|
40
|
-
|
|
41
33
|
private var lastTopInset: Int = 0
|
|
42
|
-
private var lastKeyboardBottomOffset: Int = 0
|
|
43
34
|
|
|
44
35
|
var lastStableDetentIndex: Int = screen.sheetInitialDetentIndex
|
|
45
36
|
private set
|
|
@@ -348,36 +339,6 @@ class SheetDelegate(
|
|
|
348
339
|
}
|
|
349
340
|
}
|
|
350
341
|
|
|
351
|
-
// This function calculates the Y offset to which the FormSheet should animate
|
|
352
|
-
// when appearing (entering) or disappearing (exiting) with the on-screen keyboard (IME) present.
|
|
353
|
-
// Its purpose is to ensure the FormSheet does not exceed the top edge of the screen.
|
|
354
|
-
// It tries to display the FormSheet fully above the keyboard when there's enough space.
|
|
355
|
-
// Otherwise, it shifts the sheet as high as possible, even if it means part of its content
|
|
356
|
-
// will remain hidden behind the keyboard.
|
|
357
|
-
internal fun computeSheetOffsetYWithIMEPresent(keyboardHeight: Int): Int {
|
|
358
|
-
val containerHeight = tryResolveMaxFormSheetHeight()
|
|
359
|
-
check(containerHeight != null) {
|
|
360
|
-
"[RNScreens] Failed to find window height during bottom sheet behaviour configuration"
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (screen.isSheetFitToContents()) {
|
|
364
|
-
val contentHeight = screen.contentWrapper?.height ?: 0
|
|
365
|
-
val offsetFromTop = maxOf(containerHeight - contentHeight, 0)
|
|
366
|
-
// If the content is higher than the Screen, offsetFromTop becomes negative.
|
|
367
|
-
// In such cases, we return 0 because a negative translation would shift the Screen
|
|
368
|
-
// to the bottom, which is not intended.
|
|
369
|
-
return minOf(offsetFromTop, keyboardHeight)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
val detents = screen.sheetDetents
|
|
373
|
-
|
|
374
|
-
val detentValue = detents.highest().coerceIn(0.0, 1.0)
|
|
375
|
-
val sheetHeight = (detentValue * containerHeight).toInt()
|
|
376
|
-
val offsetFromTop = containerHeight - sheetHeight
|
|
377
|
-
|
|
378
|
-
return minOf(offsetFromTop, keyboardHeight)
|
|
379
|
-
}
|
|
380
|
-
|
|
381
342
|
// This is listener function, not the view's.
|
|
382
343
|
override fun onApplyWindowInsets(
|
|
383
344
|
v: View,
|
|
@@ -473,128 +434,18 @@ class SheetDelegate(
|
|
|
473
434
|
|
|
474
435
|
// Sheet entering/exiting animations
|
|
475
436
|
|
|
476
|
-
internal fun createSheetEnterAnimator(sheetAnimationContext: SheetAnimationContext): Animator
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
val dimmingDelegate = sheetAnimationContext.dimmingDelegate
|
|
480
|
-
val screenStackFragment = sheetAnimationContext.fragment
|
|
481
|
-
|
|
482
|
-
val alphaAnimator = createDimmingViewAlphaAnimator(0f, dimmingDelegate.maxAlpha, dimmingDelegate)
|
|
483
|
-
val slideAnimator = createSheetSlideInAnimator()
|
|
484
|
-
|
|
485
|
-
animatorSet
|
|
486
|
-
.play(slideAnimator)
|
|
487
|
-
.takeIf {
|
|
488
|
-
dimmingDelegate.willDimForDetentIndex(screen, screen.sheetInitialDetentIndex)
|
|
489
|
-
}?.with(alphaAnimator)
|
|
490
|
-
|
|
491
|
-
attachCommonListeners(animatorSet, isEnter = true, screenStackFragment)
|
|
492
|
-
|
|
493
|
-
return animatorSet
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
internal fun createSheetExitAnimator(sheetAnimationContext: SheetAnimationContext): Animator {
|
|
497
|
-
val animatorSet = AnimatorSet()
|
|
498
|
-
|
|
499
|
-
val coordinatorLayout = sheetAnimationContext.coordinatorLayout
|
|
500
|
-
val dimmingDelegate = sheetAnimationContext.dimmingDelegate
|
|
501
|
-
val screenStackFragment = sheetAnimationContext.fragment
|
|
502
|
-
|
|
503
|
-
val alphaAnimator =
|
|
504
|
-
createDimmingViewAlphaAnimator(dimmingDelegate.dimmingView.alpha, 0f, dimmingDelegate)
|
|
505
|
-
val slideAnimator = createSheetSlideOutAnimator(coordinatorLayout)
|
|
506
|
-
|
|
507
|
-
animatorSet.play(alphaAnimator).with(slideAnimator)
|
|
508
|
-
|
|
509
|
-
attachCommonListeners(animatorSet, isEnter = false, screenStackFragment)
|
|
510
|
-
|
|
511
|
-
return animatorSet
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
private fun createDimmingViewAlphaAnimator(
|
|
515
|
-
from: Float,
|
|
516
|
-
to: Float,
|
|
517
|
-
dimmingDelegate: DimmingViewManager,
|
|
518
|
-
): ValueAnimator =
|
|
519
|
-
ValueAnimator.ofFloat(from, to).apply {
|
|
520
|
-
addUpdateListener { animator ->
|
|
521
|
-
(animator.animatedValue as? Float)?.let {
|
|
522
|
-
dimmingDelegate.dimmingView.alpha = it
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
437
|
+
internal fun createSheetEnterAnimator(sheetAnimationContext: SheetAnimationContext): Animator =
|
|
438
|
+
screen.sheetAnimationCoordinator.createSheetEnterAnimator(sheetAnimationContext)
|
|
526
439
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
val evaluator = ExternalBoundaryValuesEvaluator(startValueCallback, { 0f })
|
|
530
|
-
|
|
531
|
-
return ValueAnimator.ofObject(evaluator, screen.height.toFloat(), 0f).apply {
|
|
532
|
-
addUpdateListener { updateSheetTranslationY(it.animatedValue as Float) }
|
|
533
|
-
}
|
|
534
|
-
}
|
|
440
|
+
internal fun createSheetExitAnimator(sheetAnimationContext: SheetAnimationContext): Animator =
|
|
441
|
+
screen.sheetAnimationCoordinator.createSheetExitAnimator(sheetAnimationContext)
|
|
535
442
|
|
|
536
|
-
|
|
537
|
-
|
|
443
|
+
internal fun handleKeyboardInsetsProgress(insets: WindowInsetsCompat) =
|
|
444
|
+
screen.sheetAnimationCoordinator.handleKeyboardInsetsProgress(insets)
|
|
538
445
|
|
|
539
|
-
|
|
540
|
-
addUpdateListener {
|
|
541
|
-
updateSheetTranslationY(it.animatedValue as Float)
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
private fun updateSheetTranslationY(baseTranslationY: Float) {
|
|
547
|
-
val keyboardCorrection = lastKeyboardBottomOffset
|
|
548
|
-
val bottomOffset = computeSheetOffsetYWithIMEPresent(keyboardCorrection).toFloat()
|
|
549
|
-
|
|
550
|
-
screen.translationY = baseTranslationY - bottomOffset
|
|
551
|
-
}
|
|
446
|
+
internal fun notifyKeyboardAnimationStart() = screen.sheetAnimationCoordinator.notifyKeyboardAnimationStart()
|
|
552
447
|
|
|
553
|
-
internal fun
|
|
554
|
-
lastKeyboardBottomOffset = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
555
|
-
// Prioritize enter/exit animations over direct keyboard inset reactions.
|
|
556
|
-
// We store the latest keyboard offset in `lastKeyboardBottomOffset`
|
|
557
|
-
// so that it can always be respected when applying translations in `updateSheetTranslationY`.
|
|
558
|
-
//
|
|
559
|
-
// This approach allows screen translation to be triggered from two sources, but without messing them together:
|
|
560
|
-
// - During enter/exit animations, while accounting for the keyboard height.
|
|
561
|
-
// - While interacting with a TextInput inside the bottom sheet, to handle keyboard show/hide events.
|
|
562
|
-
if (!isSheetAnimationInProgress) {
|
|
563
|
-
updateSheetTranslationY(0f)
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
private fun attachCommonListeners(
|
|
568
|
-
animatorSet: AnimatorSet,
|
|
569
|
-
isEnter: Boolean,
|
|
570
|
-
screenStackFragment: ScreenStackFragment,
|
|
571
|
-
) {
|
|
572
|
-
animatorSet.addListener(
|
|
573
|
-
ScreenAnimationDelegate(
|
|
574
|
-
screenStackFragment,
|
|
575
|
-
ScreenEventEmitter(screen),
|
|
576
|
-
if (isEnter) {
|
|
577
|
-
ScreenAnimationDelegate.AnimationType.ENTER
|
|
578
|
-
} else {
|
|
579
|
-
ScreenAnimationDelegate.AnimationType.EXIT
|
|
580
|
-
},
|
|
581
|
-
),
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
animatorSet.addListener(
|
|
585
|
-
object : AnimatorListenerAdapter() {
|
|
586
|
-
override fun onAnimationStart(animation: Animator) {
|
|
587
|
-
isSheetAnimationInProgress = true
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
override fun onAnimationEnd(animation: Animator) {
|
|
591
|
-
isSheetAnimationInProgress = false
|
|
592
|
-
|
|
593
|
-
screen.onSheetYTranslationChanged()
|
|
594
|
-
}
|
|
595
|
-
},
|
|
596
|
-
)
|
|
597
|
-
}
|
|
448
|
+
internal fun notifyKeyboardAnimationEnd() = screen.sheetAnimationCoordinator.notifyKeyboardAnimationEnd()
|
|
598
449
|
|
|
599
450
|
private inner class KeyboardHandler : BottomSheetBehavior.BottomSheetCallback() {
|
|
600
451
|
override fun onStateChanged(
|
|
@@ -158,3 +158,19 @@ fun Screen.sheetShouldUseDimmingView(): Boolean {
|
|
|
158
158
|
* is reattached to container.
|
|
159
159
|
*/
|
|
160
160
|
fun View.isLaidOutOrHasCachedLayout() = this.isLaidOut || height > 0 || width > 0
|
|
161
|
+
|
|
162
|
+
internal fun Screen.resolveClampedHeight(
|
|
163
|
+
targetHeight: Int,
|
|
164
|
+
currentTranslationY: Float,
|
|
165
|
+
): Int {
|
|
166
|
+
val maxAvailableVerticalSpace =
|
|
167
|
+
fragment
|
|
168
|
+
?.asScreenStackFragment()
|
|
169
|
+
?.sheetDelegate
|
|
170
|
+
?.tryResolveMaxFormSheetHeight() ?: return targetHeight
|
|
171
|
+
|
|
172
|
+
// Please note that currentTranslationY is rather < 0 here.
|
|
173
|
+
// The translation is included in constraining the available space, because the FormSheet can have some offset, e.g. to
|
|
174
|
+
// avoid the keyboard.
|
|
175
|
+
return targetHeight.coerceAtMost((maxAvailableVerticalSpace + currentTranslationY).toInt())
|
|
176
|
+
}
|
|
@@ -265,6 +265,19 @@ class TabsContainer internal constructor(
|
|
|
265
265
|
|
|
266
266
|
super.onAttachedToWindow()
|
|
267
267
|
setupFragmentManager()
|
|
268
|
+
|
|
269
|
+
// When TabsContainer is reattached to window, it might find new fragment manager (other
|
|
270
|
+
// than previous instance, e.g. in Stack v4 when screen is pushed & popped over screen with
|
|
271
|
+
// Tabs). In such case, we need to re-add currently selected tab screen fragment. As there
|
|
272
|
+
// might be another operation pending, we need to make sure that the state is restored
|
|
273
|
+
// before flushPendingUpdates is called. That's why inside restoreNavigationStateIfNeeded
|
|
274
|
+
// we're committing the transaction synchronously. This might lead to a crash if another
|
|
275
|
+
// transaction is currently being committed. If this happens to be problematic, we might need
|
|
276
|
+
// to reevaluate our approach. See #4035.
|
|
277
|
+
if (navState.isNotEmpty()) {
|
|
278
|
+
restoreNavigationStateIfNeeded()
|
|
279
|
+
}
|
|
280
|
+
|
|
268
281
|
flushPendingUpdates()
|
|
269
282
|
|
|
270
283
|
colorSchemeCoordinator.setup(this) { uiNightMode ->
|
|
@@ -275,6 +288,7 @@ class TabsContainer internal constructor(
|
|
|
275
288
|
override fun onDetachedFromWindow() {
|
|
276
289
|
super.onDetachedFromWindow()
|
|
277
290
|
teardownFragmentManager()
|
|
291
|
+
colorSchemeCoordinator.teardown()
|
|
278
292
|
}
|
|
279
293
|
|
|
280
294
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
|
@@ -558,6 +572,36 @@ class TabsContainer internal constructor(
|
|
|
558
572
|
return true
|
|
559
573
|
}
|
|
560
574
|
|
|
575
|
+
/**
|
|
576
|
+
* When Tabs are reattached to window, they might find new fragment manager. In this case we
|
|
577
|
+
* need to restore navigation state. We're committing the transaction synchronously so that any
|
|
578
|
+
* following operations have valid restored state before their execution.
|
|
579
|
+
*
|
|
580
|
+
* This function is a no-op if navigation state is empty.
|
|
581
|
+
*/
|
|
582
|
+
private fun restoreNavigationStateIfNeeded() {
|
|
583
|
+
if (navState.isEmpty()) {
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
val currentFragments =
|
|
588
|
+
requireFragmentManager.fragments
|
|
589
|
+
.filterIsInstance<TabsScreenFragment>()
|
|
590
|
+
.filter { it in tabsModel }
|
|
591
|
+
.toList()
|
|
592
|
+
|
|
593
|
+
if (currentFragments.size == 1 && currentFragments[0] === selectedTab) {
|
|
594
|
+
return
|
|
595
|
+
} else if (currentFragments.isEmpty()) {
|
|
596
|
+
requireFragmentManager
|
|
597
|
+
.createTransactionWithReordering()
|
|
598
|
+
.add(contentView.id, selectedTab)
|
|
599
|
+
.commitNowAllowingStateLoss()
|
|
600
|
+
} else {
|
|
601
|
+
error("[RNScreens] Unexpected fragment manager state.")
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
561
605
|
private fun applyDayNightUiMode(uiMode: Int) {
|
|
562
606
|
// update the appearance when user toggles between dark/light mode
|
|
563
607
|
when (uiMode) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-screens",
|
|
3
|
-
"version": "4.25.
|
|
3
|
+
"version": "4.25.1",
|
|
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 ../)",
|