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