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