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