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.content.state.TransitionState 29 import com.android.compose.animation.scene.transformation.PropertyTransformation 30 import com.android.compose.animation.scene.transformation.SharedElementTransformation 31 import com.android.compose.animation.scene.transformation.TransformationMatcher 32 import com.android.compose.animation.scene.transformation.TransformationWithRange 33 34 /** The transitions configuration of a [SceneTransitionLayout]. */ 35 class SceneTransitions 36 internal constructor( 37 internal val defaultSwipeSpec: SpringSpec<Float>, 38 internal val transitionSpecs: List<TransitionSpecImpl>, 39 internal val overscrollSpecs: List<OverscrollSpecImpl>, 40 internal val interruptionHandler: InterruptionHandler, 41 internal val defaultProgressConverter: ProgressConverter, 42 ) { 43 private val transitionCache = 44 mutableMapOf< 45 ContentKey, 46 MutableMap<ContentKey, MutableMap<TransitionKey?, TransitionSpecImpl>>, 47 >() 48 49 private val overscrollCache = 50 mutableMapOf<ContentKey, MutableMap<Orientation, OverscrollSpecImpl?>>() 51 52 internal fun transitionSpec( 53 from: ContentKey, 54 to: ContentKey, 55 key: TransitionKey?, 56 ): TransitionSpecImpl { 57 return transitionCache 58 .getOrPut(from) { mutableMapOf() } 59 .getOrPut(to) { mutableMapOf() } 60 .getOrPut(key) { findSpec(from, to, key) } 61 } 62 63 private fun findSpec( 64 from: ContentKey, 65 to: ContentKey, 66 key: TransitionKey?, 67 ): TransitionSpecImpl { 68 val spec = transition(from, to, key) { it.from == from && it.to == to } 69 if (spec != null) { 70 return spec 71 } 72 73 val reversed = transition(from, to, key) { it.from == to && it.to == from } 74 if (reversed != null) { 75 return reversed.reversed() 76 } 77 78 val relaxedSpec = 79 transition(from, to, key) { 80 (it.from == from && it.to == null) || (it.to == to && it.from == null) 81 } 82 if (relaxedSpec != null) { 83 return relaxedSpec 84 } 85 86 val relaxedReversed = 87 transition(from, to, key) { 88 (it.from == to && it.to == null) || (it.to == from && it.from == null) 89 } 90 if (relaxedReversed != null) { 91 return relaxedReversed.reversed() 92 } 93 94 return if (key != null) { 95 findSpec(from, to, null) 96 } else { 97 defaultTransition(from, to) 98 } 99 } 100 101 private fun transition( 102 from: ContentKey, 103 to: ContentKey, 104 key: TransitionKey?, 105 filter: (TransitionSpecImpl) -> Boolean, 106 ): TransitionSpecImpl? { 107 var match: TransitionSpecImpl? = null 108 transitionSpecs.fastForEach { spec -> 109 if (spec.key == key && filter(spec)) { 110 if (match != null) { 111 error("Found multiple transition specs for transition $from => $to") 112 } 113 match = spec 114 } 115 } 116 return match 117 } 118 119 private fun defaultTransition(from: ContentKey, to: ContentKey) = 120 TransitionSpecImpl(key = null, from, to, null, null, TransformationSpec.EmptyProvider) 121 122 internal fun overscrollSpec(scene: ContentKey, orientation: Orientation): OverscrollSpecImpl? = 123 overscrollCache 124 .getOrPut(scene) { mutableMapOf() } 125 .getOrPut(orientation) { overscroll(scene, orientation) { it.content == scene } } 126 127 private fun overscroll( 128 scene: ContentKey, 129 orientation: Orientation, 130 filter: (OverscrollSpecImpl) -> Boolean, 131 ): OverscrollSpecImpl? { 132 var match: OverscrollSpecImpl? = null 133 overscrollSpecs.fastForEach { spec -> 134 if (spec.orientation == orientation && filter(spec)) { 135 if (match != null) { 136 error("Found multiple overscroll specs for overscroll $scene") 137 } 138 match = spec 139 } 140 } 141 return match 142 } 143 144 companion object { 145 internal val DefaultSwipeSpec = 146 spring( 147 stiffness = Spring.StiffnessMediumLow, 148 dampingRatio = Spring.DampingRatioLowBouncy, 149 visibilityThreshold = OffsetVisibilityThreshold, 150 ) 151 152 val Empty = 153 SceneTransitions( 154 defaultSwipeSpec = DefaultSwipeSpec, 155 transitionSpecs = emptyList(), 156 overscrollSpecs = emptyList(), 157 interruptionHandler = DefaultInterruptionHandler, 158 defaultProgressConverter = ProgressConverter.Default, 159 ) 160 } 161 } 162 163 /** The definition of a transition between [from] and [to]. */ 164 internal interface TransitionSpec { 165 /** The key of this [TransitionSpec]. */ 166 val key: TransitionKey? 167 168 /** 169 * The content we are transitioning from. If `null`, this spec can be used to animate from any 170 * content. 171 */ 172 val from: ContentKey? 173 174 /** 175 * The content we are transitioning to. If `null`, this spec can be used to animate from any 176 * content. 177 */ 178 val to: ContentKey? 179 180 /** 181 * Return a reversed version of this [TransitionSpec] for a transition going from [to] to 182 * [from]. 183 */ reversednull184 fun reversed(): TransitionSpec 185 186 /** 187 * The [TransformationSpec] associated to this [TransitionSpec] for the given [transition]. 188 * 189 * Note that this is called once whenever a transition associated to this [TransitionSpec] is 190 * started. 191 */ 192 fun transformationSpec(transition: TransitionState.Transition): TransformationSpec 193 194 /** 195 * The preview [TransformationSpec] associated to this [TransitionSpec] for the given 196 * [transition]. 197 * 198 * Note that this is called once whenever a transition associated to this [TransitionSpec] is 199 * started. 200 */ 201 fun previewTransformationSpec(transition: TransitionState.Transition): TransformationSpec? 202 } 203 204 internal interface TransformationSpec { 205 /** 206 * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when 207 * the transition is triggered (i.e. it is not gesture-based). 208 */ 209 val progressSpec: AnimationSpec<Float> 210 211 /** 212 * The [SpringSpec] used to animate the associated transition progress when the transition was 213 * started by a swipe and is now animating back to a scene because the user lifted their finger. 214 * 215 * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used. 216 */ 217 val swipeSpec: SpringSpec<Float>? 218 219 /** 220 * The distance it takes for this transition to animate from 0% to 100% when it is driven by a 221 * [UserAction]. 222 * 223 * If `null`, a default distance will be used that depends on the [UserAction] performed. 224 */ 225 val distance: UserActionDistance? 226 227 /** The list of [TransformationMatcher] applied to elements during this transformation. */ 228 val transformationMatchers: List<TransformationMatcher> 229 230 companion object { 231 internal val Empty = 232 TransformationSpecImpl( 233 progressSpec = snap(), 234 swipeSpec = null, 235 distance = null, 236 transformationMatchers = emptyList(), 237 ) 238 internal val EmptyProvider = { _: TransitionState.Transition -> Empty } 239 } 240 } 241 242 internal class TransitionSpecImpl( 243 override val key: TransitionKey?, 244 override val from: ContentKey?, 245 override val to: ContentKey?, 246 private val previewTransformationSpec: 247 ((TransitionState.Transition) -> TransformationSpecImpl)? = 248 null, 249 private val reversePreviewTransformationSpec: 250 ((TransitionState.Transition) -> TransformationSpecImpl)? = 251 null, 252 private val transformationSpec: (TransitionState.Transition) -> TransformationSpecImpl, 253 ) : TransitionSpec { reversednull254 override fun reversed(): TransitionSpecImpl { 255 return TransitionSpecImpl( 256 key = key, 257 from = to, 258 to = from, 259 previewTransformationSpec = reversePreviewTransformationSpec, 260 reversePreviewTransformationSpec = previewTransformationSpec, 261 transformationSpec = { transition -> 262 val reverse = transformationSpec.invoke(transition) 263 TransformationSpecImpl( 264 progressSpec = reverse.progressSpec, 265 swipeSpec = reverse.swipeSpec, 266 distance = reverse.distance, 267 transformationMatchers = 268 reverse.transformationMatchers.map { 269 TransformationMatcher( 270 matcher = it.matcher, 271 factory = it.factory, 272 range = it.range?.reversed(), 273 ) 274 }, 275 ) 276 }, 277 ) 278 } 279 transformationSpecnull280 override fun transformationSpec( 281 transition: TransitionState.Transition 282 ): TransformationSpecImpl = transformationSpec.invoke(transition) 283 284 override fun previewTransformationSpec( 285 transition: TransitionState.Transition 286 ): TransformationSpecImpl? = previewTransformationSpec?.invoke(transition) 287 } 288 289 /** The definition of the overscroll behavior of the [content]. */ 290 internal interface OverscrollSpec { 291 /** The scene we are over scrolling. */ 292 val content: ContentKey 293 294 /** The orientation of this [OverscrollSpec]. */ 295 val orientation: Orientation 296 297 /** The [TransformationSpec] associated to this [OverscrollSpec]. */ 298 val transformationSpec: TransformationSpec 299 300 /** 301 * Function that takes a linear overscroll progress value ranging from 0 to +/- infinity and 302 * outputs the desired **overscroll progress value**. 303 * 304 * When the progress value is: 305 * - 0, the user is not overscrolling. 306 * - 1, the user overscrolled by exactly the [OverscrollBuilder.distance]. 307 * - Greater than 1, the user overscrolled more than the [OverscrollBuilder.distance]. 308 */ 309 val progressConverter: ProgressConverter? 310 } 311 312 internal class OverscrollSpecImpl( 313 override val content: ContentKey, 314 override val orientation: Orientation, 315 override val transformationSpec: TransformationSpecImpl, 316 override val progressConverter: ProgressConverter?, 317 ) : OverscrollSpec 318 319 /** 320 * An implementation of [TransformationSpec] that allows the quick retrieval of an element 321 * [ElementTransformations]. 322 */ 323 internal class TransformationSpecImpl( 324 override val progressSpec: AnimationSpec<Float>, 325 override val swipeSpec: SpringSpec<Float>?, 326 override val distance: UserActionDistance?, 327 override val transformationMatchers: List<TransformationMatcher>, 328 ) : TransformationSpec { 329 private val cache = mutableMapOf<ElementKey, MutableMap<ContentKey, ElementTransformations>>() 330 transformationsnull331 internal fun transformations(element: ElementKey, content: ContentKey): ElementTransformations { 332 return cache 333 .getOrPut(element) { mutableMapOf() } 334 .getOrPut(content) { computeTransformations(element, content) } 335 } 336 337 /** Filter [transformationMatchers] to compute the [ElementTransformations] of [element]. */ computeTransformationsnull338 private fun computeTransformations( 339 element: ElementKey, 340 content: ContentKey, 341 ): ElementTransformations { 342 var shared: TransformationWithRange<SharedElementTransformation>? = null 343 var offset: TransformationWithRange<PropertyTransformation<Offset>>? = null 344 var size: TransformationWithRange<PropertyTransformation<IntSize>>? = null 345 var drawScale: TransformationWithRange<PropertyTransformation<Scale>>? = null 346 var alpha: TransformationWithRange<PropertyTransformation<Float>>? = null 347 348 transformationMatchers.fastForEach { transformationMatcher -> 349 if (!transformationMatcher.matcher.matches(element, content)) { 350 return@fastForEach 351 } 352 353 val transformation = transformationMatcher.factory.create() 354 val property = 355 when (transformation) { 356 is SharedElementTransformation -> { 357 throwIfNotNull(shared, element, name = "shared") 358 shared = 359 TransformationWithRange(transformation, transformationMatcher.range) 360 return@fastForEach 361 } 362 is PropertyTransformation<*> -> transformation.property 363 } 364 365 when (property) { 366 is PropertyTransformation.Property.Offset -> { 367 throwIfNotNull(offset, element, name = "offset") 368 offset = 369 TransformationWithRange( 370 transformation as PropertyTransformation<Offset>, 371 transformationMatcher.range, 372 ) 373 } 374 is PropertyTransformation.Property.Size -> { 375 throwIfNotNull(size, element, name = "size") 376 size = 377 TransformationWithRange( 378 transformation as PropertyTransformation<IntSize>, 379 transformationMatcher.range, 380 ) 381 } 382 is PropertyTransformation.Property.Scale -> { 383 throwIfNotNull(drawScale, element, name = "drawScale") 384 drawScale = 385 TransformationWithRange( 386 transformation as PropertyTransformation<Scale>, 387 transformationMatcher.range, 388 ) 389 } 390 is PropertyTransformation.Property.Alpha -> { 391 throwIfNotNull(alpha, element, name = "alpha") 392 alpha = 393 TransformationWithRange( 394 transformation as PropertyTransformation<Float>, 395 transformationMatcher.range, 396 ) 397 } 398 } 399 } 400 401 return ElementTransformations(shared, offset, size, drawScale, alpha) 402 } 403 throwIfNotNullnull404 private fun throwIfNotNull( 405 previous: TransformationWithRange<*>?, 406 element: ElementKey, 407 name: String, 408 ) { 409 if (previous != null) { 410 error("$element has multiple $name transformations") 411 } 412 } 413 } 414 415 /** The transformations of an element during a transition. */ 416 internal class ElementTransformations( 417 val shared: TransformationWithRange<SharedElementTransformation>?, 418 val offset: TransformationWithRange<PropertyTransformation<Offset>>?, 419 val size: TransformationWithRange<PropertyTransformation<IntSize>>?, 420 val drawScale: TransformationWithRange<PropertyTransformation<Scale>>?, 421 val alpha: TransformationWithRange<PropertyTransformation<Float>>?, 422 ) 423