1 /* <lambda>null2 * Copyright (C) 2025 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.mechanics 18 19 import androidx.annotation.VisibleForTesting 20 import androidx.compose.runtime.mutableFloatStateOf 21 import com.android.compose.animation.scene.ContentKey 22 import com.android.compose.animation.scene.ElementKey 23 import com.android.compose.animation.scene.ElementStateScope 24 import com.android.compose.animation.scene.content.state.TransitionState 25 import com.android.compose.animation.scene.transformation.CustomPropertyTransformation 26 import com.android.compose.animation.scene.transformation.PropertyTransformationScope 27 import com.android.mechanics.MotionValue 28 import com.android.mechanics.ProvidedGestureContext 29 import com.android.mechanics.spec.InputDirection 30 import com.android.mechanics.spec.MotionSpec 31 import kotlinx.coroutines.CoroutineScope 32 import kotlinx.coroutines.launch 33 34 /** 35 * Callback to create a [MotionSpec] on the first call to [CustomPropertyTransformation.transform] 36 */ 37 typealias SpecFactory = 38 PropertyTransformationScope.(content: ContentKey, element: ElementKey) -> MotionSpec 39 40 /** Callback to compute the [MotionValue] per frame */ 41 typealias MotionValueInput = 42 PropertyTransformationScope.(progress: Float, content: ContentKey, element: ElementKey) -> Float 43 44 /** 45 * Adapter to create a [MotionValue] and `keepRunning()` it temporarily while a 46 * [CustomPropertyTransformation] is in progress and until the animation settles. 47 * 48 * The [MotionValue]'s input is by default the transition progress. 49 */ 50 internal class TransitionScopedMechanicsAdapter( 51 private val computeInput: MotionValueInput = { progress, _, _ -> progress }, 52 private val stableThreshold: Float = MotionValue.StableThresholdEffect, 53 private val label: String? = null, 54 private val createSpec: SpecFactory, 55 ) { 56 57 private val input = mutableFloatStateOf(0f) 58 private var motionValue: MotionValue? = null 59 PropertyTransformationScopenull60 fun PropertyTransformationScope.update( 61 content: ContentKey, 62 element: ElementKey, 63 transition: TransitionState.Transition, 64 transitionScope: CoroutineScope, 65 ): Float { 66 val progress = transition.progressTo(content) 67 input.floatValue = computeInput(progress, content, element) 68 var motionValue = motionValue 69 70 if (motionValue == null) { 71 motionValue = 72 MotionValue( 73 input::floatValue, 74 transition.gestureContext 75 ?: ProvidedGestureContext( 76 0f, 77 appearDirection(content, element, transition), 78 ), 79 createSpec(content, element), 80 stableThreshold = stableThreshold, 81 label = label, 82 ) 83 this@TransitionScopedMechanicsAdapter.motionValue = motionValue 84 85 transitionScope.launch { 86 motionValue.keepRunningWhile { !transition.isProgressStable || !isStable } 87 } 88 } 89 90 return motionValue.output 91 } 92 93 companion object { 94 /** 95 * Computes the InputDirection for a triggered transition of an element appearing / 96 * disappearing. 97 * 98 * Since [CustomPropertyTransformation] are only supported for non-shared elements, the 99 * [TransitionScopedMechanicsAdapter] is only used in the context of an element appearing / 100 * disappearing. This helper computes the direction to result in [InputDirection.Max] for an 101 * appear transition, and [InputDirection.Min] for a disappear transition. 102 */ 103 @VisibleForTesting appearDirectionnull104 internal fun ElementStateScope.appearDirection( 105 content: ContentKey, 106 element: ElementKey, 107 transition: TransitionState.Transition, 108 ): InputDirection { 109 check(!transition.isInitiatedByUserInput) 110 111 val inMaxDirection = 112 when (transition) { 113 is TransitionState.Transition.ChangeScene -> { 114 val transitionTowardsContent = content == transition.toContent 115 val elementInContent = element.targetSize(content) != null 116 val isReversed = transition.currentScene != transition.toScene 117 (transitionTowardsContent xor elementInContent) xor !isReversed 118 } 119 120 is TransitionState.Transition.ShowOrHideOverlay -> { 121 val transitioningTowardsOverlay = transition.overlay == transition.toContent 122 val isReversed = 123 transitioningTowardsOverlay xor transition.isEffectivelyShown 124 transitioningTowardsOverlay xor isReversed 125 } 126 127 is TransitionState.Transition.ReplaceOverlay -> { 128 transition.effectivelyShownOverlay == content 129 } 130 } 131 132 return if (inMaxDirection) InputDirection.Max else InputDirection.Min 133 } 134 } 135 } 136