1 /*
2  * Copyright 2022 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.path
18 
19 import android.graphics.Path
20 import android.graphics.PathIterator as PlatformPathIterator
21 import android.graphics.PointF
22 import androidx.annotation.RequiresApi
23 import androidx.graphics.path.PathIterator.ConicEvaluation
24 import dalvik.annotation.optimization.FastNative
25 
26 /**
27  * Base class for API-version-specific PathIterator implementation classes. All functionality is
28  * implemented in the subclasses except for [next], which relies on shared native code to perform
29  * conic conversion.
30  */
31 internal abstract class PathIteratorImpl(
32     val path: Path,
33     val conicEvaluation: ConicEvaluation = ConicEvaluation.AsQuadratics,
34     val tolerance: Float = 0.25f
35 ) {
36     /**
37      * pointsData is used internally when the no-arg variant of next() is called, to avoid
38      * allocating a new array every time.
39      */
40     private val pointsData = FloatArray(8)
41 
42     private companion object {
43         init {
44             /**
45              * The native library is used mainly for pre-API34, but we also rely on the conic
46              * conversion code in API34+, thus it is initialized here.
47              *
48              * Currently this native library is only available for Android, it should not be loaded
49              * on host platforms, e.g. LayoutLib and Robolectric.
50              */
51             val isDalvik = "dalvik".equals(System.getProperty("java.vm.name"), ignoreCase = true)
52             if (isDalvik) {
53                 System.loadLibrary("androidx.graphics.path")
54             }
55         }
56     }
57 
calculateSizenull58     abstract fun calculateSize(includeConvertedConics: Boolean): Int
59 
60     abstract fun hasNext(): Boolean
61 
62     abstract fun peek(): PathSegment.Type
63 
64     abstract fun next(points: FloatArray, offset: Int = 0): PathSegment.Type
65 
66     fun next(): PathSegment {
67         val type = next(pointsData, 0)
68         if (type == PathSegment.Type.Done) return DoneSegment
69         if (type == PathSegment.Type.Close) return CloseSegment
70         val weight = if (type == PathSegment.Type.Conic) pointsData[6] else 0.0f
71         return PathSegment(type, floatsToPoints(pointsData, type), weight)
72     }
73 
74     /**
75      * Utility function to convert a FloatArray to an array of PointF objects, where every two
76      * Floats in the FloatArray correspond to a single PointF in the resulting point array. The
77      * FloatArray is used internally to process a next() call, the array of points is used to create
78      * a PathSegment from the operation.
79      */
floatsToPointsnull80     private fun floatsToPoints(pointsData: FloatArray, type: PathSegment.Type): Array<PointF> {
81         val points =
82             when (type) {
83                 PathSegment.Type.Move -> {
84                     arrayOf(PointF(pointsData[0], pointsData[1]))
85                 }
86                 PathSegment.Type.Line -> {
87                     arrayOf(
88                         PointF(pointsData[0], pointsData[1]),
89                         PointF(pointsData[2], pointsData[3])
90                     )
91                 }
92                 PathSegment.Type.Quadratic,
93                 PathSegment.Type.Conic -> {
94                     arrayOf(
95                         PointF(pointsData[0], pointsData[1]),
96                         PointF(pointsData[2], pointsData[3]),
97                         PointF(pointsData[4], pointsData[5])
98                     )
99                 }
100                 PathSegment.Type.Cubic -> {
101                     arrayOf(
102                         PointF(pointsData[0], pointsData[1]),
103                         PointF(pointsData[2], pointsData[3]),
104                         PointF(pointsData[4], pointsData[5]),
105                         PointF(pointsData[6], pointsData[7])
106                     )
107                 }
108                 // This should not happen because of the early returns above
109                 else -> emptyArray()
110             }
111         return points
112     }
113 }
114 
115 /**
116  * In API level 34, we can use new platform functionality for most of what PathIterator does. The
117  * exceptions are conic conversion (which is handled in the base impl class) and [calculateSize],
118  * which is implemented here.
119  */
120 @RequiresApi(34)
121 internal class PathIteratorApi34Impl(
122     path: Path,
123     conicEvaluation: ConicEvaluation = ConicEvaluation.AsQuadratics,
124     tolerance: Float = 0.25f
125 ) : PathIteratorImpl(path, conicEvaluation, tolerance) {
126     /**
127      * The platform iterator handles most of what we need for iterating. We hold an instance of that
128      * object in this class.
129      */
130     private val platformIterator = path.pathIterator
131 
132     /**
133      * An iterator's ConicConverter converts from a conic to a series of quadratics. It keeps track
134      * of the resulting quadratics and iterates through them on ensuing calls to next(). The
135      * converter is only ever called if [conicEvaluation] is set to [ConicEvaluation.AsQuadratics].
136      */
137     private val conicConverter = ConicConverter()
138 
139     /**
140      * The platform does not expose a calculateSize() method, so we implement our own. In the
141      * simplest case, this is done by simply iterating through all segments until done. However, if
142      * the caller requested the true size (including any conic conversion) and if there are any
143      * conics in the path segments, then there is more work to do since we have to convert and count
144      * those segments as well.
145      */
calculateSizenull146     override fun calculateSize(includeConvertedConics: Boolean): Int {
147         val convertConics =
148             includeConvertedConics && conicEvaluation == ConicEvaluation.AsQuadratics
149         var numVerbs = 0
150         val tempIterator = path.pathIterator
151         val tempFloats = FloatArray(8)
152         while (tempIterator.hasNext()) {
153             val type = tempIterator.next(tempFloats, 0)
154             if (type == PlatformPathIterator.VERB_CONIC && convertConics) {
155                 with(conicConverter) {
156                     convert(tempFloats, tempFloats[6], tolerance)
157                     numVerbs += quadraticCount
158                 }
159             } else {
160                 numVerbs++
161             }
162         }
163         return numVerbs
164     }
165 
nextnull166     override fun next(points: FloatArray, offset: Int): PathSegment.Type {
167         // First check to see if we are currently iterating through converted conics
168         if (conicConverter.currentQuadratic < conicConverter.quadraticCount) {
169             conicConverter.nextQuadratic(points, offset)
170             return PathSegment.Type.Quadratic
171         } else {
172             val typeValue = platformToAndroidXSegmentType(platformIterator.next(points, offset))
173             if (
174                 typeValue == PathSegment.Type.Conic &&
175                     conicEvaluation == ConicEvaluation.AsQuadratics
176             ) {
177                 with(conicConverter) {
178                     convert(points, points[6 + offset], tolerance, offset)
179                     if (quadraticCount > 0) {
180                         nextQuadratic(points, offset)
181                     }
182                 }
183                 return PathSegment.Type.Quadratic
184             }
185             return typeValue
186         }
187     }
188 
hasNextnull189     override fun hasNext(): Boolean = platformIterator.hasNext()
190 
191     override fun peek() = platformToAndroidXSegmentType(platformIterator.peek())
192 }
193 
194 /** Callers need the AndroidX segment types, so we must convert from the platform types. */
195 private fun platformToAndroidXSegmentType(platformType: Int): PathSegment.Type {
196     return when (platformType) {
197         PlatformPathIterator.VERB_CLOSE -> PathSegment.Type.Close
198         PlatformPathIterator.VERB_CONIC -> PathSegment.Type.Conic
199         PlatformPathIterator.VERB_CUBIC -> PathSegment.Type.Cubic
200         PlatformPathIterator.VERB_DONE -> PathSegment.Type.Done
201         PlatformPathIterator.VERB_LINE -> PathSegment.Type.Line
202         PlatformPathIterator.VERB_MOVE -> PathSegment.Type.Move
203         PlatformPathIterator.VERB_QUAD -> PathSegment.Type.Quadratic
204         else -> {
205             throw IllegalArgumentException("Unknown path segment type $platformType")
206         }
207     }
208 }
209 
210 /**
211  * Most of the functionality for pre-34 iteration is handled in the native code. The only exception,
212  * similar to the API34 implementation, is the calculateSize(). There is a size() function in native
213  * code which is very quick (it simply tracks the number of verbs in the native structure). But if
214  * the caller wants conic conversion, then we need to iterate through and convert appropriately,
215  * counting as we iterate.
216  */
217 internal class PathIteratorPreApi34Impl(
218     path: Path,
219     conicEvaluation: ConicEvaluation = ConicEvaluation.AsQuadratics,
220     tolerance: Float = 0.25f
221 ) : PathIteratorImpl(path, conicEvaluation, tolerance) {
222 
223     @Suppress("KotlinJniMissingFunction")
createInternalPathIteratornull224     private external fun createInternalPathIterator(
225         path: Path,
226         conicEvaluation: Int,
227         tolerance: Float
228     ): Long
229 
230     @Suppress("KotlinJniMissingFunction")
231     private external fun destroyInternalPathIterator(internalPathIterator: Long)
232 
233     @Suppress("KotlinJniMissingFunction")
234     @FastNative
235     private external fun internalPathIteratorHasNext(internalPathIterator: Long): Boolean
236 
237     @Suppress("KotlinJniMissingFunction")
238     @FastNative
239     private external fun internalPathIteratorNext(
240         internalPathIterator: Long,
241         points: FloatArray,
242         offset: Int
243     ): Int
244 
245     @Suppress("KotlinJniMissingFunction")
246     @FastNative
247     private external fun internalPathIteratorPeek(internalPathIterator: Long): Int
248 
249     @Suppress("KotlinJniMissingFunction")
250     @FastNative
251     private external fun internalPathIteratorRawSize(internalPathIterator: Long): Int
252 
253     @Suppress("KotlinJniMissingFunction")
254     @FastNative
255     private external fun internalPathIteratorSize(internalPathIterator: Long): Int
256 
257     /** Defines the type of evaluation to apply to conic segments during iteration. */
258     private val internalPathIterator =
259         createInternalPathIterator(path, conicEvaluation.ordinal, tolerance)
260 
261     /**
262      * Returns the number of verbs present in this iterator's path. If [includeConvertedConics]
263      * property is false and the path has any conic elements, the returned size might be smaller
264      * than the number of calls to [next] required to fully iterate over the path. An accurate size
265      * can be computed by setting the parameter to true instead, at a performance cost. Including
266      * converted conics requires iterating through the entire path, including converting any conics
267      * along the way, to calculate the true size.
268      */
269     override fun calculateSize(includeConvertedConics: Boolean): Int =
270         if (!includeConvertedConics || conicEvaluation == ConicEvaluation.AsConic) {
271             internalPathIteratorRawSize(internalPathIterator)
272         } else {
273             internalPathIteratorSize(internalPathIterator)
274         }
275 
276     /** Returns `true` if the iteration has more elements. */
hasNextnull277     override fun hasNext(): Boolean = internalPathIteratorHasNext(internalPathIterator)
278 
279     /**
280      * Returns the type of the current segment in the iteration, or [Done][PathSegment.Type.Done] if
281      * the iteration is finished.
282      */
283     override fun peek() = PathSegmentTypes[internalPathIteratorPeek(internalPathIterator)]
284 
285     /**
286      * This is where the actual work happens to get the next segment in the path, which happens in
287      * native code. This function is called by [next] in the base class, which then converts the
288      * resulting segment from conics to quadratics as necessary.
289      */
290     override fun next(points: FloatArray, offset: Int) =
291         PathSegmentTypes[internalPathIteratorNext(internalPathIterator, points, offset)]
292 
293     protected fun finalize() {
294         destroyInternalPathIterator(internalPathIterator)
295     }
296 }
297 
298 /** Cache of [PathSegment.Type] values to avoid internal allocation on each use. */
299 private val PathSegmentTypes = PathSegment.Type.values()
300