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.mechanics.demo.staging.behavior 18 19 import android.util.Log 20 import androidx.compose.animation.core.ExperimentalAnimatableApi 21 import androidx.compose.runtime.mutableFloatStateOf 22 import androidx.compose.ui.unit.IntSize 23 import androidx.compose.ui.unit.dp 24 import androidx.compose.ui.unit.toSize 25 import com.android.compose.animation.scene.ContentKey 26 import com.android.compose.animation.scene.ElementKey 27 import com.android.compose.animation.scene.TransitionBuilder 28 import com.android.compose.animation.scene.UserActionDistance 29 import com.android.compose.animation.scene.content.state.TransitionState 30 import com.android.compose.animation.scene.transformation.CustomPropertyTransformation 31 import com.android.compose.animation.scene.transformation.PropertyTransformation 32 import com.android.compose.animation.scene.transformation.PropertyTransformationScope 33 import com.android.mechanics.MotionValue 34 import com.android.mechanics.ProvidedGestureContext 35 import com.android.mechanics.spec.InputDirection 36 import com.android.mechanics.spec.Mapping 37 import com.android.mechanics.spec.MotionSpec 38 import com.android.mechanics.spec.builder 39 import com.android.mechanics.spring.SpringParameters 40 import kotlinx.coroutines.CoroutineScope 41 import kotlinx.coroutines.Job 42 import kotlinx.coroutines.launch 43 44 /** Animate the reveal of [container] by animating its size. */ 45 fun TransitionBuilder.magneticDetach(container: ElementKey) { 46 // Make the swipe distance be exactly the target height of the container. 47 // TODO(b/376438969): Make sure that this works correctly when the target size of the element 48 // is changing during the transition (e.g. a notification was added). At the moment, the user 49 // action distance is only called until it returns a value > 0f, which is then cached. 50 distance = UserActionDistance { fromContent, toContent, _ -> 51 val targetSizeInFromContent = container.targetSize(fromContent) 52 val targetSizeInToContent = container.targetSize(toContent) 53 if (targetSizeInFromContent != null && targetSizeInToContent != null) { 54 error( 55 "verticalContainerReveal should not be used with shared elements, but " + 56 "${container.debugName} is in both ${fromContent.debugName} and " + 57 toContent.debugName 58 ) 59 } 60 61 (targetSizeInToContent?.height ?: targetSizeInFromContent?.height)?.toFloat() ?: 0f 62 } 63 64 Log.d("MIKES", "magneticDetach() called with: container = $container") 65 66 transformation(container) { 67 Log.d("MIKES", "magneticDetach() Created") 68 MagneticDetachTransformation() 69 } 70 } 71 72 @OptIn(ExperimentalAnimatableApi::class) 73 private class MagneticDetachTransformation() : CustomPropertyTransformation<IntSize> { 74 override val property = PropertyTransformation.Property.Size 75 76 val input = mutableFloatStateOf(0f) 77 78 var heightValue: MotionValue? = null 79 var heightValueJob: Job? = null 80 transformnull81 override fun PropertyTransformationScope.transform( 82 content: ContentKey, 83 element: ElementKey, 84 transition: TransitionState.Transition, 85 transitionScope: CoroutineScope, 86 ): IntSize { 87 Log.d( 88 "MIKES", 89 "transform() called with: content = $content, element = $element, transition = $transition, transitionScope = $transitionScope", 90 ) 91 val idleSize = checkNotNull(element.targetSize(content)) 92 93 if ( 94 heightValue?.isStable == true && 95 transition.progress == 1f && 96 !transition.isUserInputOngoing 97 ) { 98 Log.d("MIKES", "transform() KILL") 99 heightValue = null 100 heightValueJob?.cancel() 101 heightValueJob = null 102 return idleSize 103 } 104 105 val fromSize = checkNotNull(element.targetSize(transition.fromContent)).toSize() 106 val toSize = checkNotNull(element.targetSize(transition.toContent)).toSize() 107 val collapsedSize = if (fromSize.height < toSize.height) fromSize else toSize 108 val expandedHeight = if (fromSize.height >= toSize.height) fromSize else toSize 109 110 input.floatValue = (expandedHeight.height - collapsedSize.height) * transition.progress 111 Log.d("MIKES", "transform()UPD") 112 113 if (heightValue == null) { 114 Log.d("MIKES", "transform()START") 115 val springParameters = SpringParameters(stiffness = 380f, dampingRatio = 0.9f) 116 117 val detachDistance = 48.dp.toPx() 118 119 val spec = 120 MotionSpec.builder( 121 springParameters, 122 initialMapping = Mapping.Fixed(collapsedSize.height), 123 ) 124 .toBreakpoint(0f) 125 .jumpBy(0f) 126 .continueWithFractionalInput(.2f) 127 .toBreakpoint(detachDistance) 128 .jumpTo(collapsedSize.height + detachDistance) 129 .continueWithFractionalInput(1f) 130 .complete() 131 132 heightValue = 133 MotionValue( 134 input::floatValue, 135 transition.gestureContext ?: ProvidedGestureContext(0f, InputDirection.Max), 136 spec, 137 ) 138 heightValueJob = transitionScope.launch { heightValue?.keepRunning() } 139 } 140 141 return IntSize(idleSize.width, height = heightValue!!.output.toInt()) 142 } 143 } 144