1 /*
<lambda>null2  * Copyright 2024 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.graphics.shapes.testcompose
18 
19 import androidx.collection.MutableFloatObjectMap
20 import androidx.collection.mutableFloatObjectMapOf
21 import androidx.compose.foundation.clickable
22 import androidx.compose.foundation.layout.Box
23 import androidx.compose.foundation.layout.ExperimentalLayoutApi
24 import androidx.compose.foundation.layout.FlowRow
25 import androidx.compose.foundation.layout.aspectRatio
26 import androidx.compose.foundation.layout.fillMaxSize
27 import androidx.compose.foundation.layout.fillMaxWidth
28 import androidx.compose.foundation.layout.height
29 import androidx.compose.foundation.layout.padding
30 import androidx.compose.material.icons.Icons
31 import androidx.compose.material.icons.filled.Pinch
32 import androidx.compose.material3.Button
33 import androidx.compose.material3.Icon
34 import androidx.compose.material3.MaterialTheme
35 import androidx.compose.material3.Text
36 import androidx.compose.runtime.Composable
37 import androidx.compose.runtime.LaunchedEffect
38 import androidx.compose.runtime.MutableState
39 import androidx.compose.runtime.getValue
40 import androidx.compose.runtime.mutableFloatStateOf
41 import androidx.compose.runtime.remember
42 import androidx.compose.runtime.setValue
43 import androidx.compose.ui.Alignment
44 import androidx.compose.ui.Modifier
45 import androidx.compose.ui.draw.alpha
46 import androidx.compose.ui.draw.clip
47 import androidx.compose.ui.draw.drawWithContent
48 import androidx.compose.ui.geometry.Offset
49 import androidx.compose.ui.geometry.Size
50 import androidx.compose.ui.graphics.Color
51 import androidx.compose.ui.graphics.Matrix
52 import androidx.compose.ui.graphics.RectangleShape
53 import androidx.compose.ui.graphics.drawscope.Fill
54 import androidx.compose.ui.graphics.drawscope.Stroke
55 import androidx.compose.ui.text.style.TextAlign
56 import androidx.compose.ui.unit.dp
57 import androidx.graphics.shapes.Cubic
58 import androidx.graphics.shapes.RoundedPolygon
59 
60 @OptIn(ExperimentalLayoutApi::class)
61 @Composable
62 internal fun ShapesGallery(
63     shapes: List<RoundedPolygon>,
64     selectedMainShape: Int,
65     selectedOtherShape: Int,
66     modifier: Modifier = Modifier,
67     onNewClicked: (Int) -> Unit
68 ) {
69     FlowRow(modifier, maxItemsInEachRow = 7) {
70         shapes.forEachIndexed { newShapeIndex, shape ->
71             val alpha =
72                 when (newShapeIndex) {
73                     selectedMainShape -> 1f
74                     selectedOtherShape -> 0.65f
75                     else -> 0.3f
76                 }
77             PolygonView(
78                 shape,
79                 Modifier.padding(2.dp).height(60.dp).weight(1f).clickable {
80                     onNewClicked(newShapeIndex)
81                 },
82                 fillColor = MaterialTheme.colorScheme.primary.copy(alpha),
83                 center = true
84             )
85         }
86     }
87 }
88 
89 @Composable
PolygonViewnull90 internal fun PolygonView(
91     polygon: RoundedPolygon,
92     modifier: Modifier = Modifier,
93     fillColor: Color = MaterialTheme.colorScheme.primary,
94     debug: Boolean = false,
95     stroked: Boolean = false,
96     center: Boolean = false,
97 ) {
98     Size
99     val sizedShapes: MutableFloatObjectMap<List<Cubic>> =
100         remember(polygon) { mutableFloatObjectMapOf() }
101     val scheme = MaterialTheme.colorScheme
102     Box(modifier.fillMaxWidth()) {
103         Box(
104             Modifier.then(if (center) Modifier.aspectRatio(1f) else Modifier)
105                 .fillMaxSize()
106                 .align(Alignment.Center)
107                 .drawWithContent {
108                     drawContent()
109                     val scale = size.minDimension
110                     if (debug) {
111                         val shape = sizedShapes.getOrPut(scale) { polygon.cubics.scaled(scale) }
112                         // Draw bounding boxes
113                         val bounds = FloatArray(4)
114                         polygon.calculateBounds(bounds = bounds)
115                         drawRect(
116                             scheme.secondary,
117                             topLeft = Offset(scale * bounds[0], scale * bounds[1]),
118                             size =
119                                 Size(
120                                     scale * (bounds[2] - bounds[0]),
121                                     scale * (bounds[3] - bounds[1])
122                                 ),
123                             style = Stroke(2f)
124                         )
125                         polygon.calculateBounds(bounds = bounds, false)
126                         drawRect(
127                             scheme.tertiary,
128                             topLeft = Offset(scale * bounds[0], scale * bounds[1]),
129                             size =
130                                 Size(
131                                     scale * (bounds[2] - bounds[0]),
132                                     scale * (bounds[3] - bounds[1])
133                                 ),
134                             style = Stroke(2f)
135                         )
136                         polygon.calculateMaxBounds(bounds = bounds)
137                         drawRect(
138                             scheme.inversePrimary,
139                             topLeft = Offset(scale * bounds[0], scale * bounds[1]),
140                             size =
141                                 Size(
142                                     scale * (bounds[2] - bounds[0]),
143                                     scale * (bounds[3] - bounds[1])
144                                 ),
145                             style = Stroke(2f)
146                         )
147 
148                         // Center of shape
149                         drawCircle(
150                             fillColor,
151                             radius = 2f,
152                             center = Offset(polygon.centerX * scale, polygon.centerY * scale),
153                             style = Stroke(2f)
154                         )
155 
156                         shape.forEach { cubic -> debugDrawCubic(cubic, scheme) }
157                     } else {
158                         val scaledPath = polygon.toPath()
159                         val matrix = Matrix()
160                         matrix.scale(scale, scale)
161                         scaledPath.transform(matrix)
162                         val style = if (stroked) Stroke(size.width / 10f) else Fill
163                         drawPath(scaledPath, fillColor, style = style)
164                     }
165                 },
166         )
167     }
168 }
169 
170 @Composable
PolygonFeatureViewnull171 internal fun PolygonFeatureView(
172     polygon: RoundedPolygon,
173     customFeaturesOverlayState: MutableState<List<FeatureType>>,
174     featureColorScheme: FeatureColorScheme,
175     modifier: Modifier = Modifier
176 ) {
177     @Suppress("PrimitiveInCollection") val scheme = MaterialTheme.colorScheme
178     val model = remember { PanZoomRotateBoxState() }
179     var scale by remember { mutableFloatStateOf(0f) }
180     LaunchedEffect(scale) {
181         // Zoom in to see the shape
182         scale.let { if (it != 0f) model.zoom.value = it }
183     }
184 
185     Box(modifier.fillMaxWidth().clip(RectangleShape)) {
186         Box(Modifier.aspectRatio(1f).fillMaxSize().align(Alignment.Center)) {
187             PanZoomRotateBox(
188                 state = model,
189                 allowRotation = false,
190                 showInteraction = false,
191                 modifier = Modifier.padding(15.dp)
192             ) {
193                 Box(
194                     Modifier.fillMaxSize().drawWithContent {
195                         drawContent()
196                         scale = size.minDimension
197                         val paths = polygon.features.map { it.toPath() }
198 
199                         // Draw outline. Color features according to their type
200                         val style = Stroke(5f / scale)
201                         paths.forEachIndexed { index, path ->
202                             drawPath(
203                                 path,
204                                 featureToColor(polygon.features[index], featureColorScheme),
205                                 style = style
206                             )
207                         }
208 
209                         polygon.features.forEach {
210                             // Separate features by Spacer points.
211                             drawCircle(
212                                 scheme.background,
213                                 radius = 15f / scale,
214                                 center = it.cubics.first().anchor0()
215                             )
216                         }
217                     }
218                 )
219             }
220 
221             // Interactive points to show features and make their type changeable.
222             polygon.features.forEachIndexed { index, feature ->
223                 FeatureRepresentativePoint(
224                     modifier = Modifier.padding(15.dp),
225                     feature,
226                     featureColorScheme,
227                     scheme.background,
228                     model
229                 ) {
230                     customFeaturesOverlayState.value =
231                         customFeaturesOverlayState.value.mapIndexed { copyIndex, type ->
232                             if (copyIndex == index) toggleFeatureType(feature.toFeatureType())
233                             else type
234                         }
235                 }
236             }
237 
238             // We are adding the button manually as the reset behavior is different in our custom
239             // model
240             if (model.offset.value != Offset.Zero || model.zoom.value != scale) {
241                 Button(
242                     onClick = {
243                         model.offset.value = Offset.Zero
244                         model.zoom.value = scale
245                     },
246                 ) {
247                     Text("Reset View", textAlign = TextAlign.Center)
248                 }
249             } else {
250                 Icon(Icons.Default.Pinch, "Zoom in", Modifier.alpha(0.2f))
251             }
252         }
253     }
254 }
255 
256 internal data class FeatureColorScheme(
257     val edgeColor: Color,
258     val convexColor: Color,
259     val concaveColor: Color
260 )
261