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