1 /*
2  * Copyright 2023 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 @file:OptIn(ExperimentalMotionApi::class)
18 
19 package androidx.constraintlayout.compose.demos
20 
21 import androidx.compose.animation.core.Animatable
22 import androidx.compose.animation.core.animateFloatAsState
23 import androidx.compose.animation.core.tween
24 import androidx.compose.foundation.Image
25 import androidx.compose.foundation.background
26 import androidx.compose.foundation.clickable
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.fillMaxSize
29 import androidx.compose.foundation.layout.fillMaxWidth
30 import androidx.compose.foundation.layout.height
31 import androidx.compose.foundation.layout.padding
32 import androidx.compose.foundation.lazy.LazyColumn
33 import androidx.compose.foundation.shape.RoundedCornerShape
34 import androidx.compose.material.Text
35 import androidx.compose.material.icons.Icons
36 import androidx.compose.material.icons.filled.Face
37 import androidx.compose.material.icons.filled.Menu
38 import androidx.compose.runtime.Composable
39 import androidx.compose.runtime.LaunchedEffect
40 import androidx.compose.runtime.getValue
41 import androidx.compose.runtime.mutableStateOf
42 import androidx.compose.runtime.remember
43 import androidx.compose.runtime.setValue
44 import androidx.compose.ui.Modifier
45 import androidx.compose.ui.draw.clip
46 import androidx.compose.ui.graphics.Color
47 import androidx.compose.ui.graphics.ColorFilter
48 import androidx.compose.ui.layout.ContentScale
49 import androidx.compose.ui.layout.layoutId
50 import androidx.compose.ui.tooling.preview.Preview
51 import androidx.compose.ui.unit.dp
52 import androidx.compose.ui.unit.sp
53 import androidx.constraintlayout.compose.Dimension
54 import androidx.constraintlayout.compose.ExperimentalMotionApi
55 import androidx.constraintlayout.compose.MotionLayout
56 import androidx.constraintlayout.compose.MotionScene
57 import kotlin.random.Random
58 
59 /**
60  * Shows how to use MotionLayout to have animated expandable items in a LazyColumn.
61  *
62  * Where the MotionScene is defined using the DSL.
63  */
64 @Preview(group = "scroll", device = "spec:width=480dp,height=800dp,dpi=440")
65 @Composable
MotionInLazyColumnDslDemonull66 fun MotionInLazyColumnDslDemo() {
67     val scene = MotionScene {
68         val title = createRefFor("title")
69         val image = createRefFor("image")
70         val icon = createRefFor("icon")
71 
72         val start1 = constraintSet {
73             constrain(title) {
74                 centerVerticallyTo(icon)
75                 start.linkTo(icon.end, 16.dp)
76             }
77             constrain(image) {
78                 width = Dimension.value(40.dp)
79                 height = Dimension.value(40.dp)
80                 centerVerticallyTo(icon)
81                 end.linkTo(parent.end, 8.dp)
82             }
83             constrain(icon) {
84                 top.linkTo(parent.top, 16.dp)
85                 bottom.linkTo(parent.bottom, 16.dp)
86                 start.linkTo(parent.start, 16.dp)
87             }
88         }
89 
90         val end1 = constraintSet {
91             constrain(title) {
92                 bottom.linkTo(parent.bottom)
93                 start.linkTo(parent.start)
94                 scaleX = 0.7f
95                 scaleY = 0.7f
96             }
97             constrain(image) {
98                 width = Dimension.matchParent
99                 height = Dimension.value(200.dp)
100                 centerVerticallyTo(parent)
101             }
102             constrain(icon) {
103                 top.linkTo(parent.top, 16.dp)
104                 start.linkTo(parent.start, 16.dp)
105             }
106         }
107         transition(start1, end1, "default") {}
108     }
109 
110     val model = remember { BooleanArray(100) }
111 
112     LazyColumn {
113         items(100) {
114             Box(modifier = Modifier.padding(3.dp)) {
115                 var animateToEnd by remember { mutableStateOf(model[it]) }
116 
117                 val progress by
118                     animateFloatAsState(
119                         targetValue = if (animateToEnd) 1f else 0f,
120                         animationSpec = tween(700)
121                     )
122 
123                 MotionLayout(
124                     modifier = Modifier.background(Color(0xFF331B1B)).fillMaxWidth().padding(1.dp),
125                     motionScene = scene,
126                     progress = progress
127                 ) {
128                     Image(
129                         modifier = Modifier.layoutId("image"),
130                         imageVector = Icons.Default.Face,
131                         contentDescription = null,
132                         contentScale = ContentScale.Crop
133                     )
134                     Image(
135                         modifier =
136                             Modifier.layoutId("icon").clickable {
137                                 animateToEnd = !animateToEnd
138                                 model[it] = animateToEnd
139                             },
140                         imageVector = Icons.Default.Menu,
141                         contentDescription = null,
142                         colorFilter = ColorFilter.tint(Color.White)
143                     )
144                     Text(
145                         modifier = Modifier.layoutId("title"),
146                         text = "San Francisco $it",
147                         fontSize = 30.sp,
148                         color = Color.White
149                     )
150                 }
151             }
152         }
153     }
154 }
155 
156 /**
157  * Shows how to use MotionLayout to have animated graphs in a LazyColumn, where each graph is
158  * animated as it's revealed.
159  *
160  * Demonstrates how to dynamically create constraints based on input. See [DynamicGraph]. Where
161  * constraints are created to lay out the given values into a single graph layout.
162  */
163 @Preview(group = "scroll", device = "spec:width=480dp,height=800dp,dpi=440")
164 @Composable
AnimateGraphsOnRevealDemonull165 fun AnimateGraphsOnRevealDemo() {
166     val graphs = mutableListOf<List<Float>>()
167     for (i in 0..100) {
168         val values = FloatArray(10) { Random.nextInt(100).toFloat() + 10f }.asList()
169         graphs.add(values)
170     }
171     LazyColumn {
172         items(100) {
173             Box(modifier = Modifier.padding(3.dp).height(200.dp)) { DynamicGraph(graphs[it]) }
174         }
175     }
176 }
177 
178 @Preview(group = "scroll", device = "spec:width=480dp,height=800dp,dpi=440")
179 @Composable
DynamicGraphnull180 private fun DynamicGraph(
181     values: List<Float> = listOf<Float>(12f, 32f, 21f, 32f, 2f),
182     max: Int = 100
183 ) {
184     val scale = values.map { (it * 0.8f) / max }
185     val count = values.size
186     val widthPercent = 1 / (count * 2f)
187     val tmpNames = arrayOfNulls<String>(count)
188     for (i in tmpNames.indices) {
189         tmpNames[i] = "foo$i"
190     }
191     val names: List<String> = tmpNames.filterNotNull()
192     val scene = MotionScene {
193         val cols = names.map { createRefFor(it) }.toTypedArray()
194         val start1 = constraintSet {
195             createHorizontalChain(elements = cols)
196             for (i in names.indices) {
197                 constrain(cols[i]) {
198                     width = Dimension.percent(widthPercent)
199                     height = Dimension.value(1.dp)
200                     bottom.linkTo(parent.bottom, 16.dp)
201                 }
202             }
203         }
204 
205         val end1 = constraintSet {
206             createHorizontalChain(elements = cols)
207             for (i in names.indices) {
208                 constrain(cols[i]) {
209                     width = Dimension.percent(widthPercent)
210                     height = Dimension.percent(scale[i])
211                     bottom.linkTo(parent.bottom, 16.dp)
212                 }
213             }
214         }
215         transition(start1, end1, "default") {}
216     }
217     var animateToEnd by remember { mutableStateOf(true) }
218     val progress = remember { Animatable(0f) }
219 
220     // Animate on reveal
221     LaunchedEffect(animateToEnd) {
222         progress.animateTo(if (animateToEnd) 1f else 0f, animationSpec = tween(800))
223     }
224 
225     MotionLayout(
226         modifier =
227             Modifier.background(Color(0xFF221010))
228                 .fillMaxSize()
229                 .clickable { animateToEnd = !animateToEnd }
230                 .padding(1.dp),
231         motionScene = scene,
232         progress = progress.value
233     ) {
234         for (i in 0..count) {
235             Box(
236                 modifier =
237                     Modifier.layoutId("foo$i")
238                         .clip(RoundedCornerShape(20.dp))
239                         .background(Color.hsv(i * 240f / count, 0.6f, 0.6f))
240             )
241         }
242     }
243 }
244