• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.reveal
18 
19 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
20 import androidx.compose.ui.unit.IntSize
21 import com.android.compose.animation.scene.ContentKey
22 import com.android.compose.animation.scene.ElementKey
23 import com.android.compose.animation.scene.TransitionBuilder
24 import com.android.compose.animation.scene.UserActionDistance
25 import com.android.compose.animation.scene.content.state.TransitionState
26 import com.android.compose.animation.scene.mechanics.MotionValueInput
27 import com.android.compose.animation.scene.mechanics.TransitionScopedMechanicsAdapter
28 import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
29 import com.android.compose.animation.scene.transformation.PropertyTransformation
30 import com.android.compose.animation.scene.transformation.PropertyTransformationScope
31 import com.android.mechanics.MotionValue
32 import com.android.mechanics.behavior.VerticalExpandContainerSpec
33 import kotlinx.coroutines.CoroutineScope
34 
35 interface ContainerRevealHaptics {
36     /**
37      * Called when the reveal threshold is crossed while the user was dragging on screen.
38      *
39      * Important: This callback is called during layout and its implementation should therefore be
40      * very fast or posted to a different thread.
41      *
42      * @param revealed whether we go from hidden to revealed, i.e. whether the container size is
43      *   going to jump from a smaller size to a bigger size.
44      */
45     fun onRevealThresholdCrossed(revealed: Boolean)
46 }
47 
48 /**
49  * Animate the reveal of [container] by animating its size.
50  *
51  * This implicitly sets the [distance] of the transition to the target size of [container]
52  */
53 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
TransitionBuildernull54 fun TransitionBuilder.verticalContainerReveal(
55     container: ElementKey,
56     motionSpec: VerticalExpandContainerSpec,
57     haptics: ContainerRevealHaptics,
58 ) {
59     // Make the swipe distance be exactly the target height of the container.
60     // TODO(b/376438969): Make sure that this works correctly when the target size of the element
61     // is changing during the transition (e.g. a notification was added). At the moment, the user
62     // action distance is only called until it returns a value > 0f, which is then cached.
63     distance = UserActionDistance { fromContent, toContent, _ ->
64         val targetSizeInFromContent = container.targetSize(fromContent)
65         val targetSizeInToContent = container.targetSize(toContent)
66         if (targetSizeInFromContent != null && targetSizeInToContent != null) {
67             error(
68                 "verticalContainerReveal should not be used with shared elements, but " +
69                     "${container.debugName} is in both ${fromContent.debugName} and " +
70                     toContent.debugName
71             )
72         }
73 
74         (targetSizeInToContent?.height ?: targetSizeInFromContent?.height)?.toFloat() ?: 0f
75     }
76 
77     // TODO(b/392534646) Add haptics back
78     val heightInput: MotionValueInput = { progress, content, element ->
79         val idleSize = checkNotNull(element.targetSize(content))
80         val targetHeight = idleSize.height.toFloat()
81         targetHeight * progress
82     }
83 
84     transformation(container) {
85         object : CustomPropertyTransformation<IntSize> {
86             override val property = PropertyTransformation.Property.Size
87 
88             val heightValue =
89                 TransitionScopedMechanicsAdapter(
90                     computeInput = heightInput,
91                     stableThreshold = MotionValue.StableThresholdSpatial,
92                     label = "verticalContainerReveal::height",
93                 ) { _, _ ->
94                     motionSpec.createHeightSpec(motionScheme, density = this)
95                 }
96             val widthValue =
97                 TransitionScopedMechanicsAdapter(
98                     computeInput = heightInput,
99                     stableThreshold = MotionValue.StableThresholdSpatial,
100                     label = "verticalContainerReveal::width",
101                 ) { content, element ->
102                     val idleSize = checkNotNull(element.targetSize(content))
103                     val intrinsicWidth = idleSize.width.toFloat()
104                     motionSpec.createWidthSpec(intrinsicWidth, motionScheme, density = this)
105                 }
106 
107             override fun PropertyTransformationScope.transform(
108                 content: ContentKey,
109                 element: ElementKey,
110                 transition: TransitionState.Transition,
111                 transitionScope: CoroutineScope,
112             ): IntSize {
113 
114                 val height =
115                     with(heightValue) { update(content, element, transition, transitionScope) }
116                 val width =
117                     with(widthValue) { update(content, element, transition, transitionScope) }
118 
119                 return IntSize(width.toInt(), height.toInt())
120             }
121         }
122     }
123 
124     transformation(container) {
125         object : CustomPropertyTransformation<Float> {
126 
127             override val property = PropertyTransformation.Property.Alpha
128             val alphaValue =
129                 TransitionScopedMechanicsAdapter(
130                     computeInput = heightInput,
131                     label = "verticalContainerReveal::alpha",
132                 ) { _, _ ->
133                     motionSpec.createAlphaSpec(motionScheme, density = this)
134                 }
135 
136             override fun PropertyTransformationScope.transform(
137                 content: ContentKey,
138                 element: ElementKey,
139                 transition: TransitionState.Transition,
140                 transitionScope: CoroutineScope,
141             ): Float {
142                 return with(alphaValue) { update(content, element, transition, transitionScope) }
143             }
144         }
145     }
146 }
147