1 /*
2 * Copyright 2020 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 androidx.compose.animation
18
19 import androidx.compose.animation.core.Animatable
20 import androidx.compose.animation.core.AnimationEndReason
21 import androidx.compose.animation.core.AnimationSpec
22 import androidx.compose.animation.core.AnimationVector2D
23 import androidx.compose.animation.core.FiniteAnimationSpec
24 import androidx.compose.animation.core.Spring
25 import androidx.compose.animation.core.VectorConverter
26 import androidx.compose.animation.core.VisibilityThreshold
27 import androidx.compose.animation.core.spring
28 import androidx.compose.runtime.getValue
29 import androidx.compose.runtime.mutableStateOf
30 import androidx.compose.runtime.setValue
31 import androidx.compose.ui.Alignment
32 import androidx.compose.ui.Modifier
33 import androidx.compose.ui.draw.clipToBounds
34 import androidx.compose.ui.layout.IntrinsicMeasurable
35 import androidx.compose.ui.layout.IntrinsicMeasureScope
36 import androidx.compose.ui.layout.LayoutModifier
37 import androidx.compose.ui.layout.Measurable
38 import androidx.compose.ui.layout.MeasureResult
39 import androidx.compose.ui.layout.MeasureScope
40 import androidx.compose.ui.node.LayoutModifierNode
41 import androidx.compose.ui.node.ModifierNodeElement
42 import androidx.compose.ui.platform.InspectorInfo
43 import androidx.compose.ui.unit.Constraints
44 import androidx.compose.ui.unit.IntSize
45 import androidx.compose.ui.unit.constrain
46 import kotlinx.coroutines.launch
47
48 /**
49 * This modifier animates its own size when its child modifier (or the child composable if it is
50 * already at the tail of the chain) changes size. This allows the parent modifier to observe a
51 * smooth size change, resulting in an overall continuous visual change.
52 *
53 * A [FiniteAnimationSpec] can be optionally specified for the size change animation. By default,
54 * [spring] will be used.
55 *
56 * An optional [finishedListener] can be supplied to get notified when the size change animation is
57 * finished. Since the content size change can be dynamic in many cases, both initial value and
58 * target value (i.e. final size) will be passed to the [finishedListener]. __Note:__ if the
59 * animation is interrupted, the initial value will be the size at the point of interruption. This
60 * is intended to help determine the direction of the size change (i.e. expand or collapse in x and
61 * y dimensions).
62 *
63 * @sample androidx.compose.animation.samples.AnimateContent
64 * @param animationSpec a finite animation that will be used to animate size change, [spring] by
65 * default
66 * @param finishedListener an optional listener to be called when the content change animation is
67 * completed.
68 */
animateContentSizenull69 public fun Modifier.animateContentSize(
70 animationSpec: FiniteAnimationSpec<IntSize> =
71 spring(
72 stiffness = Spring.StiffnessMediumLow,
73 visibilityThreshold = IntSize.VisibilityThreshold
74 ),
75 finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null
76 ): Modifier =
77 this.clipToBounds() then
78 SizeAnimationModifierElement(animationSpec, Alignment.TopStart, finishedListener)
79
80 /**
81 * This modifier animates its own size when its child modifier (or the child composable if it is
82 * already at the tail of the chain) changes size. This allows the parent modifier to observe a
83 * smooth size change, resulting in an overall continuous visual change.
84 *
85 * A [FiniteAnimationSpec] can be optionally specified for the size change animation. By default,
86 * [spring] will be used.
87 *
88 * An optional [finishedListener] can be supplied to get notified when the size change animation is
89 * finished. Since the content size change can be dynamic in many cases, both initial value and
90 * target value (i.e. final size) will be passed to the [finishedListener]. __Note:__ if the
91 * animation is interrupted, the initial value will be the size at the point of interruption. This
92 * is intended to help determine the direction of the size change (i.e. expand or collapse in x and
93 * y dimensions).
94 *
95 * @sample androidx.compose.animation.samples.AnimateContent
96 * @param animationSpec a finite animation that will be used to animate size change, [spring] by
97 * default
98 * @param alignment sets the alignment of the content during the animation. [Alignment.TopStart] by
99 * default.
100 * @param finishedListener an optional listener to be called when the content change animation is
101 * completed.
102 */
103 public fun Modifier.animateContentSize(
104 animationSpec: FiniteAnimationSpec<IntSize> =
105 spring(
106 stiffness = Spring.StiffnessMediumLow,
107 visibilityThreshold = IntSize.VisibilityThreshold
108 ),
109 alignment: Alignment = Alignment.TopStart,
110 finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null,
111 ): Modifier =
112 this.clipToBounds() then
113 SizeAnimationModifierElement(animationSpec, alignment, finishedListener)
114
115 private data class SizeAnimationModifierElement(
116 val animationSpec: FiniteAnimationSpec<IntSize>,
117 val alignment: Alignment,
118 val finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)?
119 ) : ModifierNodeElement<SizeAnimationModifierNode>() {
120 override fun create(): SizeAnimationModifierNode =
121 SizeAnimationModifierNode(animationSpec, alignment, finishedListener)
122
123 override fun update(node: SizeAnimationModifierNode) {
124 node.animationSpec = animationSpec
125 node.listener = finishedListener
126 node.alignment = alignment
127 }
128
129 override fun InspectorInfo.inspectableProperties() {
130 name = "animateContentSize"
131 properties["animationSpec"] = animationSpec
132 properties["alignment"] = alignment
133 properties["finishedListener"] = finishedListener
134 }
135 }
136
137 internal val InvalidSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE)
138 internal val IntSize.isValid: Boolean
139 get() = this != InvalidSize
140
141 /**
142 * This class creates a [LayoutModifier] that measures children, and responds to children's size
143 * change by animating to that size. The size reported to parents will be the animated size.
144 */
145 private class SizeAnimationModifierNode(
146 var animationSpec: AnimationSpec<IntSize>,
147 var alignment: Alignment = Alignment.TopStart,
148 var listener: ((startSize: IntSize, endSize: IntSize) -> Unit)? = null
149 ) : LayoutModifierNodeWithPassThroughIntrinsics() {
150 private var lookaheadSize: IntSize = InvalidSize
151 private var lookaheadConstraints: Constraints = Constraints()
152 set(value) {
153 field = value
154 lookaheadConstraintsAvailable = true
155 }
156
157 private var lookaheadConstraintsAvailable: Boolean = false
158
targetConstraintsnull159 private fun targetConstraints(default: Constraints) =
160 if (lookaheadConstraintsAvailable) {
161 lookaheadConstraints
162 } else {
163 default
164 }
165
166 data class AnimData(val anim: Animatable<IntSize, AnimationVector2D>, var startSize: IntSize)
167
168 var animData: AnimData? by mutableStateOf(null)
169
onResetnull170 override fun onReset() {
171 super.onReset()
172 // Reset is an indication that the node may be re-used, in such case, animData becomes stale
173 animData = null
174 }
175
onAttachnull176 override fun onAttach() {
177 super.onAttach()
178 // When re-attached, we may be attached to a tree without lookahead scope.
179 lookaheadSize = InvalidSize
180 lookaheadConstraintsAvailable = false
181 }
182
measurenull183 override fun MeasureScope.measure(
184 measurable: Measurable,
185 constraints: Constraints
186 ): MeasureResult {
187 val placeable =
188 if (isLookingAhead) {
189 lookaheadConstraints = constraints
190 measurable.measure(constraints)
191 } else {
192 // Measure with lookahead constraints when available, to avoid unnecessary relayout
193 // in child during the lookahead animation.
194 measurable.measure(targetConstraints(constraints))
195 }
196 val measuredSize = IntSize(placeable.width, placeable.height)
197 val (width, height) =
198 if (isLookingAhead) {
199 lookaheadSize = measuredSize
200 measuredSize
201 } else {
202 animateTo(if (lookaheadSize.isValid) lookaheadSize else measuredSize).let {
203 // Constrain the measure result to incoming constraints, so that parent doesn't
204 // force center this layout.
205 constraints.constrain(it)
206 }
207 }
208 return layout(width, height) {
209 val offset =
210 alignment.align(
211 size = measuredSize,
212 space = IntSize(width, height),
213 layoutDirection = this@measure.layoutDirection
214 )
215 placeable.place(offset)
216 }
217 }
218
animateTonull219 fun animateTo(targetSize: IntSize): IntSize {
220 val data =
221 animData?.apply {
222 // TODO(b/322878517): Figure out a way to seamlessly continue the animation after
223 // re-attach. Note that in some cases restarting the animation is the correct
224 // behavior.
225 val wasInterrupted = (targetSize != anim.value && !anim.isRunning)
226
227 if (targetSize != anim.targetValue || wasInterrupted) {
228 startSize = anim.value
229 coroutineScope.launch {
230 val result = anim.animateTo(targetSize, animationSpec)
231 if (result.endReason == AnimationEndReason.Finished) {
232 listener?.invoke(startSize, result.endState.value)
233 }
234 }
235 }
236 }
237 ?: AnimData(
238 Animatable(targetSize, IntSize.VectorConverter, IntSize(1, 1)),
239 targetSize
240 )
241
242 animData = data
243 return data.anim.value
244 }
245 }
246
247 internal abstract class LayoutModifierNodeWithPassThroughIntrinsics :
248 LayoutModifierNode, Modifier.Node() {
minIntrinsicWidthnull249 override fun IntrinsicMeasureScope.minIntrinsicWidth(
250 measurable: IntrinsicMeasurable,
251 height: Int
252 ) = measurable.minIntrinsicWidth(height)
253
254 override fun IntrinsicMeasureScope.minIntrinsicHeight(
255 measurable: IntrinsicMeasurable,
256 width: Int
257 ) = measurable.minIntrinsicHeight(width)
258
259 override fun IntrinsicMeasureScope.maxIntrinsicWidth(
260 measurable: IntrinsicMeasurable,
261 height: Int
262 ) = measurable.maxIntrinsicWidth(height)
263
264 override fun IntrinsicMeasureScope.maxIntrinsicHeight(
265 measurable: IntrinsicMeasurable,
266 width: Int
267 ) = measurable.maxIntrinsicHeight(width)
268 }
269