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 package androidx.graphics.shapes.testcompose
18
19 import androidx.compose.ui.geometry.Offset
20 import androidx.compose.ui.geometry.Rect
21 import androidx.compose.ui.geometry.Size
22 import androidx.compose.ui.graphics.Matrix
23 import androidx.compose.ui.graphics.Outline
24 import androidx.compose.ui.graphics.Path
25 import androidx.compose.ui.graphics.Shape
26 import androidx.compose.ui.unit.Density
27 import androidx.compose.ui.unit.LayoutDirection
28 import androidx.graphics.shapes.Cubic
29 import androidx.graphics.shapes.Feature
30 import androidx.graphics.shapes.Morph
31 import androidx.graphics.shapes.RoundedPolygon
32 import androidx.graphics.shapes.TransformResult
33
34 /**
35 * Utility functions providing more idiomatic ways of transforming RoundedPolygons and transforming
36 * shapes into a compose Path, for drawing them.
37 *
38 * This should in the future move into the compose library, maybe with additional API that makes it
39 * easier to create, draw, and animate from Compose apps.
40 *
41 * This code is just here for now prior to integration into compose
42 */
43
44 /** Scales a shape (given as a List), creating a new List. */
45 fun List<Cubic>.scaled(scale: Float) = map {
46 it.transformed { x, y -> TransformResult(x * scale, y * scale) }
47 }
48
49 /**
50 * Gets a [Path] representation for a [RoundedPolygon] shape, which can be used to draw the polygon.
51 *
52 * @param path an optional [Path] object which, if supplied, will avoid the function having to
53 * create a new [Path] object
54 */
55 @JvmOverloads
RoundedPolygonnull56 fun RoundedPolygon.toPath(path: Path = Path()): Path {
57 pathFromCubics(path, cubics)
58 return path
59 }
60
61 /**
62 * Gets a [Path] representation for a [Morph] shape. This [Path] can be used to draw the morph.
63 *
64 * @param progress a value from 0 to 1 that determines the morph's current shape, between the start
65 * and end shapes provided at construction time. A value of 0 results in the start shape, a value
66 * of 1 results in the end shape, and any value in between results in a shape which is a linear
67 * interpolation between those two shapes. The range is generally [0..1] and values outside could
68 * result in undefined shapes, but values close to (but outside) the range can be used to get an
69 * exaggerated effect (e.g., for a bounce or overshoot animation).
70 * @param path an optional [Path] object which, if supplied, will avoid the function having to
71 * create a new [Path] object
72 */
toPathnull73 fun Morph.toPath(progress: Float, path: Path = Path()): Path {
74 pathFromCubics(path, asCubics(progress))
75 return path
76 }
77
78 /**
79 * Gets a [Path] representation for a [Feature] shape. This [Path] can be used to draw the feature.
80 *
81 * @param path an optional [Path] object which, if supplied, will avoid the function having to
82 * create a new [Path] object
83 */
84 @JvmOverloads
toPathnull85 fun Feature.toPath(path: Path = Path()): Path {
86 pathFromCubics(path, cubics, false)
87 return path
88 }
89
90 /**
91 * Returns the geometry of the given [cubics] in the given [path] object. This is used internally by
92 * the toPath functions, but we could consider exposing it as public API in case anyone was dealing
93 * directly with the cubics we create for our shapes.
94 */
pathFromCubicsnull95 private fun pathFromCubics(path: Path, cubics: List<Cubic>, closePath: Boolean = true) {
96 var first = true
97 path.rewind()
98 for (i in 0 until cubics.size) {
99 val cubic = cubics[i]
100 if (first) {
101 path.moveTo(cubic.anchor0X, cubic.anchor0Y)
102 first = false
103 }
104 path.cubicTo(
105 cubic.control0X,
106 cubic.control0Y,
107 cubic.control1X,
108 cubic.control1Y,
109 cubic.anchor1X,
110 cubic.anchor1Y
111 )
112 }
113 if (closePath) {
114 path.close()
115 }
116 }
117
118 /** Transforms a [RoundedPolygon] with the given [Matrix] */
RoundedPolygonnull119 fun RoundedPolygon.transformed(matrix: Matrix): RoundedPolygon = transformed { x, y ->
120 val transformedPoint = matrix.map(Offset(x, y))
121 TransformResult(transformedPoint.x, transformedPoint.y)
122 }
123
124 /** Calculates and returns the bounds of this [RoundedPolygon] as a [Rect] */
<lambda>null125 fun RoundedPolygon.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) }
126
127 /** Calculates and returns the bounds of this [Morph] as a [Rect] */
<lambda>null128 fun Morph.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) }
129
130 /**
131 * This class can be used to create a [Shape] object from a [RoundedPolygon]
132 *
133 * @param polygon The [RoundedPolygon] to be used for this [Shape]
134 * @param matrix An optional transformation matrix. If none is supplied, or null is passed as the
135 * value, a transformation matrix will be calculated internally, based on the bounds of [polygon].
136 * The result will be that [polygon] will be scaled and translated to fit within the size of the
137 * [Shape].
138 */
139 class RoundedPolygonShape(
140 private val polygon: RoundedPolygon,
141 private var matrix: Matrix = Matrix()
142 ) : Shape {
143 private val path = Path()
144
createOutlinenull145 override fun createOutline(
146 size: Size,
147 layoutDirection: LayoutDirection,
148 density: Density
149 ): Outline {
150 path.rewind()
151 polygon.toPath(path)
152 fitToViewport(path, polygon.getBounds(), size, matrix)
153 return Outline.Generic(path)
154 }
155 }
156
157 /**
158 * This class can be used to create a [Shape] object from a [RoundedPolygon]
159 *
160 * @param morph The [Morph] to be used for this [Shape]
161 * @param progress a value from 0 to 1 that determines the morph's current shape, between the start
162 * and end shapes provided at construction time. A value of 0 results in the start shape, a value
163 * of 1 results in the end shape, and any value in between results in a shape which is a linear
164 * interpolation between those two shapes. The range is generally [0..1] and values outside could
165 * result in undefined shapes, but values close to (but outside) the range can be used to get an
166 * exaggerated effect (e.g., for a bounce or overshoot animation).
167 * @param matrix An optional transformation matrix. If none is supplied, or null is passed as the
168 * value, a transformation matrix will be calculated internally, based on the bounds of [morph].
169 * The result will be that [morph] will be scaled and translated to fit within the size of the
170 * [Shape].
171 */
172 class MorphShape(
173 private val morph: Morph,
174 private val progress: Float,
175 private var matrix: Matrix = Matrix()
176 ) : Shape {
177 private val path = Path()
178
createOutlinenull179 override fun createOutline(
180 size: Size,
181 layoutDirection: LayoutDirection,
182 density: Density
183 ): Outline {
184 path.rewind()
185 morph.toPath(progress, path)
186 fitToViewport(path, morph.getBounds(), size, matrix)
187 return Outline.Generic(path)
188 }
189 }
190
191 /**
192 * Scales and translates the given [path] to fit within the given [viewport], using the max
193 * dimension of [bounds] and min dimension of [viewport] to ensure that the path fits completely
194 * within the viewport.
195 *
196 * @param path the path to be transformed
197 * @param bounds the bounds of the shape represented by [path]
198 * @param viewport the area within which [path] will be transformed to fit
199 * @param matrix optional [Matrix] item which can be supplied to avoid creating a new matrix every
200 * time the function is called.
201 */
fitToViewportnull202 fun fitToViewport(path: Path, bounds: Rect, viewport: Size, matrix: Matrix = Matrix()) {
203 matrix.reset()
204 val maxDimension = bounds.maxDimension
205 if (maxDimension > 0f) {
206 val scaleFactor = viewport.minDimension / maxDimension
207 val pathCenterX = bounds.left + bounds.width / 2
208 val pathCenterY = bounds.top + bounds.height / 2
209 matrix.translate(viewport.minDimension / 2, viewport.minDimension / 2)
210 matrix.scale(scaleFactor, scaleFactor)
211 matrix.translate(-pathCenterX, -pathCenterY)
212 path.transform(matrix)
213 }
214 }
215
radialToCartesiannull216 fun radialToCartesian(radius: Float, angleRadians: Float, center: Offset = Offset.Zero) =
217 directionVector(angleRadians) * radius + center
218