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