Skip to content

Commit 83be835

Browse files
authored
refactor(Android, FormSheet): Minor cleanups in SheetAnimationCoordinator (#3921)
## Description Extracting long code blocks to separate functions, creating extension methods for repeated calls, fixing potential issues. ## Changes - layoutBottomSheetAtHeight - relpacing 3 similar calls - synchronizeBottomSheetBehaviorWithLayout - initially, I wanted to replace just calls inside content size change animations, but I decided to keep all 3 `parent.requestLayout` calls together, not sure if `onSheetYTranslationChanged` is needed in `updateSheetContentHeightWithAnimation`, but I think it won't hurt, because if layout metrics won't change, we'll early return and don't send update to yoga - split content size change default animation into two separate helpers for expanding and shrinking - these long comments blocks started annoying me - resetting `isSheetAnimationInProgress` flag in `onCancel` callback - might not be needed, because the enter/exit animation has a priority over any other animation, and when dismissing the sheet and showing it once again, we'll receive a new instance, but decided to add this override for safety, because I'm not sure whether the OS cannot cancel this animation in some specific case ## Before & after - visual documentation N/A - refactor ## Test plan Tested with TestFormSheet, Test3336, Test2560 ## Checklist - [ ] Included code example that can be used to test this change. - [ ] For visual changes, included screenshots / GIFs / recordings documenting the change. - [ ] For API changes, updated relevant public types. - [ ] Ensured that CI passes
1 parent 00578bb commit 83be835

1 file changed

Lines changed: 93 additions & 71 deletions

File tree

android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetAnimationCoordinator.kt

Lines changed: 93 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -100,87 +100,92 @@ internal class SheetAnimationCoordinator(
100100
// lands at the correct final geometry, without firing a competing animation.
101101
if (isSheetAnimationInProgress) {
102102
behavior.updateMetrics(clampedNewHeight)
103-
screen.layout(screen.left, screen.bottom - clampedNewHeight, screen.right, screen.bottom)
104-
// Force a layout pass on the CoordinatorLayout to synchronize BottomSheetBehavior's
105-
// internal offsets with the new maxHeight. This prevents the sheet from snapping back
106-
// to its old position when the user starts a gesture.
107-
screen.parent.requestLayout()
103+
screen.layoutBottomSheetAtHeight(clampedNewHeight)
104+
screen.finalizeBottomSheetLayoutUpdates()
108105
return
109106
}
110107

111108
val visibleDelta = (clampedNewHeight - clampedOldHeight).toFloat()
112109
if (visibleDelta == 0f) return
113110

114-
val isContentExpanding = visibleDelta > 0
115-
116-
if (isContentExpanding) {
117-
/*
118-
* Expanding content animation:
119-
*
120-
* Before animation, we're updating the SheetBehavior - the maximum height is the new
121-
* content height, then we're forcing a layout pass. This ensures the view calculates
122-
* with its new bounds when the animation starts.
123-
*
124-
* In the animation, we're translating the Screen back to it's (newly calculated) origin
125-
* position, providing an impression that FormSheet expands. It already has the final size,
126-
* but some content is not yet visible on the screen.
127-
*
128-
* After animation, we just need to send a notification that ShadowTree state should be updated,
129-
* as the positioning of pressables has changed due to the Y translation manipulation.
130-
*/
131-
screen.translationY += visibleDelta
132-
screen
133-
.animate()
134-
.translationY(currentTranslationY)
135-
.withStartAction {
136-
behavior.updateMetrics(clampedNewHeight)
137-
screen.layout(screen.left, screen.bottom - clampedNewHeight, screen.right, screen.bottom)
138-
}.withEndAction {
139-
// Force a layout pass on the CoordinatorLayout to synchronize BottomSheetBehavior's
140-
// internal offsets with the new maxHeight. This prevents the sheet from snapping back
141-
// to its old position when the user starts a gesture.
142-
screen.parent.requestLayout()
143-
screen.onSheetYTranslationChanged()
144-
}.start()
111+
if (visibleDelta > 0) {
112+
animateContentExpanding(behavior, clampedNewHeight, currentTranslationY, visibleDelta)
145113
} else {
146-
/*
147-
* Shrinking content animation:
148-
*
149-
* Before the animation, our Screen translationY is 0 - because its actual layout and visual position are equal.
150-
*
151-
* Before the animation, I'm updating sheet metrics to the target value - it won't update until the next layout pass,
152-
* which is controlled by end action. This is done deliberately, to allow catching the case when quick combination
153-
* of shrink & expand animation is detected.
154-
*
155-
* In the animation, we're translating the Screen down by the calculated height delta to the position (which will
156-
* be new absolute 0 for the Screen, after ending the transition), providing an impression that FormSheet shrinks.
157-
* FormSheet's size remains unchanged during the whole animation, therefore there is no view clipping.
158-
*
159-
* After animation, we can update the layout: the maximum FormSheet height is updated and we're forcing
160-
* another layout pass. Additionally, since the actual layout and the target position are equal,
161-
* we can reset translationY to 0.
162-
*
163-
* After animation, we need to send a notification that ShadowTree state should be updated,
164-
* as the FormSheet size has changed and the positioning of pressables has changed due to the Y translation manipulation.
165-
*/
166-
val targetTranslationY = currentTranslationY - visibleDelta
167-
screen
168-
.animate()
169-
.translationY(targetTranslationY)
170-
.withStartAction {
171-
behavior.updateMetrics(clampedNewHeight)
172-
}.withEndAction {
173-
screen.layout(screen.left, screen.bottom - clampedNewHeight, screen.right, screen.bottom)
174-
screen.translationY = currentTranslationY
175-
// Force a layout pass on the CoordinatorLayout to synchronize BottomSheetBehavior's
176-
// internal offsets with the new maxHeight. This prevents the sheet from snapping back
177-
// to its old position when the user starts a gesture.
178-
screen.parent.requestLayout()
179-
screen.onSheetYTranslationChanged()
180-
}.start()
114+
animateContentShrinking(behavior, clampedNewHeight, currentTranslationY, visibleDelta)
181115
}
182116
}
183117

118+
/*
119+
* Expanding content animation:
120+
*
121+
* Before animation, we're updating the SheetBehavior - the maximum height is the new
122+
* content height, then we're forcing a layout pass. This ensures the view calculates
123+
* with its new bounds when the animation starts.
124+
*
125+
* In the animation, we're translating the Screen back to its (newly calculated) origin
126+
* position, providing an impression that FormSheet expands. It already has the final size,
127+
* but some content is not yet visible on the screen.
128+
*
129+
* After animation, we just need to send a notification that ShadowTree state should be updated,
130+
* as the positioning of pressables has changed due to the Y translation manipulation.
131+
*/
132+
private fun animateContentExpanding(
133+
behavior: BottomSheetBehavior<Screen>,
134+
clampedNewHeight: Int,
135+
currentTranslationY: Float,
136+
visibleDelta: Float,
137+
) {
138+
screen.translationY += visibleDelta
139+
screen
140+
.animate()
141+
.translationY(currentTranslationY)
142+
.withStartAction {
143+
behavior.updateMetrics(clampedNewHeight)
144+
screen.layoutBottomSheetAtHeight(clampedNewHeight)
145+
}.withEndAction {
146+
screen.finalizeBottomSheetLayoutUpdates()
147+
}.start()
148+
}
149+
150+
/*
151+
* Shrinking content animation:
152+
*
153+
* Before the animation, our Screen translationY is 0 - because its actual layout and visual position are equal.
154+
*
155+
* Before the animation, I'm updating sheet metrics to the target value - it won't update until the next layout pass,
156+
* which is controlled by end action. This is done deliberately, to allow catching the case when quick combination
157+
* of shrink & expand animation is detected.
158+
*
159+
* In the animation, we're translating the Screen down by the calculated height delta to the position (which will
160+
* be new absolute 0 for the Screen, after ending the transition), providing an impression that FormSheet shrinks.
161+
* FormSheet's size remains unchanged during the whole animation, therefore there is no view clipping.
162+
*
163+
* After animation, we can update the layout: the maximum FormSheet height is updated and we're forcing
164+
* another layout pass. Additionally, since the actual layout and the target position are equal,
165+
* we can reset translationY to 0.
166+
*
167+
* After animation, we need to send a notification that ShadowTree state should be updated,
168+
* as the FormSheet size has changed and the positioning of pressables has changed due to the Y translation manipulation.
169+
*/
170+
private fun animateContentShrinking(
171+
behavior: BottomSheetBehavior<Screen>,
172+
clampedNewHeight: Int,
173+
currentTranslationY: Float,
174+
visibleDelta: Float,
175+
) {
176+
val targetTranslationY = currentTranslationY - visibleDelta
177+
screen
178+
.animate()
179+
.translationY(targetTranslationY)
180+
.withStartAction {
181+
behavior.updateMetrics(clampedNewHeight)
182+
}.withEndAction {
183+
screen.layoutBottomSheetAtHeight(clampedNewHeight)
184+
screen.translationY = currentTranslationY
185+
screen.finalizeBottomSheetLayoutUpdates()
186+
}.start()
187+
}
188+
184189
internal fun handleKeyboardInsetsProgress(insets: WindowInsetsCompat) {
185190
lastKeyboardBottomOffset = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
186191
// Prioritize enter/exit animations over direct keyboard inset reactions.
@@ -268,6 +273,19 @@ internal class SheetAnimationCoordinator(
268273
}
269274
}
270275

276+
private fun Screen.layoutBottomSheetAtHeight(height: Int) = layout(left, bottom - height, right, bottom)
277+
278+
private fun Screen.finalizeBottomSheetLayoutUpdates() {
279+
// Force a layout pass on the CoordinatorLayout to synchronize BottomSheetBehavior's
280+
// internal offsets with the new maxHeight. This prevents the sheet from snapping back
281+
// to its old position when the user starts a gesture.
282+
parent.requestLayout()
283+
284+
// Notify that ShadowTree state should be updated, as the positioning of pressables
285+
// has changed due to the Y translation manipulation.
286+
onSheetYTranslationChanged()
287+
}
288+
271289
private fun attachCommonListeners(
272290
animatorSet: AnimatorSet,
273291
isEnter: Boolean,
@@ -291,6 +309,10 @@ internal class SheetAnimationCoordinator(
291309
isSheetAnimationInProgress = true
292310
}
293311

312+
override fun onAnimationCancel(animation: Animator) {
313+
isSheetAnimationInProgress = false
314+
}
315+
294316
override fun onAnimationEnd(animation: Animator) {
295317
isSheetAnimationInProgress = false
296318

0 commit comments

Comments
 (0)