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