1 /*
2 * Copyright 2023 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 com.android.compose.animation.scene.transformation
18
19 import androidx.compose.ui.util.fastCoerceAtLeast
20 import androidx.compose.ui.util.fastCoerceAtMost
21 import androidx.compose.ui.util.fastCoerceIn
22 import com.android.compose.animation.scene.Element
23 import com.android.compose.animation.scene.ElementMatcher
24 import com.android.compose.animation.scene.SceneKey
25 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
26 import com.android.compose.animation.scene.TransitionState
27
28 /** A transformation applied to one or more elements during a transition. */
29 sealed interface Transformation {
30 /**
31 * The matcher that should match the element(s) to which this transformation should be applied.
32 */
33 val matcher: ElementMatcher
34
35 /**
36 * The range during which the transformation is applied. If it is `null`, then the
37 * transformation will be applied throughout the whole scene transition.
38 */
39 // TODO(b/240432457): Move this back to PropertyTransformation.
40 val range: TransformationRange?
41 get() = null
42
43 /*
44 * Reverse this transformation. This is called when we use Transition(from = A, to = B) when
45 * animating from B to A and there is no Transition(from = B, to = A) defined.
46 */
reversednull47 fun reversed(): Transformation = this
48 }
49
50 internal class SharedElementTransformation(
51 override val matcher: ElementMatcher,
52 internal val enabled: Boolean,
53 ) : Transformation
54
55 /** A transformation that changes the value of an element property, like its size or offset. */
56 internal sealed interface PropertyTransformation<T> : Transformation {
57 /**
58 * Transform [value], i.e. the value of the transformed property without this transformation.
59 */
60 // TODO(b/290184746): Figure out a public API for custom transformations that don't have access
61 // to these internal classes.
62 fun transform(
63 layoutImpl: SceneTransitionLayoutImpl,
64 scene: SceneKey,
65 element: Element,
66 sceneState: Element.SceneState,
67 transition: TransitionState.Transition,
68 value: T,
69 ): T
70 }
71
72 /**
73 * A [PropertyTransformation] associated to a range. This is a helper class so that normal
74 * implementations of [PropertyTransformation] don't have to take care of reversing their range when
75 * they are reversed.
76 */
77 internal class RangedPropertyTransformation<T>(
78 val delegate: PropertyTransformation<T>,
79 override val range: TransformationRange,
<lambda>null80 ) : PropertyTransformation<T> by delegate {
81 override fun reversed(): Transformation {
82 return RangedPropertyTransformation(
83 delegate.reversed() as PropertyTransformation<T>,
84 range.reversed()
85 )
86 }
87 }
88
89 /** The progress-based range of a [PropertyTransformation]. */
90 data class TransformationRange(
91 val start: Float,
92 val end: Float,
93 ) {
94 constructor(
95 start: Float? = null,
96 end: Float? = null
97 ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified)
98
99 init {
100 require(!start.isSpecified() || (start in 0f..1f))
101 require(!end.isSpecified() || (end in 0f..1f))
102 require(!start.isSpecified() || !end.isSpecified() || start <= end)
103 }
104
105 /** Reverse this range. */
reversednull106 fun reversed() = TransformationRange(start = reverseBound(end), end = reverseBound(start))
107
108 /** Get the progress of this range given the global [transitionProgress]. */
109 fun progress(transitionProgress: Float): Float {
110 return when {
111 start.isSpecified() && end.isSpecified() ->
112 ((transitionProgress - start) / (end - start)).fastCoerceIn(0f, 1f)
113 !start.isSpecified() && !end.isSpecified() -> transitionProgress
114 end.isSpecified() -> (transitionProgress / end).fastCoerceAtMost(1f)
115 else -> ((transitionProgress - start) / (1f - start)).fastCoerceAtLeast(0f)
116 }
117 }
118
Floatnull119 private fun Float.isSpecified() = this != BoundUnspecified
120
121 private fun reverseBound(bound: Float): Float {
122 return if (bound.isSpecified()) {
123 1f - bound
124 } else {
125 BoundUnspecified
126 }
127 }
128
129 companion object {
130 const val BoundUnspecified = Float.MIN_VALUE
131 }
132 }
133