@@ -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