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