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