1 /*
<lambda>null2 * 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.animateFloatAsState
22 import androidx.compose.animation.core.tween
23 import androidx.compose.foundation.Canvas
24 import androidx.compose.foundation.background
25 import androidx.compose.foundation.clickable
26 import androidx.compose.foundation.layout.fillMaxSize
27 import androidx.compose.material.icons.Icons
28 import androidx.compose.material.icons.filled.Face
29 import androidx.compose.runtime.Composable
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.mutableStateOf
32 import androidx.compose.runtime.remember
33 import androidx.compose.runtime.setValue
34 import androidx.compose.ui.Modifier
35 import androidx.compose.ui.graphics.Color
36 import androidx.compose.ui.graphics.drawscope.clipRect
37 import androidx.compose.ui.graphics.drawscope.translate
38 import androidx.compose.ui.graphics.painter.Painter
39 import androidx.compose.ui.graphics.vector.rememberVectorPainter
40 import androidx.compose.ui.layout.layoutId
41 import androidx.compose.ui.tooling.preview.Preview
42 import androidx.constraintlayout.compose.Arc
43 import androidx.constraintlayout.compose.ConstraintLayoutBaseScope
44 import androidx.constraintlayout.compose.Dimension
45 import androidx.constraintlayout.compose.ExperimentalMotionApi
46 import androidx.constraintlayout.compose.MotionLayout
47 import androidx.constraintlayout.compose.MotionScene
48 import androidx.constraintlayout.compose.Wrap
49
50 /**
51 * Shows how to animate moving pieces of a puzzle using MotionLayout.
52 *
53 *
54 *
55 * The [PuzzlePiece]s are laid out using the [ConstraintLayoutBaseScope.createFlow] helper.
56 *
57 * And the animation is achieved by creating two ConstraintSets. One providing ordered IDs to Flow,
58 * and the other providing a shuffled list of the same IDs.
59 *
60 * @see PuzzlePiece
61 */
62 @Preview
63 @Composable
64 fun AnimatedPuzzlePiecesDemo() {
65 val grid = 5
66 val blocks = grid * grid
67
68 var animateToEnd by remember { mutableStateOf(true) }
69
70 val index = remember { Array(blocks) { it }.apply { shuffle() } }
71 val refId = remember { Array(blocks) { "W$it" } }
72
73 // Recreate scene when order changes (which is driven by toggling `animateToEnd`)
74 val scene =
75 remember(animateToEnd) {
76 MotionScene {
77 val ordered = refId.map { createRefFor(it) }.toTypedArray()
78 val shuffle = index.map { ordered[it] }.toTypedArray()
79 val set1 = constraintSet {
80 val flow =
81 createFlow(
82 elements = ordered,
83 maxElement = grid,
84 wrapMode = Wrap.Aligned,
85 )
86 constrain(flow) {
87 centerTo(parent)
88 width = Dimension.ratio("1:1")
89 height = Dimension.ratio("1:1")
90 }
91 ordered.forEach {
92 constrain(it) {
93 width = Dimension.percent(1f / grid)
94 height = Dimension.ratio("1:1")
95 }
96 }
97 }
98 val set2 = constraintSet {
99 val flow =
100 createFlow(
101 elements = shuffle,
102 maxElement = grid,
103 wrapMode = Wrap.Aligned,
104 )
105 constrain(flow) {
106 centerTo(parent)
107 width = Dimension.ratio("1:1")
108 height = Dimension.ratio("1:1")
109 }
110 ordered.forEach {
111 constrain(it) {
112 width = Dimension.percent(1f / grid)
113 height = Dimension.ratio("1:1")
114 }
115 }
116 }
117 transition(set1, set2, "default") {
118 motionArc = Arc.StartHorizontal
119 keyAttributes(*ordered) {
120 frame(40) {
121 // alpha = 0.0f
122 rotationZ = -90f
123 scaleX = 0.1f
124 scaleY = 0.1f
125 }
126 frame(70) {
127 rotationZ = 90f
128 scaleX = 0.1f
129 scaleY = 0.1f
130 }
131 }
132 }
133 }
134 }
135
136 val progress by
137 animateFloatAsState(targetValue = if (animateToEnd) 1f else 0f, animationSpec = tween(800))
138
139 MotionLayout(
140 motionScene = scene,
141 modifier =
142 Modifier.clickable {
143 animateToEnd = !animateToEnd
144 index.shuffle()
145 }
146 .background(Color.Red)
147 .fillMaxSize(),
148 progress = progress
149 ) {
150 val painter = rememberVectorPainter(image = Icons.Default.Face)
151 index.forEachIndexed { i, id ->
152 PuzzlePiece(
153 x = i % grid,
154 y = i / grid,
155 gridSize = grid,
156 painter = painter,
157 modifier = Modifier.layoutId(refId[id])
158 )
159 }
160 }
161 }
162
163 /**
164 * Composable that displays a fragment of the given surface (provided through [painter]) based on
165 * the given position ([x], [y]) of a square grid of size [gridSize].
166 */
167 @Composable
PuzzlePiecenull168 fun PuzzlePiece(x: Int, y: Int, gridSize: Int, painter: Painter, modifier: Modifier = Modifier) {
169 Canvas(modifier.fillMaxSize()) {
170 clipRect {
171 translate(left = -x * size.width, top = -y * size.height) {
172 with(painter) { draw(size.times(gridSize.toFloat())) }
173 }
174 }
175 }
176 }
177