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