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