1 /* <lambda>null2 * 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 18 19 import androidx.compose.animation.core.AnimationSpec 20 import androidx.compose.animation.core.Spring 21 import androidx.compose.animation.core.SpringSpec 22 import androidx.compose.animation.core.snap 23 import androidx.compose.animation.core.spring 24 import androidx.compose.foundation.gestures.Orientation 25 import androidx.compose.ui.geometry.Offset 26 import androidx.compose.ui.unit.IntSize 27 import androidx.compose.ui.util.fastForEach 28 import com.android.compose.animation.scene.transformation.AnchoredSize 29 import com.android.compose.animation.scene.transformation.AnchoredTranslate 30 import com.android.compose.animation.scene.transformation.DrawScale 31 import com.android.compose.animation.scene.transformation.EdgeTranslate 32 import com.android.compose.animation.scene.transformation.Fade 33 import com.android.compose.animation.scene.transformation.OverscrollTranslate 34 import com.android.compose.animation.scene.transformation.PropertyTransformation 35 import com.android.compose.animation.scene.transformation.RangedPropertyTransformation 36 import com.android.compose.animation.scene.transformation.ScaleSize 37 import com.android.compose.animation.scene.transformation.SharedElementTransformation 38 import com.android.compose.animation.scene.transformation.Transformation 39 import com.android.compose.animation.scene.transformation.Translate 40 41 /** The transitions configuration of a [SceneTransitionLayout]. */ 42 class SceneTransitions 43 internal constructor( 44 internal val defaultSwipeSpec: SpringSpec<Float>, 45 internal val transitionSpecs: List<TransitionSpecImpl>, 46 internal val overscrollSpecs: List<OverscrollSpecImpl>, 47 internal val interruptionHandler: InterruptionHandler, 48 ) { 49 private val transitionCache = 50 mutableMapOf< 51 SceneKey, MutableMap<SceneKey, MutableMap<TransitionKey?, TransitionSpecImpl>> 52 >() 53 54 private val overscrollCache = 55 mutableMapOf<SceneKey, MutableMap<Orientation, OverscrollSpecImpl?>>() 56 57 internal fun transitionSpec( 58 from: SceneKey, 59 to: SceneKey, 60 key: TransitionKey?, 61 ): TransitionSpecImpl { 62 return transitionCache 63 .getOrPut(from) { mutableMapOf() } 64 .getOrPut(to) { mutableMapOf() } 65 .getOrPut(key) { findSpec(from, to, key) } 66 } 67 68 private fun findSpec(from: SceneKey, to: SceneKey, key: TransitionKey?): TransitionSpecImpl { 69 val spec = transition(from, to, key) { it.from == from && it.to == to } 70 if (spec != null) { 71 return spec 72 } 73 74 val reversed = transition(from, to, key) { it.from == to && it.to == from } 75 if (reversed != null) { 76 return reversed.reversed() 77 } 78 79 val relaxedSpec = 80 transition(from, to, key) { 81 (it.from == from && it.to == null) || (it.to == to && it.from == null) 82 } 83 if (relaxedSpec != null) { 84 return relaxedSpec 85 } 86 87 return transition(from, to, key) { 88 (it.from == to && it.to == null) || (it.to == from && it.from == null) 89 } 90 ?.reversed() 91 ?: defaultTransition(from, to) 92 } 93 94 private fun transition( 95 from: SceneKey, 96 to: SceneKey, 97 key: TransitionKey?, 98 filter: (TransitionSpecImpl) -> Boolean, 99 ): TransitionSpecImpl? { 100 var match: TransitionSpecImpl? = null 101 transitionSpecs.fastForEach { spec -> 102 if (spec.key == key && filter(spec)) { 103 if (match != null) { 104 error("Found multiple transition specs for transition $from => $to") 105 } 106 match = spec 107 } 108 } 109 return match 110 } 111 112 private fun defaultTransition(from: SceneKey, to: SceneKey) = 113 TransitionSpecImpl(key = null, from, to, TransformationSpec.EmptyProvider) 114 115 internal fun overscrollSpec(scene: SceneKey, orientation: Orientation): OverscrollSpecImpl? = 116 overscrollCache 117 .getOrPut(scene) { mutableMapOf() } 118 .getOrPut(orientation) { overscroll(scene, orientation) { it.scene == scene } } 119 120 private fun overscroll( 121 scene: SceneKey, 122 orientation: Orientation, 123 filter: (OverscrollSpecImpl) -> Boolean, 124 ): OverscrollSpecImpl? { 125 var match: OverscrollSpecImpl? = null 126 overscrollSpecs.fastForEach { spec -> 127 if (spec.orientation == orientation && filter(spec)) { 128 if (match != null) { 129 error("Found multiple overscroll specs for overscroll $scene") 130 } 131 match = spec 132 } 133 } 134 return match 135 } 136 137 companion object { 138 internal val DefaultSwipeSpec = 139 spring( 140 stiffness = Spring.StiffnessMediumLow, 141 visibilityThreshold = OffsetVisibilityThreshold, 142 ) 143 144 val Empty = 145 SceneTransitions( 146 defaultSwipeSpec = DefaultSwipeSpec, 147 transitionSpecs = emptyList(), 148 overscrollSpecs = emptyList(), 149 interruptionHandler = DefaultInterruptionHandler, 150 ) 151 } 152 } 153 154 /** The definition of a transition between [from] and [to]. */ 155 interface TransitionSpec { 156 /** The key of this [TransitionSpec]. */ 157 val key: TransitionKey? 158 159 /** 160 * The scene we are transitioning from. If `null`, this spec can be used to animate from any 161 * scene. 162 */ 163 val from: SceneKey? 164 165 /** 166 * The scene we are transitioning to. If `null`, this spec can be used to animate from any 167 * scene. 168 */ 169 val to: SceneKey? 170 171 /** 172 * Return a reversed version of this [TransitionSpec] for a transition going from [to] to 173 * [from]. 174 */ reversednull175 fun reversed(): TransitionSpec 176 177 /** 178 * The [TransformationSpec] associated to this [TransitionSpec]. 179 * 180 * Note that this is called once every a transition associated to this [TransitionSpec] is 181 * started. 182 */ 183 fun transformationSpec(): TransformationSpec 184 } 185 186 interface TransformationSpec { 187 /** 188 * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when 189 * the transition is triggered (i.e. it is not gesture-based). 190 */ 191 val progressSpec: AnimationSpec<Float> 192 193 /** 194 * The [SpringSpec] used to animate the associated transition progress when the transition was 195 * started by a swipe and is now animating back to a scene because the user lifted their finger. 196 * 197 * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used. 198 */ 199 val swipeSpec: SpringSpec<Float>? 200 201 /** 202 * The distance it takes for this transition to animate from 0% to 100% when it is driven by a 203 * [UserAction]. 204 * 205 * If `null`, a default distance will be used that depends on the [UserAction] performed. 206 */ 207 val distance: UserActionDistance? 208 209 /** The list of [Transformation] applied to elements during this transition. */ 210 val transformations: List<Transformation> 211 212 companion object { 213 internal val Empty = 214 TransformationSpecImpl( 215 progressSpec = snap(), 216 swipeSpec = null, 217 distance = null, 218 transformations = emptyList(), 219 ) 220 internal val EmptyProvider = { Empty } 221 } 222 } 223 224 internal class TransitionSpecImpl( 225 override val key: TransitionKey?, 226 override val from: SceneKey?, 227 override val to: SceneKey?, 228 private val transformationSpec: () -> TransformationSpecImpl, 229 ) : TransitionSpec { reversednull230 override fun reversed(): TransitionSpecImpl { 231 return TransitionSpecImpl( 232 key = key, 233 from = to, 234 to = from, 235 transformationSpec = { 236 val reverse = transformationSpec.invoke() 237 TransformationSpecImpl( 238 progressSpec = reverse.progressSpec, 239 swipeSpec = reverse.swipeSpec, 240 distance = reverse.distance, 241 transformations = reverse.transformations.map { it.reversed() } 242 ) 243 } 244 ) 245 } 246 transformationSpecnull247 override fun transformationSpec(): TransformationSpecImpl = this.transformationSpec.invoke() 248 } 249 250 /** The definition of the overscroll behavior of the [scene]. */ 251 interface OverscrollSpec { 252 /** The scene we are over scrolling. */ 253 val scene: SceneKey 254 255 /** The orientation of this [OverscrollSpec]. */ 256 val orientation: Orientation 257 258 /** The [TransformationSpec] associated to this [OverscrollSpec]. */ 259 val transformationSpec: TransformationSpec 260 } 261 262 internal class OverscrollSpecImpl( 263 override val scene: SceneKey, 264 override val orientation: Orientation, 265 override val transformationSpec: TransformationSpecImpl, 266 ) : OverscrollSpec 267 268 /** 269 * An implementation of [TransformationSpec] that allows the quick retrieval of an element 270 * [ElementTransformations]. 271 */ 272 internal class TransformationSpecImpl( 273 override val progressSpec: AnimationSpec<Float>, 274 override val swipeSpec: SpringSpec<Float>?, 275 override val distance: UserActionDistance?, 276 override val transformations: List<Transformation>, 277 ) : TransformationSpec { 278 private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>() 279 transformationsnull280 internal fun transformations(element: ElementKey, scene: SceneKey): ElementTransformations { 281 return cache 282 .getOrPut(element) { mutableMapOf() } 283 .getOrPut(scene) { computeTransformations(element, scene) } 284 } 285 286 /** Filter [transformations] to compute the [ElementTransformations] of [element]. */ computeTransformationsnull287 private fun computeTransformations( 288 element: ElementKey, 289 scene: SceneKey, 290 ): ElementTransformations { 291 var shared: SharedElementTransformation? = null 292 var offset: PropertyTransformation<Offset>? = null 293 var size: PropertyTransformation<IntSize>? = null 294 var drawScale: PropertyTransformation<Scale>? = null 295 var alpha: PropertyTransformation<Float>? = null 296 297 fun <T> onPropertyTransformation( 298 root: PropertyTransformation<T>, 299 current: PropertyTransformation<T> = root, 300 ) { 301 when (current) { 302 is Translate, 303 is OverscrollTranslate, 304 is EdgeTranslate, 305 is AnchoredTranslate -> { 306 throwIfNotNull(offset, element, name = "offset") 307 offset = root as PropertyTransformation<Offset> 308 } 309 is ScaleSize, 310 is AnchoredSize -> { 311 throwIfNotNull(size, element, name = "size") 312 size = root as PropertyTransformation<IntSize> 313 } 314 is DrawScale -> { 315 throwIfNotNull(drawScale, element, name = "drawScale") 316 drawScale = root as PropertyTransformation<Scale> 317 } 318 is Fade -> { 319 throwIfNotNull(alpha, element, name = "alpha") 320 alpha = root as PropertyTransformation<Float> 321 } 322 is RangedPropertyTransformation -> onPropertyTransformation(root, current.delegate) 323 } 324 } 325 326 transformations.fastForEach { transformation -> 327 if (!transformation.matcher.matches(element, scene)) { 328 return@fastForEach 329 } 330 331 when (transformation) { 332 is SharedElementTransformation -> { 333 throwIfNotNull(shared, element, name = "shared") 334 shared = transformation 335 } 336 is PropertyTransformation<*> -> onPropertyTransformation(transformation) 337 } 338 } 339 340 return ElementTransformations(shared, offset, size, drawScale, alpha) 341 } 342 throwIfNotNullnull343 private fun throwIfNotNull( 344 previous: Transformation?, 345 element: ElementKey, 346 name: String, 347 ) { 348 if (previous != null) { 349 error("$element has multiple $name transformations") 350 } 351 } 352 } 353 354 /** The transformations of an element during a transition. */ 355 internal class ElementTransformations( 356 val shared: SharedElementTransformation?, 357 val offset: PropertyTransformation<Offset>?, 358 val size: PropertyTransformation<IntSize>?, 359 val drawScale: PropertyTransformation<Scale>?, 360 val alpha: PropertyTransformation<Float>?, 361 ) 362