1 /*
<lambda>null2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.material3.adaptive.layout
18 
19 import androidx.annotation.VisibleForTesting
20 import androidx.compose.animation.EnterTransition
21 import androidx.compose.animation.ExitTransition
22 import androidx.compose.animation.core.AnimationVector
23 import androidx.compose.animation.core.AnimationVector4D
24 import androidx.compose.animation.core.FiniteAnimationSpec
25 import androidx.compose.animation.core.Spring
26 import androidx.compose.animation.core.TwoWayConverter
27 import androidx.compose.animation.core.VectorizedFiniteAnimationSpec
28 import androidx.compose.animation.core.spring
29 import androidx.compose.animation.expandHorizontally
30 import androidx.compose.animation.shrinkHorizontally
31 import androidx.compose.animation.slideInHorizontally
32 import androidx.compose.animation.slideOutHorizontally
33 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
34 import androidx.compose.runtime.Immutable
35 import androidx.compose.runtime.Stable
36 import androidx.compose.runtime.getValue
37 import androidx.compose.runtime.mutableStateOf
38 import androidx.compose.runtime.setValue
39 import androidx.compose.ui.Alignment
40 import androidx.compose.ui.unit.IntOffset
41 import androidx.compose.ui.unit.IntRect
42 import androidx.compose.ui.unit.IntSize
43 import androidx.compose.ui.util.fastRoundToInt
44 import kotlin.jvm.JvmInline
45 import kotlin.math.max
46 import kotlin.math.min
47 
48 /**
49  * Scope for performing pane motions within a pane scaffold. It provides the spec and necessary info
50  * to decide a pane's [EnterTransition] and [ExitTransition], as well as how bounds morphing will be
51  * performed.
52  *
53  * @param Role the pane scaffold role class used to specify panes in the associated pane scaffold;
54  *   see [ThreePaneScaffoldRole], [ListDetailPaneScaffoldRole], and [SupportingPaneScaffoldRole].
55  */
56 @Suppress("PrimitiveInCollection") // No way to get underlying Long of IntSize or IntOffset
57 @ExperimentalMaterial3AdaptiveApi
58 sealed interface PaneScaffoldMotionDataProvider<Role> {
59     /**
60      * The scaffold's current size. Note that the value of the field will only be updated during
61      * measurement of the scaffold and before the first measurement the value will be
62      * [IntSize.Zero].
63      *
64      * Note that this field is not backed by snapshot states so it's supposed to be only read
65      * proactively by the motion logic "on-the-fly" when the scaffold motion is happening.
66      */
67     val scaffoldSize: IntSize
68 
69     /** The number of [PaneMotionData] stored in the provider. */
70     val count: Int
71 
72     /**
73      * Returns the role of the pane at the given index.
74      *
75      * @param index the index of the associated pane
76      * @throws IndexOutOfBoundsException if [index] is larger than or equals to [count]
77      */
78     fun getRoleAt(index: Int): Role
79 
80     /**
81      * Returns [PaneMotionData] associated with the given pane scaffold [role].
82      *
83      * The size and the offset info provided by motion data will only be update during the
84      * measurement stage of the scaffold. Before the first measurement, their values will be
85      * [IntSize.Zero] and [IntOffset.Zero].
86      *
87      * Note that the aforementioned variable fields are **NOT** backed by snapshot states and they
88      * are supposed to be only read proactively by the motion logic "on-the-fly" when the scaffold
89      * motion is happening. Using them elsewhere may cause unexpected behavior.
90      *
91      * @param role the role of the associated pane
92      */
93     operator fun get(role: Role): PaneMotionData
94 
95     /**
96      * Returns [PaneMotionData] associated with the given index, in the left-to-right order of the
97      * panes in the scaffold.
98      *
99      * The size and the offset info provided by motion data will only be update during the
100      * measurement stage of the scaffold. Before the first measurement, their values will be
101      * [IntSize.Zero] and [IntOffset.Zero].
102      *
103      * Note that the aforementioned variable fields are **NOT** backed by snapshot states and they
104      * are supposed to be only read proactively by the motion logic "on-the-fly" when the scaffold
105      * motion is happening. Using them elsewhere may cause unexpected behavior.
106      *
107      * @param index the index of the associated pane
108      * @throws IndexOutOfBoundsException if [index] is larger than or equals to [count]
109      */
110     operator fun get(index: Int): PaneMotionData
111 }
112 
113 /**
114  * Perform actions on each [PaneMotionData], in the left-to-right order of the panes in the
115  * scaffold.
116  *
117  * @param action action to perform on each [PaneMotionData].
118  */
119 @ExperimentalMaterial3AdaptiveApi
forEachnull120 inline fun <Role> PaneScaffoldMotionDataProvider<Role>.forEach(
121     action: (Role, PaneMotionData) -> Unit
122 ) {
123     for (i in 0 until count) {
124         action(getRoleAt(i), get(i))
125     }
126 }
127 
128 /**
129  * Perform actions on each [PaneMotionData], in the right-to-left order of the panes in the
130  * scaffold.
131  *
132  * @param action action to perform on each [PaneMotionData].
133  */
134 @ExperimentalMaterial3AdaptiveApi
forEachReversednull135 inline fun <Role> PaneScaffoldMotionDataProvider<Role>.forEachReversed(
136     action: (Role, PaneMotionData) -> Unit
137 ) {
138     for (i in count - 1 downTo 0) {
139         action(getRoleAt(i), get(i))
140     }
141 }
142 
143 /** The default settings of pane motions. */
144 @ExperimentalMaterial3AdaptiveApi
145 object PaneMotionDefaults {
146     private val IntRectVisibilityThreshold = IntRect(1, 1, 1, 1)
147 
148     /**
149      * The default [FiniteAnimationSpec] used to animate panes. Note that the animation spec is
150      * based on bounds animation - in a situation to animate offset or size independently,
151      * developers can use the derived [OffsetAnimationSpec] and [SizeAnimationSpec].
152      */
153     val AnimationSpec: FiniteAnimationSpec<IntRect> =
154         spring(
155             dampingRatio = 0.8f,
156             stiffness = 380f,
157             visibilityThreshold = IntRectVisibilityThreshold
158         )
159 
160     /**
161      * The default [FiniteAnimationSpec] used to animate panes with a delay. Note that the animation
162      * spec is based on bounds animation - in a situation to animate offset or size independently,
163      * developers can use the derived [DelayedOffsetAnimationSpec] and [DelayedSizeAnimationSpec].
164      */
165     val DelayedAnimationSpec: FiniteAnimationSpec<IntRect> =
166         DelayedSpringSpec(
167             dampingRatio = 0.8f,
168             stiffness = 380f,
169             delayedRatio = 0.1f,
170             visibilityThreshold = IntRectVisibilityThreshold
171         )
172 
173     /**
174      * The derived [FiniteAnimationSpec] that can be used to animate panes' positions when the
175      * specified pane motion is sliding in or out without size change. The spec will be derived from
176      * the provided [AnimationSpec] the using the corresponding top-left coordinates.
177      */
178     val OffsetAnimationSpec: FiniteAnimationSpec<IntOffset> =
179         DerivedOffsetAnimationSpec(AnimationSpec)
180 
181     /**
182      * The derived [FiniteAnimationSpec] that can be used to animate panes' sizes when the specified
183      * pane motion is expanding or shrinking without position change. The spec will be derived from
184      * the provided [AnimationSpec] by using the corresponding sizes.
185      */
186     val SizeAnimationSpec: FiniteAnimationSpec<IntSize> = DerivedSizeAnimationSpec(AnimationSpec)
187 
188     /**
189      * The derived [FiniteAnimationSpec] that can be used to animate panes' positions when the
190      * specified pane motion is sliding in or out with a delay without size change. The spec will be
191      * derived from the provided [DelayedAnimationSpec] the using the corresponding top-left
192      * coordinates.
193      */
194     val DelayedOffsetAnimationSpec: FiniteAnimationSpec<IntOffset> =
195         DerivedOffsetAnimationSpec(DelayedAnimationSpec)
196 
197     /**
198      * The derived [FiniteAnimationSpec] that can be used to animate panes' sizes when the specified
199      * pane motion is expanding or shrinking with a delay without position change. The spec will be
200      * derived from the provided [DelayedAnimationSpec] by using the corresponding sizes.
201      */
202     val DelayedSizeAnimationSpec: FiniteAnimationSpec<IntSize> =
203         DerivedSizeAnimationSpec(DelayedAnimationSpec)
204 }
205 
206 /**
207  * A class to collect motion-relevant data of a specific pane.
208  *
209  * @property motion The specified [PaneMotion] of the pane.
210  * @property originSize The origin measured size of the pane that it should animate from.
211  * @property originPosition The origin placement of the pane that it should animate from, with the
212  *   offset relative to the associated pane scaffold's local coordinates.
213  * @property targetSize The target measured size of the pane that it should animate to.
214  * @property targetPosition The target placement of the pane that it should animate to, with the
215  *   offset relative to the associated pane scaffold's local coordinates.
216  */
217 @ExperimentalMaterial3AdaptiveApi
218 class PaneMotionData internal constructor() {
219     var motion: PaneMotion by mutableStateOf(PaneMotion.NoMotion)
220         internal set
221 
222     var originSize: IntSize = IntSize.Zero
223         internal set
224 
225     var originPosition: IntOffset = IntOffset.Zero
226         internal set
227 
228     var targetSize: IntSize = IntSize.Zero
229         internal set
230 
231     var targetPosition: IntOffset = IntOffset.Zero
232         internal set
233 
234     internal var isOriginSizeAndPositionSet = false
235 }
236 
237 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
238 internal val PaneMotionData.targetLeft
239     get() = targetPosition.x
240 
241 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
242 internal val PaneMotionData.targetRight
243     get() = targetPosition.x + targetSize.width
244 
245 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
246 internal val PaneMotionData.targetTop
247     get() = targetPosition.y
248 
249 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
250 internal val PaneMotionData.targetBottom
251     get() = targetPosition.y + targetSize.height
252 
253 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
254 @VisibleForTesting
255 internal val PaneMotionData.currentLeft
256     get() = originPosition.x
257 
258 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
259 @VisibleForTesting
260 internal val PaneMotionData.currentRight
261     get() = originPosition.x + originSize.width
262 
263 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
264 @VisibleForTesting
265 internal val PaneScaffoldMotionDataProvider<*>.slideInFromLeftOffset: Int
266     get() {
267         // The sliding in distance from left will either be:
268         // 1. The target offset of the left edge of the pane after all panes that are sliding in
269         //    from left, so to account for the spacer size between the sliding panes and other
270         //    panes.
271         // 2. If no such panes exist, use the right edge of the last pane that is sliding in from
272         //    left, as in this case we don't need to account for the spacer size.
273         var previousPane: PaneMotionData? = null
itnull274         forEachReversed { _, it ->
275             if (
276                 it.motion == PaneMotion.EnterFromLeft ||
277                     it.motion == PaneMotion.EnterFromLeftDelayed
278             ) {
279                 return -(previousPane?.targetLeft ?: it.targetRight)
280             }
281             if (
282                 it.motion.type == PaneMotion.Type.Shown ||
283                     it.motion.type == PaneMotion.Type.Entering
284             ) {
285                 previousPane = it
286             }
287         }
288         return 0
289     }
290 
291 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
292 @VisibleForTesting
293 internal val PaneScaffoldMotionDataProvider<*>.slideInFromRightOffset: Int
294     get() {
295         // The sliding in distance from right will either be:
296         // 1. The target offset of the right edge of the pane before all panes that are sliding in
297         //    from right, so to account for the spacer size between the sliding panes and other
298         //    panes.
299         // 2. If no such panes exist, use the left edge of the first pane that is sliding in from
300         //    right, as in this case we don't need to account for the spacer size.
301         var previousPane: PaneMotionData? = null
itnull302         forEach { _, it ->
303             if (
304                 it.motion == PaneMotion.EnterFromRight ||
305                     it.motion == PaneMotion.EnterFromRightDelayed
306             ) {
307                 return scaffoldSize.width - (previousPane?.targetRight ?: it.targetLeft)
308             }
309             if (
310                 it.motion.type == PaneMotion.Type.Shown ||
311                     it.motion.type == PaneMotion.Type.Entering
312             ) {
313                 previousPane = it
314             }
315         }
316         return 0
317     }
318 
319 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
320 @VisibleForTesting
321 internal val PaneScaffoldMotionDataProvider<*>.slideOutToLeftOffset: Int
322     get() {
323         // The sliding out distance to left will either be:
324         // 1. The current offset of the left edge of the pane after all panes that are sliding out
325         //    to left, so to account for the spacer size between the sliding panes and other panes.
326         // 2. If no such panes exist, use the right edge of the last pane that is sliding out to
327         //    left, as in this case we don't need to account for the spacer size.
328         var previousPane: PaneMotionData? = null
itnull329         forEachReversed { _, it ->
330             if (it.motion == PaneMotion.ExitToLeft) {
331                 return -(previousPane?.currentLeft ?: it.currentRight)
332             }
333             if (
334                 it.motion.type == PaneMotion.Type.Shown || it.motion.type == PaneMotion.Type.Exiting
335             ) {
336                 previousPane = it
337             }
338         }
339         return 0
340     }
341 
342 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
343 @VisibleForTesting
344 internal val PaneScaffoldMotionDataProvider<*>.slideOutToRightOffset: Int
345     get() {
346         // The sliding out distance to right will either be:
347         // 1. The current offset of the right edge of the pane before all panes that are sliding out
348         //    to right, so to account for the spacer size between the sliding panes and other panes.
349         // 2. If no such panes exist, use the left edge of the first pane that is sliding out to
350         //    right, as in this case we don't need to account for the spacer size.
351         var previousPane: PaneMotionData? = null
itnull352         forEach { _, it ->
353             if (it.motion == PaneMotion.ExitToRight) {
354                 return scaffoldSize.width - (previousPane?.currentRight ?: it.currentLeft)
355             }
356             if (
357                 it.motion.type == PaneMotion.Type.Shown || it.motion.type == PaneMotion.Type.Exiting
358             ) {
359                 previousPane = it
360             }
361         }
362         return 0
363     }
364 
365 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
366 @VisibleForTesting
getHiddenPaneCurrentLeftnull367 internal fun <Role> PaneScaffoldMotionDataProvider<Role>.getHiddenPaneCurrentLeft(role: Role): Int {
368     var currentLeft = 0
369     forEach { paneRole, data ->
370         // Find the right edge of the shown pane next to the left.
371         if (paneRole == role) {
372             return currentLeft
373         }
374         if (
375             data.motion.type == PaneMotion.Type.Shown || data.motion.type == PaneMotion.Type.Exiting
376         ) {
377             currentLeft = data.currentRight
378         }
379     }
380     return currentLeft
381 }
382 
383 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
384 @VisibleForTesting
getHidingPaneTargetLeftnull385 internal fun <Role> PaneScaffoldMotionDataProvider<Role>.getHidingPaneTargetLeft(role: Role): Int {
386     var targetLeft = 0
387     forEach { paneRole, data ->
388         // Find the right edge of the shown pane next to the left.
389         if (paneRole == role) {
390             return targetLeft
391         }
392         if (
393             data.motion.type == PaneMotion.Type.Shown ||
394                 data.motion.type == PaneMotion.Type.Entering
395         ) {
396             targetLeft = data.targetRight
397         }
398     }
399     return targetLeft
400 }
401 
402 /**
403  * Calculates the default [EnterTransition] of the pane associated to the given role when it's
404  * showing. The [PaneMotion] and pane measurement data provided by [PaneScaffoldMotionDataProvider]
405  * will be used to decide the transition type and relevant values like sliding offsets.
406  *
407  * @param role the role of the pane that is supposed to perform the [EnterTransition] when showing.
408  */
409 @ExperimentalMaterial3AdaptiveApi
calculateDefaultEnterTransitionnull410 fun <Role> PaneScaffoldMotionDataProvider<Role>.calculateDefaultEnterTransition(role: Role) =
411     when (this[role].motion) {
412         PaneMotion.EnterFromLeft ->
413             slideInHorizontally(PaneMotionDefaults.OffsetAnimationSpec) { slideInFromLeftOffset }
414         PaneMotion.EnterFromLeftDelayed ->
415             slideInHorizontally(PaneMotionDefaults.DelayedOffsetAnimationSpec) {
416                 slideInFromLeftOffset
417             }
418         PaneMotion.EnterFromRight ->
419             slideInHorizontally(PaneMotionDefaults.OffsetAnimationSpec) { slideInFromRightOffset }
420         PaneMotion.EnterFromRightDelayed ->
421             slideInHorizontally(PaneMotionDefaults.DelayedOffsetAnimationSpec) {
422                 slideInFromRightOffset
423             }
424         PaneMotion.EnterWithExpand -> {
425             expandHorizontally(PaneMotionDefaults.SizeAnimationSpec, Alignment.CenterHorizontally) +
426                 slideInHorizontally(PaneMotionDefaults.OffsetAnimationSpec) {
427                     getHiddenPaneCurrentLeft(role) - this[role].targetLeft
428                 }
429         }
430         else -> EnterTransition.None
431     }
432 
433 /**
434  * Calculates the default [ExitTransition] of the pane associated to the given role when it's
435  * hiding. The [PaneMotion] and pane measurement data provided by [PaneScaffoldMotionDataProvider]
436  * will be used to decide the transition type and relevant values like sliding offsets.
437  *
438  * @param role the role of the pane that is supposed to perform the [ExitTransition] when hiding.
439  */
440 @ExperimentalMaterial3AdaptiveApi
calculateDefaultExitTransitionnull441 fun <Role> PaneScaffoldMotionDataProvider<Role>.calculateDefaultExitTransition(role: Role) =
442     when (this[role].motion) {
443         PaneMotion.ExitToLeft ->
444             slideOutHorizontally(PaneMotionDefaults.OffsetAnimationSpec) { slideOutToLeftOffset }
445         PaneMotion.ExitToRight ->
446             slideOutHorizontally(PaneMotionDefaults.OffsetAnimationSpec) { slideOutToRightOffset }
447         PaneMotion.ExitWithShrink -> {
448             shrinkHorizontally(PaneMotionDefaults.SizeAnimationSpec, Alignment.CenterHorizontally) +
449                 slideOutHorizontally(PaneMotionDefaults.OffsetAnimationSpec) {
450                     getHidingPaneTargetLeft(role) - this[role].currentLeft
451                 }
452         }
453         else -> ExitTransition.None
454     }
455 
456 /** Interface to specify a custom pane enter/exit motion when a pane's visibility changes. */
457 @ExperimentalMaterial3AdaptiveApi
458 @Stable
459 sealed interface PaneMotion {
460     /** The type of the motion, like exiting, entering, etc. See [Type]. */
461     val type: Type
462 
463     /**
464      * Indicates the current type of pane motion, like if the pane is entering or exiting, or is
465      * kept showing or hidden.
466      */
467     @ExperimentalMaterial3AdaptiveApi
468     @JvmInline
469     value class Type private constructor(val value: Int) {
toStringnull470         override fun toString(): String {
471             return "PaneMotion.Type[${
472                 when(this) {
473                     Hidden -> "Hidden"
474                     Exiting -> "Exiting"
475                     Entering -> "Entering"
476                     Shown -> "Shown"
477                     else -> "Unknown value=$value"
478                 }
479             }]"
480         }
481 
482         companion object {
483             /** Indicates the pane is kept hidden during the current motion. */
484             val Hidden = Type(0)
485 
486             /** Indicates the pane is exiting or hiding during the current motion. */
487             val Exiting = Type(1)
488 
489             /** Indicates the pane is entering or showing during the current motion. */
490             val Entering = Type(2)
491 
492             /** Indicates the pane is keeping being shown during the current motion. */
493             val Shown = Type(3)
494 
calculatenull495             internal fun calculate(
496                 previousValue: PaneAdaptedValue,
497                 currentValue: PaneAdaptedValue
498             ): Type {
499                 val wasShown = if (previousValue == PaneAdaptedValue.Hidden) 0 else 1
500                 val isShown = if (currentValue == PaneAdaptedValue.Hidden) 0 else 2
501                 return Type(wasShown or isShown)
502             }
503         }
504     }
505 
506     @Immutable
507     private class DefaultImpl(val name: String, override val type: Type) : PaneMotion {
toStringnull508         override fun toString() = name
509     }
510 
511     companion object {
512         /** The default pane motion that no animation will be performed. */
513         val NoMotion: PaneMotion = DefaultImpl("NoMotion", Type.Hidden)
514 
515         /**
516          * The default pane motion that will animate panes bounds with the given animation specs
517          * during motion. Note that this should only be used when the associated pane is keeping
518          * showing during the motion.
519          */
520         val AnimateBounds: PaneMotion = DefaultImpl("AnimateBounds", Type.Shown)
521 
522         /**
523          * The default pane motion that will slide panes in from left. Note that this should only be
524          * used when the associated pane is entering - i.e. becoming visible from a hidden state.
525          */
526         val EnterFromLeft: PaneMotion = DefaultImpl("EnterFromLeft", Type.Entering)
527 
528         /**
529          * The default pane motion that will slide panes in from right. Note that this should only
530          * be used when the associated pane is entering - i.e. becoming visible from a hidden state.
531          */
532         val EnterFromRight: PaneMotion = DefaultImpl("EnterFromRight", Type.Entering)
533 
534         /**
535          * The default pane motion that will slide panes in from left with a delay, usually to avoid
536          * the interference of other exiting panes. Note that this should only be used when the
537          * associated pane is entering - i.e. becoming visible from a hidden state.
538          */
539         val EnterFromLeftDelayed: PaneMotion = DefaultImpl("EnterFromLeftDelayed", Type.Entering)
540 
541         /**
542          * The default pane motion that will slide panes in from right with a delay, usually to
543          * avoid the interference of other exiting panes. Note that this should only be used when
544          * the associated pane is entering - i.e. becoming visible from a hidden state.
545          */
546         val EnterFromRightDelayed: PaneMotion = DefaultImpl("EnterFromRightDelayed", Type.Entering)
547 
548         /**
549          * The default pane motion that will slide panes out to left. Note that this should only be
550          * used when the associated pane is exiting - i.e. becoming hidden from a visible state.
551          */
552         val ExitToLeft: PaneMotion = DefaultImpl("ExitToLeft", Type.Exiting)
553 
554         /**
555          * The default pane motion that will slide panes out to right. Note that this should only be
556          * used when the associated pane is exiting - i.e. becoming hidden from a visible state.
557          */
558         val ExitToRight: PaneMotion = DefaultImpl("ExitToRight", Type.Exiting)
559 
560         /**
561          * The default pane motion that will expand panes from a zero size. Note that this should
562          * only be used when the associated pane is entering - i.e. becoming visible from a hidden
563          * state.
564          */
565         val EnterWithExpand: PaneMotion = DefaultImpl("EnterWithExpand", Type.Entering)
566 
567         /**
568          * The default pane motion that will shrink panes until it's gone. Note that this should
569          * only be used when the associated pane is exiting - i.e. becoming hidden from a visible
570          * state.
571          */
572         val ExitWithShrink: PaneMotion = DefaultImpl("ExitWithShrink", Type.Exiting)
573     }
574 }
575 
576 @ExperimentalMaterial3AdaptiveApi
calculatePaneMotionnull577 internal fun <T> calculatePaneMotion(
578     previousScaffoldValue: PaneScaffoldValue<T>,
579     currentScaffoldValue: PaneScaffoldValue<T>,
580     paneOrder: PaneScaffoldHorizontalOrder<T>
581 ): List<PaneMotion> {
582     val numOfPanes = paneOrder.size
583     val paneMotionTypes = Array(numOfPanes) { PaneMotion.Type.Hidden }
584     val paneMotions = MutableList(numOfPanes) { PaneMotion.NoMotion }
585     var firstShownPaneIndex = numOfPanes
586     var firstEnteringPaneIndex = numOfPanes
587     var lastShownPaneIndex = -1
588     var lastEnteringPaneIndex = -1
589     // First pass, to decide the entering/exiting status of each pane, and collect info for
590     // deciding, given a certain pane, if there's a pane on its left or on its right that is
591     // entering or keep showing during the transition.
592     // Also set up the motions of all panes that keep showing to AnimateBounds.
593     paneOrder.forEachIndexed { i, role ->
594         paneMotionTypes[i] =
595             PaneMotion.Type.calculate(previousScaffoldValue[role], currentScaffoldValue[role])
596         when (paneMotionTypes[i]) {
597             PaneMotion.Type.Shown -> {
598                 firstShownPaneIndex = min(firstShownPaneIndex, i)
599                 lastShownPaneIndex = max(lastShownPaneIndex, i)
600                 paneMotions[i] = PaneMotion.AnimateBounds
601             }
602             PaneMotion.Type.Entering -> {
603                 firstEnteringPaneIndex = min(firstEnteringPaneIndex, i)
604                 lastEnteringPaneIndex = max(lastEnteringPaneIndex, i)
605             }
606         }
607     }
608     // Second pass, to decide the exiting motions of all exiting panes.
609     // Also collects info for the next pass to decide the entering motions of entering panes.
610     var hasPanesExitToRight = false
611     var hasPanesExitToLeft = false
612     var firstPaneExitToRightIndex = numOfPanes
613     var lastPaneExitToLeftIndex = -1
614     paneOrder.forEachIndexed { i, _ ->
615         val hasShownPanesOnLeft = firstShownPaneIndex < i
616         val hasEnteringPanesOnLeft = firstEnteringPaneIndex < i
617         val hasShownPanesOnRight = lastShownPaneIndex > i
618         val hasEnteringPanesOnRight = lastEnteringPaneIndex > i
619         if (paneMotionTypes[i] == PaneMotion.Type.Exiting) {
620             paneMotions[i] =
621                 if (!hasShownPanesOnRight && !hasEnteringPanesOnRight) {
622                     // No panes will interfere the motion on the right, exit to right.
623                     hasPanesExitToRight = true
624                     firstPaneExitToRightIndex = min(firstPaneExitToRightIndex, i)
625                     PaneMotion.ExitToRight
626                 } else if (!hasShownPanesOnLeft && !hasEnteringPanesOnLeft) {
627                     // No panes will interfere the motion on the left, exit to left.
628                     hasPanesExitToLeft = true
629                     lastPaneExitToLeftIndex = max(lastPaneExitToLeftIndex, i)
630                     PaneMotion.ExitToLeft
631                 } else if (!hasShownPanesOnRight) {
632                     // Only showing panes can interfere the motion on the right, exit to right.
633                     hasPanesExitToRight = true
634                     firstPaneExitToRightIndex = min(firstPaneExitToRightIndex, i)
635                     PaneMotion.ExitToRight
636                 } else if (!hasShownPanesOnLeft) { // Only showing panes on left
637                     // Only showing panes can interfere the motion on the left, exit to left.
638                     hasPanesExitToLeft = true
639                     lastPaneExitToLeftIndex = max(lastPaneExitToLeftIndex, i)
640                     PaneMotion.ExitToLeft
641                 } else {
642                     // Both sides has panes that keep being visible during transition, shrink to
643                     // exit
644                     PaneMotion.ExitWithShrink
645                 }
646         }
647     }
648     // Third pass, to decide the entering motions of all entering panes.
649     paneOrder.forEachIndexed { i, _ ->
650         val hasShownPanesOnLeft = firstShownPaneIndex < i
651         val hasShownPanesOnRight = lastShownPaneIndex > i
652         val hasLeftPaneExitToRight = firstPaneExitToRightIndex < i
653         val hasRightPaneExitToLeft = lastPaneExitToLeftIndex > i
654         // For a given pane, if there's another pane that keeps showing on its right, or there's
655         // a pane on its right that's exiting to its left, the pane cannot enter from right since
656         // doing so will either interfere with the showing pane, or cause incorrect order of the
657         // pane position during the transition. In other words, this case is considered "blocking".
658         // Same on the other side.
659         val noBlockingPanesOnRight = !hasShownPanesOnRight && !hasRightPaneExitToLeft
660         val noBlockingPanesOnLeft = !hasShownPanesOnLeft && !hasLeftPaneExitToRight
661         if (paneMotionTypes[i] == PaneMotion.Type.Entering) {
662             paneMotions[i] =
663                 if (noBlockingPanesOnRight && !hasPanesExitToRight) {
664                     // No panes will block the motion on the right, enter from right.
665                     PaneMotion.EnterFromRight
666                 } else if (noBlockingPanesOnLeft && !hasPanesExitToLeft) {
667                     // No panes will block the motion on the left, enter from left.
668                     PaneMotion.EnterFromLeft
669                 } else if (noBlockingPanesOnRight) {
670                     // Only hiding panes can interfere the motion on the right, enter from right.
671                     PaneMotion.EnterFromRightDelayed
672                 } else if (noBlockingPanesOnLeft) {
673                     // Only hiding panes can interfere the motion on the left, enter from left.
674                     PaneMotion.EnterFromLeftDelayed
675                 } else {
676                     // Both sides has panes that keep being visible during transition, expand to
677                     // enter
678                     PaneMotion.EnterWithExpand
679                 }
680         }
681     }
682     return paneMotions
683 }
684 
685 internal val IntRectToVector: TwoWayConverter<IntRect, AnimationVector4D> =
686     TwoWayConverter(
<lambda>null687         convertToVector = {
688             AnimationVector4D(
689                 it.left.toFloat(),
690                 it.top.toFloat(),
691                 it.right.toFloat(),
692                 it.bottom.toFloat()
693             )
694         },
<lambda>null695         convertFromVector = {
696             IntRect(
697                 it.v1.fastRoundToInt(),
698                 it.v2.fastRoundToInt(),
699                 it.v3.fastRoundToInt(),
700                 it.v4.fastRoundToInt()
701             )
702         }
703     )
704 
705 internal class DerivedSizeAnimationSpec(private val boundsSpec: FiniteAnimationSpec<IntRect>) :
706     FiniteAnimationSpec<IntSize> {
vectorizenull707     override fun <V : AnimationVector> vectorize(
708         converter: TwoWayConverter<IntSize, V>
709     ): VectorizedFiniteAnimationSpec<V> =
710         boundsSpec.vectorize(
711             object : TwoWayConverter<IntRect, V> {
712                 override val convertFromVector: (V) -> IntRect = { vector ->
713                     with(converter.convertFromVector(vector)) { IntRect(0, 0, width, height) }
714                 }
715                 override val convertToVector: (IntRect) -> V = { bounds ->
716                     converter.convertToVector(bounds.size)
717                 }
718             }
719         )
720 
equalsnull721     override fun equals(other: Any?): Boolean {
722         if (this === other) return true
723         if (other !is DerivedSizeAnimationSpec) return false
724         return boundsSpec == other.boundsSpec
725     }
726 
hashCodenull727     override fun hashCode(): Int = boundsSpec.hashCode()
728 }
729 
730 internal class DerivedOffsetAnimationSpec(private val boundsSpec: FiniteAnimationSpec<IntRect>) :
731     FiniteAnimationSpec<IntOffset> {
732     override fun <V : AnimationVector> vectorize(
733         converter: TwoWayConverter<IntOffset, V>
734     ): VectorizedFiniteAnimationSpec<V> =
735         boundsSpec.vectorize(
736             object : TwoWayConverter<IntRect, V> {
737                 override val convertFromVector: (V) -> IntRect = { vector ->
738                     with(converter.convertFromVector(vector)) { IntRect(x, y, x, y) }
739                 }
740                 override val convertToVector: (IntRect) -> V = { bounds ->
741                     converter.convertToVector(bounds.topLeft)
742                 }
743             }
744         )
745 
746     override fun equals(other: Any?): Boolean {
747         if (this === other) return true
748         if (other !is DerivedOffsetAnimationSpec) return false
749         return boundsSpec == other.boundsSpec
750     }
751 
752     override fun hashCode(): Int = boundsSpec.hashCode()
753 }
754 
755 internal class DelayedSpringSpec<T>(
756     dampingRatio: Float = Spring.DampingRatioNoBouncy,
757     stiffness: Float = Spring.StiffnessMedium,
758     private val delayedRatio: Float,
759     visibilityThreshold: T? = null
760 ) : FiniteAnimationSpec<T> {
761     private val originalSpringSpec = spring(dampingRatio, stiffness, visibilityThreshold)
762 
vectorizenull763     override fun <V : AnimationVector> vectorize(
764         converter: TwoWayConverter<T, V>
765     ): VectorizedFiniteAnimationSpec<V> =
766         DelayedVectorizedSpringSpec(originalSpringSpec.vectorize(converter), delayedRatio)
767 }
768 
769 private class DelayedVectorizedSpringSpec<V : AnimationVector>(
770     val originalVectorizedSpringSpec: VectorizedFiniteAnimationSpec<V>,
771     val delayedRatio: Float,
772 ) : VectorizedFiniteAnimationSpec<V> {
773     var delayedTimeNanos: Long = 0
774     var cachedInitialValue: V? = null
775     var cachedTargetValue: V? = null
776     var cachedInitialVelocity: V? = null
777     var cachedOriginalDurationNanos: Long = 0
778 
779     override fun getValueFromNanos(
780         playTimeNanos: Long,
781         initialValue: V,
782         targetValue: V,
783         initialVelocity: V
784     ): V {
785         updateDelayedTimeNanosIfNeeded(initialValue, targetValue, initialVelocity)
786         return if (playTimeNanos <= delayedTimeNanos) {
787             initialValue
788         } else {
789             originalVectorizedSpringSpec.getValueFromNanos(
790                 playTimeNanos - delayedTimeNanos,
791                 initialValue,
792                 targetValue,
793                 initialVelocity
794             )
795         }
796     }
797 
798     override fun getVelocityFromNanos(
799         playTimeNanos: Long,
800         initialValue: V,
801         targetValue: V,
802         initialVelocity: V
803     ): V {
804         updateDelayedTimeNanosIfNeeded(initialValue, targetValue, initialVelocity)
805         return if (playTimeNanos <= delayedTimeNanos) {
806             initialVelocity
807         } else {
808             originalVectorizedSpringSpec.getVelocityFromNanos(
809                 playTimeNanos - delayedTimeNanos,
810                 initialValue,
811                 targetValue,
812                 initialVelocity
813             )
814         }
815     }
816 
817     override fun getDurationNanos(initialValue: V, targetValue: V, initialVelocity: V): Long {
818         updateDelayedTimeNanosIfNeeded(initialValue, targetValue, initialVelocity)
819         return cachedOriginalDurationNanos + delayedTimeNanos
820     }
821 
822     private fun updateDelayedTimeNanosIfNeeded(
823         initialValue: V,
824         targetValue: V,
825         initialVelocity: V
826     ) {
827         if (
828             initialValue != cachedInitialValue ||
829                 targetValue != cachedTargetValue ||
830                 initialVelocity != cachedInitialVelocity
831         ) {
832             cachedOriginalDurationNanos =
833                 originalVectorizedSpringSpec.getDurationNanos(
834                     initialValue,
835                     targetValue,
836                     initialVelocity
837                 )
838             delayedTimeNanos = (cachedOriginalDurationNanos * delayedRatio).toLong()
839         }
840     }
841 }
842