• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.animation.core.Easing
20 import androidx.compose.animation.core.LinearEasing
21 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
22 import androidx.compose.material3.MotionScheme
23 import androidx.compose.ui.unit.Density
24 import androidx.compose.ui.unit.IntSize
25 import androidx.compose.ui.unit.LayoutDirection
26 import androidx.compose.ui.util.fastCoerceAtLeast
27 import androidx.compose.ui.util.fastCoerceAtMost
28 import androidx.compose.ui.util.fastCoerceIn
29 import com.android.compose.animation.scene.ContentKey
30 import com.android.compose.animation.scene.ElementKey
31 import com.android.compose.animation.scene.ElementMatcher
32 import com.android.compose.animation.scene.ElementStateScope
33 import com.android.compose.animation.scene.content.state.TransitionState
34 import com.android.compose.animation.scene.transformation.PropertyTransformation.Property
35 import kotlinx.coroutines.CoroutineScope
36 
37 /** A transformation applied to one or more elements during a transition. */
38 sealed interface Transformation {
interfacenull39     fun interface Factory {
40         fun create(): Transformation
41     }
42 }
43 
44 // Important: SharedElementTransformation must be a data class because we check that we don't
45 // provide 2 different transformations for the same element in Element.kt
46 internal data class SharedElementTransformation(
47     internal val enabled: Boolean,
48     internal val elevateInContent: ContentKey?,
49 ) : Transformation {
50     class Factory(
51         internal val matcher: ElementMatcher,
52         internal val enabled: Boolean,
53         internal val elevateInContent: ContentKey?,
54     ) : Transformation.Factory {
createnull55         override fun create(): Transformation {
56             return SharedElementTransformation(enabled, elevateInContent)
57         }
58     }
59 }
60 
61 /**
62  * A transformation that changes the value of an element [Property], like its [size][Property.Size]
63  * or [offset][Property.Offset].
64  */
65 sealed interface PropertyTransformation<T> : Transformation {
66     /** The property to which this transformation is applied. */
67     val property: Property<T>
68 
69     sealed class Property<T> {
70         /** The size of an element. */
71         data object Size : Property<IntSize>()
72 
73         /** The offset (position) of an element. */
74         data object Offset : Property<androidx.compose.ui.geometry.Offset>()
75 
76         /** The alpha of an element. */
77         data object Alpha : Property<Float>()
78 
79         /**
80          * The drawing scale of an element. Animating the scale does not have any effect on the
81          * layout.
82          */
83         data object Scale : Property<com.android.compose.animation.scene.Scale>()
84     }
85 }
86 
87 /**
88  * A transformation to a target/transformed value that is automatically interpolated using the
89  * transition progress and transformation range.
90  */
91 interface InterpolatedPropertyTransformation<T> : PropertyTransformation<T> {
92     /**
93      * Return the transformed value for the given property, i.e.:
94      * - the value at progress = 0% for elements that are entering the layout (i.e. elements in the
95      *   content we are transitioning to).
96      * - the value at progress = 100% for elements that are leaving the layout (i.e. elements in the
97      *   content we are transitioning from).
98      *
99      * The returned value will be automatically interpolated using the [transition] progress, the
100      * transformation range and [idleValue], the value of the property when we are idle.
101      */
PropertyTransformationScopenull102     fun PropertyTransformationScope.transform(
103         content: ContentKey,
104         element: ElementKey,
105         transition: TransitionState.Transition,
106         idleValue: T,
107     ): T
108 }
109 
110 interface CustomPropertyTransformation<T> : PropertyTransformation<T> {
111     /**
112      * Return the value that the property should have in the current frame for the given [content]
113      * and [element].
114      *
115      * This transformation can use [transitionScope] to launch animations associated to
116      * [transition], which will not finish until at least one animation/job is still running in the
117      * scope.
118      *
119      * Important: Make sure to never launch long-running jobs in [transitionScope], otherwise
120      * [transition] will never be considered as finished.
121      */
122     fun PropertyTransformationScope.transform(
123         content: ContentKey,
124         element: ElementKey,
125         transition: TransitionState.Transition,
126         transitionScope: CoroutineScope,
127     ): T
128 }
129 
130 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
131 interface PropertyTransformationScope : Density, ElementStateScope {
132     /** The current [direction][LayoutDirection] of the layout. */
133     val layoutDirection: LayoutDirection
134 
135     /** The [MotionScheme] in use by the [SceneTransitionLayout]. */
136     val motionScheme: MotionScheme
137 }
138 
139 /** Defines the transformation-type to be applied to all elements matching [matcher]. */
140 internal class TransformationMatcher(
141     val matcher: ElementMatcher,
142     val factory: Transformation.Factory,
143     val range: TransformationRange?,
144 )
145 
146 /** A pair consisting of a [transformation] and optional [range]. */
147 internal data class TransformationWithRange<out T : Transformation>(
148     val transformation: T,
149     val range: TransformationRange?,
150 ) {
reversednull151     fun reversed(): TransformationWithRange<T> {
152         if (range == null) return this
153 
154         return TransformationWithRange(transformation = transformation, range = range.reversed())
155     }
156 }
157 
158 /** The progress-based range of a [PropertyTransformation]. */
159 internal data class TransformationRange(val start: Float, val end: Float, val easing: Easing) {
160     constructor(
161         start: Float? = null,
162         end: Float? = null,
163         easing: Easing = LinearEasing,
164     ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified, easing)
165 
166     init {
167         require(!start.isSpecified() || (start in 0f..1f))
168         require(!end.isSpecified() || (end in 0f..1f))
169         require(!start.isSpecified() || !end.isSpecified() || start <= end)
170     }
171 
172     /** Reverse this range. */
reversednull173     internal fun reversed() =
174         TransformationRange(start = reverseBound(end), end = reverseBound(start), easing = easing)
175 
176     /** Get the progress of this range given the global [transitionProgress]. */
177     fun progress(transitionProgress: Float): Float {
178         val progress =
179             when {
180                 start.isSpecified() && end.isSpecified() ->
181                     ((transitionProgress - start) / (end - start)).fastCoerceIn(0f, 1f)
182                 !start.isSpecified() && !end.isSpecified() -> transitionProgress
183                 end.isSpecified() -> (transitionProgress / end).fastCoerceAtMost(1f)
184                 else -> ((transitionProgress - start) / (1f - start)).fastCoerceAtLeast(0f)
185             }
186         return easing.transform(progress)
187     }
188 
Floatnull189     private fun Float.isSpecified() = this != BoundUnspecified
190 
191     private fun reverseBound(bound: Float): Float {
192         return if (bound.isSpecified()) {
193             1f - bound
194         } else {
195             BoundUnspecified
196         }
197     }
198 
199     companion object {
200         internal const val BoundUnspecified = Float.MIN_VALUE
201     }
202 }
203