react-native-screens 4.25.0 → 4.26.0-nightly-20260512-e6b354434
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 +25 -3
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackViewManager.kt +0 -14
- package/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt +0 -5
- 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/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigState.h +3 -5
- package/ios/RNSEnums.h +6 -6
- package/ios/RNSScreen.h +8 -17
- package/ios/RNSScreen.mm +83 -141
- package/ios/RNSScreenContainer.mm +11 -13
- package/ios/RNSScreenStack.h +5 -9
- package/ios/RNSScreenStack.mm +20 -69
- package/ios/RNSScreenStackHeaderConfig.mm +27 -30
- package/ios/RNSScreenStackHeaderSubview.mm +14 -15
- package/ios/RNSSearchBar.mm +11 -12
- package/ios/conversion/RNSConversions-SplitView.mm +16 -0
- package/ios/conversion/RNSConversions-Tabs.mm +4 -5
- package/ios/conversion/RNSConversions.h +4 -3
- package/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm +6 -8
- package/ios/gamma/split/RNSSplitAppearanceApplicator.swift +1 -0
- package/ios/gamma/split/RNSSplitHostComponentView.h +1 -0
- package/ios/gamma/split/RNSSplitHostComponentView.mm +16 -14
- package/ios/gamma/split/RNSSplitScreenShadowStateProxy.mm +3 -4
- package/ios/gamma/stack/host/RNSStackHostComponentView.mm +10 -12
- package/ios/safe-area/RNSSafeAreaViewComponentView.mm +3 -4
- package/ios/tabs/RCTConvert+RNSTabs.mm +58 -63
- package/ios/tabs/bottom-accessory/RNSTabsBottomAccessoryHelper.mm +2 -3
- package/ios/tabs/bottom-accessory/RNSTabsBottomAccessoryShadowStateProxy.mm +3 -4
- package/ios/tabs/host/RNSTabsHostComponentViewManager.mm +3 -4
- package/ios/tabs/screen/RNSTabsScreenComponentView.h +4 -5
- package/ios/tabs/screen/RNSTabsScreenViewController.mm +5 -6
- package/lib/commonjs/components/Screen.js +1 -4
- package/lib/commonjs/components/Screen.js.map +1 -1
- package/lib/commonjs/components/ScreenStack.js +0 -8
- package/lib/commonjs/components/ScreenStack.js.map +1 -1
- package/lib/commonjs/fabric/ScreenNativeComponent.js.map +1 -1
- package/lib/commonjs/fabric/ScreenStackNativeComponent.js.map +1 -1
- package/lib/commonjs/fabric/gamma/split/SplitHostNativeComponent.js.map +1 -1
- package/lib/commonjs/flags.js +13 -25
- package/lib/commonjs/flags.js.map +1 -1
- package/lib/module/components/Screen.js +1 -4
- package/lib/module/components/Screen.js.map +1 -1
- package/lib/module/components/ScreenStack.js +0 -8
- package/lib/module/components/ScreenStack.js.map +1 -1
- package/lib/module/fabric/ScreenNativeComponent.js.map +1 -1
- package/lib/module/fabric/ScreenStackNativeComponent.js.map +1 -1
- package/lib/module/fabric/gamma/split/SplitHostNativeComponent.js.map +1 -1
- package/lib/module/flags.js +13 -25
- package/lib/module/flags.js.map +1 -1
- package/lib/typescript/components/Screen.d.ts.map +1 -1
- package/lib/typescript/components/ScreenStack.d.ts.map +1 -1
- package/lib/typescript/components/gamma/split/SplitHost.types.d.ts +15 -1
- package/lib/typescript/components/gamma/split/SplitHost.types.d.ts.map +1 -1
- package/lib/typescript/fabric/ModalScreenNativeComponent.d.ts +1 -1
- package/lib/typescript/fabric/ModalScreenNativeComponent.d.ts.map +1 -1
- package/lib/typescript/fabric/ScreenNativeComponent.d.ts +1 -2
- package/lib/typescript/fabric/ScreenNativeComponent.d.ts.map +1 -1
- package/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts +1 -1
- package/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts.map +1 -1
- package/lib/typescript/fabric/ScreenStackHeaderSubviewNativeComponent.d.ts +1 -1
- package/lib/typescript/fabric/ScreenStackHeaderSubviewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/fabric/ScreenStackNativeComponent.d.ts +0 -2
- package/lib/typescript/fabric/ScreenStackNativeComponent.d.ts.map +1 -1
- package/lib/typescript/fabric/gamma/split/SplitHostNativeComponent.d.ts +2 -0
- package/lib/typescript/fabric/gamma/split/SplitHostNativeComponent.d.ts.map +1 -1
- package/lib/typescript/flags.d.ts +3 -0
- package/lib/typescript/flags.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Screen.tsx +0 -11
- package/src/components/ScreenStack.tsx +0 -13
- package/src/components/gamma/split/SplitHost.types.ts +16 -1
- package/src/fabric/ModalScreenNativeComponent.ts +1 -1
- package/src/fabric/ScreenNativeComponent.ts +1 -2
- package/src/fabric/ScreenStackHeaderConfigNativeComponent.ts +1 -1
- package/src/fabric/ScreenStackHeaderSubviewNativeComponent.ts +1 -1
- package/src/fabric/ScreenStackNativeComponent.ts +0 -3
- package/src/fabric/gamma/split/SplitHostNativeComponent.ts +3 -0
- package/src/flags.ts +12 -39
- package/ios/helpers/RNSViewInteractionAware.h +0 -9
- package/ios/helpers/RNSViewInteractionManager.h +0 -25
- package/ios/helpers/RNSViewInteractionManager.mm +0 -64
|
@@ -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
|
}
|
|
@@ -37,6 +37,7 @@ import com.swmansion.rnscreens.events.ScreenDismissedEvent
|
|
|
37
37
|
import com.swmansion.rnscreens.ext.recycle
|
|
38
38
|
import com.swmansion.rnscreens.stack.views.ScreensCoordinatorLayout
|
|
39
39
|
import com.swmansion.rnscreens.utils.DeviceUtils
|
|
40
|
+
import com.swmansion.rnscreens.utils.RNSLog
|
|
40
41
|
import com.swmansion.rnscreens.utils.resolveBackgroundColor
|
|
41
42
|
import kotlin.math.max
|
|
42
43
|
|
|
@@ -128,9 +129,14 @@ class ScreenStackFragment :
|
|
|
128
129
|
|
|
129
130
|
override fun setToolbarTranslucent(translucent: Boolean) {
|
|
130
131
|
if (isToolbarTranslucent != translucent) {
|
|
131
|
-
|
|
132
|
-
(
|
|
133
|
-
|
|
132
|
+
// FormSheet is using BottomSheetBehavior which shouldn't be overwritten.
|
|
133
|
+
if (!screen.usesFormSheetPresentation()) {
|
|
134
|
+
val params = screen.layoutParams
|
|
135
|
+
(params as CoordinatorLayout.LayoutParams).behavior =
|
|
136
|
+
if (translucent) null else ScrollingViewBehavior()
|
|
137
|
+
} else {
|
|
138
|
+
RNSLog.w(TAG, "Skipping behavior update: Cannot override BottomSheetBehavior on a FormSheet.")
|
|
139
|
+
}
|
|
134
140
|
isToolbarTranslucent = translucent
|
|
135
141
|
}
|
|
136
142
|
}
|
|
@@ -270,6 +276,14 @@ class ScreenStackFragment :
|
|
|
270
276
|
object : WindowInsetsAnimationCompat.Callback(
|
|
271
277
|
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
|
|
272
278
|
) {
|
|
279
|
+
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
|
|
280
|
+
super.onPrepare(animation)
|
|
281
|
+
|
|
282
|
+
if ((animation.typeMask and WindowInsetsCompat.Type.ime()) != 0) {
|
|
283
|
+
sheetDelegate.notifyKeyboardAnimationStart()
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
273
287
|
// Replace InsetsAnimationCallback created by BottomSheetBehavior
|
|
274
288
|
// to avoid interfering with custom animations.
|
|
275
289
|
// See: https://github.com/software-mansion/react-native-screens/pull/2909
|
|
@@ -288,6 +302,10 @@ class ScreenStackFragment :
|
|
|
288
302
|
override fun onEnd(animation: WindowInsetsAnimationCompat) {
|
|
289
303
|
super.onEnd(animation)
|
|
290
304
|
|
|
305
|
+
if ((animation.typeMask and WindowInsetsCompat.Type.ime()) != 0) {
|
|
306
|
+
sheetDelegate.notifyKeyboardAnimationEnd()
|
|
307
|
+
}
|
|
308
|
+
|
|
291
309
|
screen.onSheetYTranslationChanged()
|
|
292
310
|
}
|
|
293
311
|
}
|
|
@@ -583,4 +601,8 @@ class ScreenStackFragment :
|
|
|
583
601
|
}
|
|
584
602
|
return bottomSheetWindowInsetListenerChain!!
|
|
585
603
|
}
|
|
604
|
+
|
|
605
|
+
companion object {
|
|
606
|
+
private const val TAG = "ScreenStackFragment"
|
|
607
|
+
}
|
|
586
608
|
}
|
|
@@ -68,20 +68,6 @@ class ScreenStackViewManager :
|
|
|
68
68
|
StackFinishTransitioningEvent.EVENT_NAME to mutableMapOf("registrationName" to "onFinishTransitioning"),
|
|
69
69
|
)
|
|
70
70
|
|
|
71
|
-
// iosPreventReattachmentOfDismissedScreens is not available on Android,
|
|
72
|
-
// however we must override the setter
|
|
73
|
-
override fun setIosPreventReattachmentOfDismissedScreens(
|
|
74
|
-
view: ScreenStack?,
|
|
75
|
-
value: Boolean,
|
|
76
|
-
) = Unit
|
|
77
|
-
|
|
78
|
-
// iosPreventReattachmentOfDismissedModals is not available on Android,
|
|
79
|
-
// however we must override the setter
|
|
80
|
-
override fun setIosPreventReattachmentOfDismissedModals(
|
|
81
|
-
view: ScreenStack?,
|
|
82
|
-
value: Boolean,
|
|
83
|
-
) = Unit
|
|
84
|
-
|
|
85
71
|
// nativeContainerBackgroundColor is iOS-only because the native view hierarchy
|
|
86
72
|
// differs between platforms. On Android, ScreenStack is used directly as the
|
|
87
73
|
// container, so `style.backgroundColor` achieves the same effect.
|
|
@@ -323,11 +323,6 @@ open class ScreenViewManager :
|
|
|
323
323
|
value: Boolean,
|
|
324
324
|
) = Unit
|
|
325
325
|
|
|
326
|
-
override fun setIos26AllowInteractionsDuringTransition(
|
|
327
|
-
view: Screen?,
|
|
328
|
-
value: Boolean,
|
|
329
|
-
) = Unit
|
|
330
|
-
|
|
331
326
|
// END mark: iOS-only
|
|
332
327
|
|
|
333
328
|
override fun setAndroidResetScreenShadowStateOnOrientationChangeEnabled(
|
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
|
+
}
|