1 /*
<lambda>null2 * 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.core.samples
18
19 import androidx.annotation.Sampled
20 import androidx.compose.animation.core.Animatable
21 import androidx.compose.animation.core.AnimationEndReason
22 import androidx.compose.animation.core.AnimationVector2D
23 import androidx.compose.animation.core.DeferredTargetAnimation
24 import androidx.compose.animation.core.ExperimentalAnimatableApi
25 import androidx.compose.animation.core.Spring
26 import androidx.compose.animation.core.VectorConverter
27 import androidx.compose.animation.core.calculateTargetValue
28 import androidx.compose.animation.core.exponentialDecay
29 import androidx.compose.animation.core.spring
30 import androidx.compose.animation.core.tween
31 import androidx.compose.animation.splineBasedDecay
32 import androidx.compose.foundation.background
33 import androidx.compose.foundation.clickable
34 import androidx.compose.foundation.gestures.awaitFirstDown
35 import androidx.compose.foundation.gestures.verticalDrag
36 import androidx.compose.foundation.layout.Box
37 import androidx.compose.foundation.layout.Row
38 import androidx.compose.foundation.layout.fillMaxHeight
39 import androidx.compose.foundation.layout.fillMaxSize
40 import androidx.compose.foundation.layout.fillMaxWidth
41 import androidx.compose.foundation.layout.height
42 import androidx.compose.foundation.layout.offset
43 import androidx.compose.foundation.layout.size
44 import androidx.compose.foundation.layout.width
45 import androidx.compose.foundation.shape.CircleShape
46 import androidx.compose.material.Text
47 import androidx.compose.runtime.Composable
48 import androidx.compose.runtime.LaunchedEffect
49 import androidx.compose.runtime.getValue
50 import androidx.compose.runtime.mutableStateOf
51 import androidx.compose.runtime.remember
52 import androidx.compose.runtime.rememberCoroutineScope
53 import androidx.compose.runtime.setValue
54 import androidx.compose.ui.Alignment
55 import androidx.compose.ui.Modifier
56 import androidx.compose.ui.composed
57 import androidx.compose.ui.geometry.Offset
58 import androidx.compose.ui.geometry.Size
59 import androidx.compose.ui.graphics.Color
60 import androidx.compose.ui.graphics.graphicsLayer
61 import androidx.compose.ui.input.pointer.pointerInput
62 import androidx.compose.ui.input.pointer.positionChange
63 import androidx.compose.ui.input.pointer.util.VelocityTracker
64 import androidx.compose.ui.layout.approachLayout
65 import androidx.compose.ui.unit.Constraints
66 import androidx.compose.ui.unit.IntOffset
67 import androidx.compose.ui.unit.IntSize
68 import androidx.compose.ui.unit.dp
69 import kotlin.math.roundToInt
70 import kotlinx.coroutines.CoroutineScope
71 import kotlinx.coroutines.coroutineScope
72 import kotlinx.coroutines.launch
73
74 @Sampled
75 @Composable
76 fun AnimatableAnimateToGenericsType() {
77 // Creates an `Animatable` to animate Offset and `remember` it.
78 val animatedOffset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
79
80 Box(
81 Modifier.fillMaxSize().background(Color(0xffb99aff)).pointerInput(Unit) {
82 coroutineScope {
83 while (true) {
84 val offset = awaitPointerEventScope { awaitFirstDown().position }
85 // Launch a new coroutine for animation so the touch detection thread is not
86 // blocked.
87 launch {
88 // Animates to the pressed position, with the given animation spec.
89 animatedOffset.animateTo(
90 offset,
91 animationSpec = spring(stiffness = Spring.StiffnessLow)
92 )
93 }
94 }
95 }
96 }
97 ) {
98 Text("Tap anywhere", Modifier.align(Alignment.Center))
99 Box(
100 Modifier.offset {
101 // Use the animated offset as the offset of the Box.
102 IntOffset(
103 animatedOffset.value.x.roundToInt(),
104 animatedOffset.value.y.roundToInt()
105 )
106 }
107 .size(40.dp)
108 .background(Color(0xff3c1361), CircleShape)
109 )
110 }
111 }
112
113 @Sampled
AnimatableDecayAndAnimateToSamplenull114 fun AnimatableDecayAndAnimateToSample() {
115 /**
116 * In this example, we create a swipe-to-dismiss modifier that dismisses the child via a
117 * vertical swipe-up.
118 */
119 fun Modifier.swipeToDismiss(): Modifier = composed {
120 // Creates a Float type `Animatable` and `remember`s it
121 val animatedOffsetY = remember { Animatable(0f) }
122 this.pointerInput(Unit) {
123 coroutineScope {
124 while (true) {
125 val pointerId = awaitPointerEventScope { awaitFirstDown().id }
126 val velocityTracker = VelocityTracker()
127 awaitPointerEventScope {
128 verticalDrag(pointerId) {
129 // Snaps the value by the amount of finger movement
130 launch {
131 animatedOffsetY.snapTo(
132 animatedOffsetY.value + it.positionChange().y
133 )
134 }
135 velocityTracker.addPosition(it.uptimeMillis, it.position)
136 }
137 }
138 // At this point, drag has finished. Now we obtain the velocity at the end
139 // of
140 // the drag, and animate the offset with it as the starting velocity.
141 val velocity = velocityTracker.calculateVelocity().y
142
143 // The goal for the animation below is to animate the dismissal if the fling
144 // velocity is high enough. Otherwise, spring back.
145 launch {
146 // Checks where the animation will end using decay
147 val decay = splineBasedDecay<Float>(this@pointerInput)
148
149 // If the animation can naturally end outside of visual bounds, we will
150 // animate with decay.
151 if (
152 decay.calculateTargetValue(animatedOffsetY.value, velocity) <
153 -size.height
154 ) {
155 // (Optionally) updates lower bounds. This stops the animation as
156 // soon
157 // as bounds are reached.
158 animatedOffsetY.updateBounds(lowerBound = -size.height.toFloat())
159 // Animate with the decay animation spec using the fling velocity
160 animatedOffsetY.animateDecay(velocity, decay)
161 } else {
162 // Not enough velocity to be dismissed, spring back to 0f
163 animatedOffsetY.animateTo(0f, initialVelocity = velocity)
164 }
165 }
166 }
167 }
168 }
169 .offset { IntOffset(0, animatedOffsetY.value.roundToInt()) }
170 }
171 }
172
173 @Sampled
AnimatableAnimationResultSamplenull174 fun AnimatableAnimationResultSample() {
175 suspend fun CoroutineScope.animateBouncingOffBounds(
176 animatable: Animatable<Offset, *>,
177 flingVelocity: Offset,
178 parentSize: Size
179 ) {
180 launch {
181 var startVelocity = flingVelocity
182 // Set bounds for the animation, so that when it reaches bounds it will stop
183 // immediately. We can then inspect the returned `AnimationResult` and decide whether
184 // we should start another animation.
185 animatable.updateBounds(Offset(0f, 0f), Offset(parentSize.width, parentSize.height))
186 do {
187 val result = animatable.animateDecay(startVelocity, exponentialDecay())
188 // Copy out the end velocity of the previous animation.
189 startVelocity = result.endState.velocity
190
191 // Negate the velocity for the dimension that hits the bounds, to create a
192 // bouncing off the bounds effect.
193 with(animatable) {
194 if (value.x == upperBound?.x || value.x == lowerBound?.x) {
195 // x dimension hits bounds
196 startVelocity = startVelocity.copy(x = -startVelocity.x)
197 }
198 if (value.y == upperBound?.y || value.y == lowerBound?.y) {
199 // y dimension hits bounds
200 startVelocity = startVelocity.copy(y = -startVelocity.y)
201 }
202 }
203 // Repeat the animation until the animation ends for reasons other than hitting
204 // bounds, e.g. if `stop()` is called, or preempted by another animation.
205 } while (result.endReason == AnimationEndReason.BoundReached)
206 }
207 }
208 }
209
210 @Sampled
AnimatableFadeInnull211 fun AnimatableFadeIn() {
212 fun Modifier.fadeIn(): Modifier = composed {
213 // Creates an `Animatable` and remembers it.
214 val alphaAnimation = remember { Animatable(0f) }
215 // Launches a coroutine for the animation when entering the composition.
216 // Uses `alphaAnimation` as the subject so the job in `LaunchedEffect` will run only when
217 // `alphaAnimation` is created, which happens one time when the modifier enters
218 // composition.
219 LaunchedEffect(alphaAnimation) {
220 // Animates to 1f from 0f for the fade-in, and uses a 500ms tween animation.
221 alphaAnimation.animateTo(
222 targetValue = 1f,
223 // Default animationSpec uses [spring] animation, here we overwrite the default.
224 animationSpec = tween(500)
225 )
226 }
227 this.graphicsLayer(alpha = alphaAnimation.value)
228 }
229 }
230
231 @OptIn(ExperimentalAnimatableApi::class)
232 @Sampled
233 @Composable
DeferredTargetAnimationSamplenull234 fun DeferredTargetAnimationSample() {
235 // Creates a custom modifier that animates the constraints and measures child with the
236 // animated constraints. This modifier is built on top of `Modifier.approachLayout` to approach
237 // th destination size determined by the lookahead pass. A resize animation will be kicked off
238 // whenever the lookahead size changes, to animate children from current size to destination
239 // size. Fixed constraints created based on the animation value will be used to measure
240 // child, so the child layout gradually changes its animated constraints until the approach
241 // completes.
242 fun Modifier.animateConstraints(
243 sizeAnimation: DeferredTargetAnimation<IntSize, AnimationVector2D>,
244 coroutineScope: CoroutineScope
245 ) =
246 this.approachLayout(
247 isMeasurementApproachInProgress = { lookaheadSize ->
248 // Update the target of the size animation.
249 sizeAnimation.updateTarget(lookaheadSize, coroutineScope)
250 // Return true if the size animation has pending target change or is currently
251 // running.
252 !sizeAnimation.isIdle
253 }
254 ) { measurable, _ ->
255 // In the measurement approach, the goal is to gradually reach the destination size
256 // (i.e. lookahead size). To achieve that, we use an animation to track the current
257 // size, and animate to the destination size whenever it changes. Once the animation
258 // finishes, the approach is complete.
259
260 // First, update the target of the animation, and read the current animated size.
261 val (width, height) = sizeAnimation.updateTarget(lookaheadSize, coroutineScope)
262 // Then create fixed size constraints using the animated size
263 val animatedConstraints = Constraints.fixed(width, height)
264 // Measure child with animated constraints.
265 val placeable = measurable.measure(animatedConstraints)
266 layout(placeable.width, placeable.height) { placeable.place(0, 0) }
267 }
268
269 var fullWidth by remember { mutableStateOf(false) }
270
271 // Creates a size animation with a target unknown at the time of instantiation.
272 val sizeAnimation = remember { DeferredTargetAnimation(IntSize.VectorConverter) }
273 val coroutineScope = rememberCoroutineScope()
274 Row(
275 (if (fullWidth) Modifier.fillMaxWidth() else Modifier.width(100.dp))
276 .height(200.dp)
277 // Use the custom modifier created above to animate the constraints passed
278 // to the child, and therefore resize children in an animation.
279 .animateConstraints(sizeAnimation, coroutineScope)
280 .clickable { fullWidth = !fullWidth }
281 ) {
282 Box(
283 Modifier.weight(1f).fillMaxHeight().background(Color(0xffff6f69)),
284 )
285 Box(Modifier.weight(2f).fillMaxHeight().background(Color(0xffffcc5c)))
286 }
287 }
288