1 /* 2 * Copyright (C) 2024 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.content.state 18 19 import androidx.compose.animation.core.Animatable 20 import androidx.compose.animation.core.AnimationVector1D 21 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 22 import androidx.compose.runtime.Stable 23 import androidx.compose.runtime.derivedStateOf 24 import androidx.compose.runtime.getValue 25 import androidx.compose.runtime.mutableStateOf 26 import androidx.compose.runtime.setValue 27 import com.android.compose.animation.scene.ContentKey 28 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState 29 import com.android.compose.animation.scene.OverlayKey 30 import com.android.compose.animation.scene.ProgressVisibilityThreshold 31 import com.android.compose.animation.scene.SceneKey 32 import com.android.compose.animation.scene.SceneTransitionLayoutImpl 33 import com.android.compose.animation.scene.TransformationSpec 34 import com.android.compose.animation.scene.TransformationSpecImpl 35 import com.android.compose.animation.scene.TransitionKey 36 import com.android.internal.jank.Cuj.CujType 37 import com.android.mechanics.GestureContext 38 import kotlinx.coroutines.CoroutineScope 39 import kotlinx.coroutines.coroutineScope 40 import kotlinx.coroutines.launch 41 42 /** The state associated to a [SceneTransitionLayout] at some specific point in time. */ 43 @Stable 44 sealed interface TransitionState { 45 /** 46 * The current effective scene. If a new scene transition was triggered, it would start from 47 * this scene. 48 * 49 * For instance, when swiping from scene A to scene B, the [currentScene] is A when the swipe 50 * gesture starts, but then if the user flings their finger and commits the transition to scene 51 * B, then [currentScene] becomes scene B even if the transition is not finished yet and is 52 * still animating to settle to scene B. 53 */ 54 val currentScene: SceneKey 55 56 /** 57 * The current set of overlays. This represents the set of overlays that will be visible on 58 * screen once all transitions are finished. 59 * 60 * @see MutableSceneTransitionLayoutState.showOverlay 61 * @see MutableSceneTransitionLayoutState.hideOverlay 62 * @see MutableSceneTransitionLayoutState.replaceOverlay 63 */ 64 val currentOverlays: Set<OverlayKey> 65 66 /** The scene [currentScene] is idle. */ 67 data class Idle( 68 override val currentScene: SceneKey, 69 override val currentOverlays: Set<OverlayKey> = emptySet(), 70 ) : TransitionState 71 72 sealed class Transition( 73 val fromContent: ContentKey, 74 val toContent: ContentKey, 75 val replacedTransition: Transition? = null, 76 ) : TransitionState { 77 /** A transition animating between [fromScene] and [toScene]. */ 78 abstract class ChangeScene( 79 /** The scene this transition is starting from. Can't be the same as toScene */ 80 val fromScene: SceneKey, 81 82 /** The scene this transition is going to. Can't be the same as fromScene */ 83 val toScene: SceneKey, 84 85 /** The transition that `this` transition is replacing, if any. */ 86 replacedTransition: Transition? = null, 87 ) : Transition(fromScene, toScene, replacedTransition) { 88 final override val currentOverlays: Set<OverlayKey> 89 get() { 90 // The set of overlays does not change in a [ChangeCurrentScene] transition. 91 return currentOverlaysWhenTransitionStarted 92 } 93 toStringnull94 override fun toString(): String { 95 return "ChangeScene(fromScene=$fromScene, toScene=$toScene)" 96 } 97 } 98 99 /** 100 * A transition that is animating one or more overlays and for which [currentOverlays] will 101 * change over the course of the transition. 102 */ 103 sealed class OverlayTransition( 104 fromContent: ContentKey, 105 toContent: ContentKey, 106 replacedTransition: Transition?, 107 ) : Transition(fromContent, toContent, replacedTransition) { 108 final override val currentScene: SceneKey 109 get() { 110 // The current scene does not change during overlay transitions. 111 return currentSceneWhenTransitionStarted 112 } 113 114 // Note: We use deriveStateOf() so that the computed set is cached and reused when the 115 // inputs of the computations don't change, to avoid recomputing and allocating a new 116 // set every time currentOverlays is called (which is every frame and for each element). <lambda>null117 final override val currentOverlays: Set<OverlayKey> by derivedStateOf { 118 computeCurrentOverlays() 119 } 120 computeCurrentOverlaysnull121 protected abstract fun computeCurrentOverlays(): Set<OverlayKey> 122 } 123 124 /** The [overlay] is either showing from [fromOrToScene] or hiding into [fromOrToScene]. */ 125 abstract class ShowOrHideOverlay( 126 val overlay: OverlayKey, 127 val fromOrToScene: SceneKey, 128 fromContent: ContentKey, 129 toContent: ContentKey, 130 replacedTransition: Transition? = null, 131 ) : OverlayTransition(fromContent, toContent, replacedTransition) { 132 /** 133 * Whether [overlay] is effectively shown. For instance, this will be `false` when 134 * starting a swipe transition to show [overlay] and will be `true` only once the swipe 135 * transition is committed. 136 */ 137 abstract val isEffectivelyShown: Boolean 138 139 init { 140 check( 141 (fromContent == fromOrToScene && toContent == overlay) || 142 (fromContent == overlay && toContent == fromOrToScene) 143 ) 144 } 145 146 final override fun computeCurrentOverlays(): Set<OverlayKey> { 147 return if (isEffectivelyShown) { 148 currentOverlaysWhenTransitionStarted + overlay 149 } else { 150 currentOverlaysWhenTransitionStarted - overlay 151 } 152 } 153 154 override fun toString(): String { 155 val isShowing = overlay == toContent 156 return "ShowOrHideOverlay(overlay=$overlay, fromOrToScene=$fromOrToScene, " + 157 "isShowing=$isShowing)" 158 } 159 } 160 161 /** We are transitioning from [fromOverlay] to [toOverlay]. */ 162 abstract class ReplaceOverlay( 163 val fromOverlay: OverlayKey, 164 val toOverlay: OverlayKey, 165 replacedTransition: Transition? = null, 166 ) : 167 OverlayTransition( 168 fromContent = fromOverlay, 169 toContent = toOverlay, 170 replacedTransition, 171 ) { 172 /** 173 * The current effective overlay, either [fromOverlay] or [toOverlay]. For instance, 174 * this will be [fromOverlay] when starting a swipe transition that replaces 175 * [fromOverlay] by [toOverlay] and will [toOverlay] once the swipe transition is 176 * committed. 177 */ 178 abstract val effectivelyShownOverlay: OverlayKey 179 180 init { 181 check(fromOverlay != toOverlay) 182 } 183 computeCurrentOverlaysnull184 final override fun computeCurrentOverlays(): Set<OverlayKey> { 185 return when (effectivelyShownOverlay) { 186 fromOverlay -> 187 computeCurrentOverlays(include = fromOverlay, exclude = toOverlay) 188 toOverlay -> computeCurrentOverlays(include = toOverlay, exclude = fromOverlay) 189 else -> 190 error( 191 "effectivelyShownOverlay=$effectivelyShownOverlay, should be " + 192 "equal to fromOverlay=$fromOverlay or toOverlay=$toOverlay" 193 ) 194 } 195 } 196 computeCurrentOverlaysnull197 private fun computeCurrentOverlays( 198 include: OverlayKey, 199 exclude: OverlayKey, 200 ): Set<OverlayKey> { 201 return buildSet { 202 addAll(currentOverlaysWhenTransitionStarted) 203 remove(exclude) 204 add(include) 205 } 206 } 207 toStringnull208 override fun toString(): String { 209 return "ReplaceOverlay(fromOverlay=$fromOverlay, toOverlay=$toOverlay)" 210 } 211 } 212 213 /** 214 * The current scene and overlays observed right when this transition started. These are set 215 * when this transition is started in 216 * [com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl.startTransition]. 217 */ 218 internal lateinit var currentSceneWhenTransitionStarted: SceneKey 219 internal lateinit var currentOverlaysWhenTransitionStarted: Set<OverlayKey> 220 221 /** 222 * The key of this transition. This should usually be null, but it can be specified to use a 223 * specific set of transformations associated to this transition. 224 */ 225 open val key: TransitionKey? = null 226 227 /** 228 * The progress of the transition. This is usually in the `[0; 1]` range, but it can also be 229 * less than `0` or greater than `1` when using transitions with a spring AnimationSpec or 230 * when flinging quickly during a swipe gesture. 231 */ 232 abstract val progress: Float 233 234 /** The current velocity of [progress], in progress units. */ 235 abstract val progressVelocity: Float 236 237 /** Whether the transition was triggered by user input rather than being programmatic. */ 238 abstract val isInitiatedByUserInput: Boolean 239 240 /** Whether user input is currently driving the transition. */ 241 abstract val isUserInputOngoing: Boolean 242 243 /** Additional gesture context whenever the transition is driven by a user gesture. */ 244 abstract val gestureContext: GestureContext? 245 246 /** 247 * True when the transition reached the end and the progress won't be updated anymore. 248 * 249 * [isProgressStable] will be `true` before this [Transition] is completed while there are 250 * still custom transition animations settling. 251 */ 252 var isProgressStable: Boolean by mutableStateOf(false) 253 private set 254 255 /** The CUJ covered by this transition. */ 256 @CujType 257 val cuj: Int? 258 get() = _cuj 259 260 /** 261 * The progress of the preview transition. This is usually in the `[0; 1]` range, but it can 262 * also be less than `0` or greater than `1` when using transitions with a spring 263 * AnimationSpec or when flinging quickly during a swipe gesture. 264 */ 265 internal open val previewProgress: Float = 0f 266 267 /** The current velocity of [previewProgress], in progress units. */ 268 internal open val previewProgressVelocity: Float = 0f 269 270 /** Whether the transition is currently in the preview stage */ 271 internal open val isInPreviewStage: Boolean = false 272 273 /** 274 * The current [TransformationSpecImpl] and other values associated to this transition from 275 * the spec. 276 * 277 * Important: These will be set exactly once, when this transition is 278 * [started][MutableSceneTransitionLayoutStateImpl.startTransition]. 279 */ 280 internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty 281 internal var previewTransformationSpec: TransformationSpecImpl? = null 282 internal var _cuj: Int? = null 283 284 /** 285 * An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden 286 * jump of values when this transitions interrupts another one. 287 */ 288 private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null 289 290 /** 291 * The coroutine scope associated to this transition. 292 * 293 * This coroutine scope can be used to launch animations associated to this transition, 294 * which will not finish until at least one animation/job is still running in the scope. 295 * 296 * Important: Make sure to never launch long-running jobs in this scope, otherwise the 297 * transition will never be considered as finished. 298 */ 299 internal val coroutineScope: CoroutineScope 300 get() = 301 _coroutineScope 302 ?: error( 303 "Transition.coroutineScope can only be accessed once the transition was " + 304 "started " 305 ) 306 307 private var _coroutineScope: CoroutineScope? = null 308 309 init { 310 check(fromContent != toContent) 311 check( 312 replacedTransition == null || 313 (replacedTransition.fromContent == fromContent && 314 replacedTransition.toContent == toContent) 315 ) 316 } 317 318 /** 319 * Whether we are transitioning. If [from] or [to] is empty, we will also check that they 320 * match the contents we are animating from and/or to. 321 */ isTransitioningnull322 fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean { 323 return (from == null || fromContent == from) && (to == null || toContent == to) 324 } 325 326 /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */ isTransitioningBetweennull327 fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean { 328 return isTransitioning(from = content, to = other) || 329 isTransitioning(from = other, to = content) 330 } 331 332 /** Whether we are transitioning from or to [content]. */ isTransitioningFromOrTonull333 fun isTransitioningFromOrTo(content: ContentKey): Boolean { 334 return fromContent == content || toContent == content 335 } 336 337 /** 338 * Return [progress] if [content] is equal to [toContent], `1f - progress` if [content] is 339 * equal to [fromContent], and throw otherwise. 340 */ progressTonull341 fun progressTo(content: ContentKey): Float { 342 return when (content) { 343 toContent -> progress 344 fromContent -> 1f - progress 345 else -> 346 throw IllegalArgumentException( 347 "content ($content) should be either toContent ($toContent) or " + 348 "fromContent ($fromContent)" 349 ) 350 } 351 } 352 353 /** Whether [fromContent] is effectively the current content of the transition. */ isFromCurrentContentnull354 internal fun isFromCurrentContent() = isCurrentContent(expectedFrom = true) 355 356 /** Whether [toContent] is effectively the current content of the transition. */ 357 internal fun isToCurrentContent() = isCurrentContent(expectedFrom = false) 358 359 private fun isCurrentContent(expectedFrom: Boolean): Boolean { 360 val expectedContent = if (expectedFrom) fromContent else toContent 361 return when (this) { 362 is ChangeScene -> currentScene == expectedContent 363 is ReplaceOverlay -> effectivelyShownOverlay == expectedContent 364 is ShowOrHideOverlay -> isEffectivelyShown == (expectedContent == overlay) 365 } 366 } 367 368 /** Run this transition and return once it is finished. */ runnull369 protected abstract suspend fun run() 370 371 /** 372 * Freeze this transition state so that neither [currentScene] nor [currentOverlays] will 373 * change in the future, and animate the progress towards that state. For instance, a 374 * [Transition.ChangeScene] should animate the progress to 0f if its [currentScene] is equal 375 * to its [fromScene][Transition.ChangeScene.fromScene] or animate it to 1f if its equal to 376 * its [toScene][Transition.ChangeScene.toScene]. 377 * 378 * This is called when this transition is interrupted (replaced) by another transition. 379 */ 380 abstract fun freezeAndAnimateToCurrentState() 381 382 internal suspend fun runInternal() { 383 check(_coroutineScope == null) { "A Transition can be started only once." } 384 coroutineScope { 385 _coroutineScope = this 386 try { 387 run() 388 } finally { 389 isProgressStable = true 390 } 391 } 392 } 393 interruptionProgressnull394 internal open fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float { 395 if (replacedTransition != null) { 396 return replacedTransition.interruptionProgress(layoutImpl) 397 } 398 399 fun create(): Animatable<Float, AnimationVector1D> { 400 val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold) 401 layoutImpl.animationScope.launch { 402 @OptIn(ExperimentalMaterial3ExpressiveApi::class) 403 animatable.animateTo( 404 targetValue = 0f, 405 // Quickly animate (use fast) the current transition and without bounces 406 // (use effects). A new transition will start soon. 407 animationSpec = layoutImpl.state.motionScheme.fastEffectsSpec(), 408 ) 409 } 410 411 return animatable 412 } 413 414 val animatable = interruptionDecay ?: create().also { interruptionDecay = it } 415 return animatable.value 416 } 417 } 418 419 companion object { 420 const val DistanceUnspecified = 0f 421 } 422 } 423