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.animation.core.Spring 22 import androidx.compose.animation.core.spring 23 import androidx.compose.foundation.gestures.Orientation 24 import androidx.compose.runtime.Stable 25 import androidx.compose.runtime.State 26 import androidx.compose.runtime.derivedStateOf 27 import androidx.compose.runtime.getValue 28 import com.android.compose.animation.scene.ContentKey 29 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState 30 import com.android.compose.animation.scene.OverlayKey 31 import com.android.compose.animation.scene.OverscrollSpecImpl 32 import com.android.compose.animation.scene.ProgressVisibilityThreshold 33 import com.android.compose.animation.scene.SceneKey 34 import com.android.compose.animation.scene.SceneTransitionLayoutImpl 35 import com.android.compose.animation.scene.TransformationSpec 36 import com.android.compose.animation.scene.TransformationSpecImpl 37 import com.android.compose.animation.scene.TransitionKey 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 /** 244 * The progress of the preview transition. This is usually in the `[0; 1]` range, but it can 245 * also be less than `0` or greater than `1` when using transitions with a spring 246 * AnimationSpec or when flinging quickly during a swipe gesture. 247 */ 248 internal open val previewProgress: Float = 0f 249 250 /** The current velocity of [previewProgress], in progress units. */ 251 internal open val previewProgressVelocity: Float = 0f 252 253 /** Whether the transition is currently in the preview stage */ 254 internal open val isInPreviewStage: Boolean = false 255 256 /** 257 * The current [TransformationSpecImpl] and [OverscrollSpecImpl] associated to this 258 * transition. 259 * 260 * Important: These will be set exactly once, when this transition is 261 * [started][MutableSceneTransitionLayoutStateImpl.startTransition]. 262 */ 263 internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty 264 internal var previewTransformationSpec: TransformationSpecImpl? = null 265 private var fromOverscrollSpec: OverscrollSpecImpl? = null 266 private var toOverscrollSpec: OverscrollSpecImpl? = null 267 268 /** 269 * The current [OverscrollSpecImpl], if this transition is currently overscrolling. 270 * 271 * Note: This is backed by a State<OverscrollSpecImpl?> because the overscroll spec is 272 * derived from progress, and we don't want readers of currentOverscrollSpec to recompose 273 * every time progress is changed. 274 */ 275 private val _currentOverscrollSpec: State<OverscrollSpecImpl?>? = 276 if (this !is HasOverscrollProperties) { 277 null 278 } else { <lambda>null279 derivedStateOf { 280 val progress = progress 281 val bouncingContent = bouncingContent 282 when { 283 progress < 0f || bouncingContent == fromContent -> fromOverscrollSpec 284 progress > 1f || bouncingContent == toContent -> toOverscrollSpec 285 else -> null 286 } 287 } 288 } 289 internal val currentOverscrollSpec: OverscrollSpecImpl? 290 get() = _currentOverscrollSpec?.value 291 292 /** 293 * An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden 294 * jump of values when this transitions interrupts another one. 295 */ 296 private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null 297 298 /** 299 * The coroutine scope associated to this transition. 300 * 301 * This coroutine scope can be used to launch animations associated to this transition, 302 * which will not finish until at least one animation/job is still running in the scope. 303 * 304 * Important: Make sure to never launch long-running jobs in this scope, otherwise the 305 * transition will never be considered as finished. 306 */ 307 internal val coroutineScope: CoroutineScope 308 get() = 309 _coroutineScope 310 ?: error( 311 "Transition.coroutineScope can only be accessed once the transition was " + 312 "started " 313 ) 314 315 private var _coroutineScope: CoroutineScope? = null 316 317 init { 318 check(fromContent != toContent) 319 check( 320 replacedTransition == null || 321 (replacedTransition.fromContent == fromContent && 322 replacedTransition.toContent == toContent) 323 ) 324 } 325 326 /** 327 * Whether we are transitioning. If [from] or [to] is empty, we will also check that they 328 * match the contents we are animating from and/or to. 329 */ isTransitioningnull330 fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean { 331 return (from == null || fromContent == from) && (to == null || toContent == to) 332 } 333 334 /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */ isTransitioningBetweennull335 fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean { 336 return isTransitioning(from = content, to = other) || 337 isTransitioning(from = other, to = content) 338 } 339 340 /** Whether we are transitioning from or to [content]. */ isTransitioningFromOrTonull341 fun isTransitioningFromOrTo(content: ContentKey): Boolean { 342 return fromContent == content || toContent == content 343 } 344 345 /** 346 * Return [progress] if [content] is equal to [toContent], `1f - progress` if [content] is 347 * equal to [fromContent], and throw otherwise. 348 */ progressTonull349 fun progressTo(content: ContentKey): Float { 350 return when (content) { 351 toContent -> progress 352 fromContent -> 1f - progress 353 else -> 354 throw IllegalArgumentException( 355 "content ($content) should be either toContent ($toContent) or " + 356 "fromContent ($fromContent)" 357 ) 358 } 359 } 360 361 /** Whether [fromContent] is effectively the current content of the transition. */ isFromCurrentContentnull362 internal fun isFromCurrentContent() = isCurrentContent(expectedFrom = true) 363 364 /** Whether [toContent] is effectively the current content of the transition. */ 365 internal fun isToCurrentContent() = isCurrentContent(expectedFrom = false) 366 367 private fun isCurrentContent(expectedFrom: Boolean): Boolean { 368 val expectedContent = if (expectedFrom) fromContent else toContent 369 return when (this) { 370 is ChangeScene -> currentScene == expectedContent 371 is ReplaceOverlay -> effectivelyShownOverlay == expectedContent 372 is ShowOrHideOverlay -> isEffectivelyShown == (expectedContent == overlay) 373 } 374 } 375 376 /** Run this transition and return once it is finished. */ runnull377 protected abstract suspend fun run() 378 379 /** 380 * Freeze this transition state so that neither [currentScene] nor [currentOverlays] will 381 * change in the future, and animate the progress towards that state. For instance, a 382 * [Transition.ChangeScene] should animate the progress to 0f if its [currentScene] is equal 383 * to its [fromScene][Transition.ChangeScene.fromScene] or animate it to 1f if its equal to 384 * its [toScene][Transition.ChangeScene.toScene]. 385 * 386 * This is called when this transition is interrupted (replaced) by another transition. 387 */ 388 abstract fun freezeAndAnimateToCurrentState() 389 390 internal suspend fun runInternal() { 391 check(_coroutineScope == null) { "A Transition can be started only once." } 392 coroutineScope { 393 _coroutineScope = this 394 run() 395 } 396 } 397 updateOverscrollSpecsnull398 internal fun updateOverscrollSpecs( 399 fromSpec: OverscrollSpecImpl?, 400 toSpec: OverscrollSpecImpl?, 401 ) { 402 fromOverscrollSpec = fromSpec 403 toOverscrollSpec = toSpec 404 } 405 406 /** Returns if the [progress] value of this transition can go beyond range `[0; 1]` */ isWithinProgressRangenull407 internal fun isWithinProgressRange(progress: Float): Boolean { 408 // If the properties are missing we assume that every [Transition] can overscroll 409 if (this !is HasOverscrollProperties) return true 410 // [OverscrollSpec] for the current scene, even if it hasn't started overscrolling yet. 411 val specForCurrentScene = 412 when { 413 progress <= 0f -> fromOverscrollSpec 414 progress >= 1f -> toOverscrollSpec 415 else -> null 416 } ?: return true 417 418 return specForCurrentScene.transformationSpec.transformationMatchers.isNotEmpty() 419 } 420 interruptionProgressnull421 internal open fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float { 422 if (replacedTransition != null) { 423 return replacedTransition.interruptionProgress(layoutImpl) 424 } 425 426 fun create(): Animatable<Float, AnimationVector1D> { 427 val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold) 428 layoutImpl.animationScope.launch { 429 val swipeSpec = layoutImpl.state.transitions.defaultSwipeSpec 430 val progressSpec = 431 spring( 432 stiffness = swipeSpec.stiffness, 433 dampingRatio = Spring.DampingRatioNoBouncy, 434 visibilityThreshold = ProgressVisibilityThreshold, 435 ) 436 animatable.animateTo(0f, progressSpec) 437 } 438 439 return animatable 440 } 441 442 val animatable = interruptionDecay ?: create().also { interruptionDecay = it } 443 return animatable.value 444 } 445 } 446 447 interface HasOverscrollProperties { 448 /** 449 * The position of the [Transition.toContent]. 450 * 451 * Used to understand the direction of the overscroll. 452 */ 453 val isUpOrLeft: Boolean 454 455 /** 456 * The relative orientation between [Transition.fromContent] and [Transition.toContent]. 457 * 458 * Used to understand the orientation of the overscroll. 459 */ 460 val orientation: Orientation 461 462 /** 463 * Return the absolute distance between fromScene and toScene, if available, otherwise 464 * [DistanceUnspecified]. 465 */ 466 val absoluteDistance: Float 467 468 /** 469 * The content (scene or overlay) around which the transition is currently bouncing. When 470 * not `null`, this transition is currently oscillating around this content and will soon 471 * settle to that content. 472 */ 473 val bouncingContent: ContentKey? 474 475 companion object { 476 const val DistanceUnspecified = 0f 477 } 478 } 479 } 480