/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.graphics.path
import android.graphics.Path
import android.graphics.PathIterator as PlatformPathIterator
import android.graphics.PointF
import androidx.annotation.RequiresApi
import androidx.core.os.BuildCompat
import androidx.graphics.path.PathIterator.ConicEvaluation
/**
* Base class for API-version-specific PathIterator implementation classes. All functionality
* is implemented in the subclasses except for [next], which relies on shared native code
* to perform conic conversion.
*/
@Suppress("IllegalExperimentalApiUsage")
@BuildCompat.PrereleaseSdkCheck
internal abstract class PathIteratorImpl(
val path: Path,
val conicEvaluation: ConicEvaluation = ConicEvaluation.AsQuadratics,
val tolerance: Float = 0.25f
) {
/**
* An iterator's ConicConverter converts from a conic to a series of
* quadratics. It keeps track of the resulting quadratics and iterates through
* them on ensuing calls to next(). The converter is only ever called if
* [conicEvaluation] is set to [ConicEvaluation.AsQuadratics].
*/
var conicConverter = ConicConverter()
/**
* pointsData is used internally when the no-arg variant of next() is called,
* to avoid allocating a new array every time.
*/
val pointsData = FloatArray(8)
private companion object {
init {
/**
* The native library is used mainly for pre-API34, but we also rely
* on the conic conversion code in API34+, thus it is initialized here.
*/
System.loadLibrary("androidx.graphics.path")
}
}
abstract fun calculateSize(includeConvertedConics: Boolean): Int
abstract fun hasNext(): Boolean
abstract fun peek(): PathSegment.Type
/**
* The core functionality of [next] is in API-specific subclasses. But we implement [next]
* at this level to share the same conic conversion implementation across all versions.
* This happens by calling [nextImpl] to get the next segment from the subclasses, then
* calling the shared [ConicConverter] code when appropriate to get and return the
* converted segments.
*/
abstract fun nextImpl(points: FloatArray, offset: Int = 0): PathSegment.Type
fun next(points: FloatArray, offset: Int = 0): PathSegment.Type {
check(points.size - offset >= 8) { "The points array must contain at least 8 floats" }
// First check to see if we are currently iterating through converted conics
if (conicConverter.currentQuadratic < conicConverter.quadraticCount
) {
conicConverter.nextQuadratic(points, offset)
return (pathSegmentTypes[PathSegment.Type.Quadratic.ordinal])
} else {
val typeValue = nextImpl(points, offset)
if (typeValue == PathSegment.Type.Conic &&
conicEvaluation == ConicEvaluation.AsQuadratics
) {
with(conicConverter) {
convert(points, points[6 + offset], tolerance, offset)
if (quadraticCount > 0) {
nextQuadratic(points, offset)
}
}
return PathSegment.Type.Quadratic
}
return typeValue
}
}
fun next(): PathSegment {
val type = next(pointsData, 0)
if (type == PathSegment.Type.Done) return DoneSegment
if (type == PathSegment.Type.Close) return CloseSegment
val weight = if (type == PathSegment.Type.Conic) pointsData[6] else 0.0f
return PathSegment(type, floatsToPoints(pointsData, type), weight)
}
/**
* Utility function to convert a FloatArray to an array of PointF objects, where
* every two Floats in the FloatArray correspond to a single PointF in the resulting
* point array. The FloatArray is used internally to process a next() call, the
* array of points is used to create a PathSegment from the operation.
*/
private fun floatsToPoints(pointsData: FloatArray, type: PathSegment.Type): Array<PointF> {
val points = when (type) {
PathSegment.Type.Move -> {
arrayOf(PointF(pointsData[0], pointsData[1]))
}
PathSegment.Type.Line -> {
arrayOf(
PointF(pointsData[0], pointsData[1]),
PointF(pointsData[2], pointsData[3])
)
}
PathSegment.Type.Quadratic,
PathSegment.Type.Conic -> {
arrayOf(
PointF(pointsData[0], pointsData[1]),
PointF(pointsData[2], pointsData[3]),
PointF(pointsData[4], pointsData[5])
)
}
PathSegment.Type.Cubic -> {
arrayOf(
PointF(pointsData[0], pointsData[1]),
PointF(pointsData[2], pointsData[3]),
PointF(pointsData[4], pointsData[5]),
PointF(pointsData[6], pointsData[7])
)
}
// This should not happen because of the early returns above
else -> emptyArray()
}
return points
}
}
/**
* In API level 34, we can use new platform functionality for most of what PathIterator does.
* The exceptions are conic conversion (which is handled in the base impl class) and
* [calculateSize], which is implemented here.
*/
@RequiresApi(34)
@Suppress("IllegalExperimentalApiUsage")
@BuildCompat.PrereleaseSdkCheck
internal class PathIteratorApi34Impl(
path: Path,
conicEvaluation: ConicEvaluation = ConicEvaluation.AsQuadratics,
tolerance: Float = 0.25f
) : PathIteratorImpl(path, conicEvaluation, tolerance) {
/**
* The platform iterator handles most of what we need for iterating. We hold an instance
* of that object in this class.
*/
private val platformIterator: PlatformPathIterator
init {
platformIterator = path.pathIterator
}
/**
* The platform does not expose a calculateSize() method, so we implement our own. In the
* simplest case, this is done by simply iterating through all segments until done. However, if
* the caller requested the true size (including any conic conversion) and if there are any
* conics in the path segments, then there is more work to do since we have to convert and count
* those segments as well.
*/
override fun calculateSize(includeConvertedConics: Boolean): Int {
val convertConics = includeConvertedConics &&
conicEvaluation == ConicEvaluation.AsQuadratics
var numVerbs = 0
val tempIterator = path.pathIterator
val tempFloats = FloatArray(8)
while (tempIterator.hasNext()) {
val type = tempIterator.next(tempFloats, 0)
if (type == PlatformPathIterator.VERB_CONIC && convertConics) {
with(conicConverter) {
convert(tempFloats, tempFloats[6], tolerance)
numVerbs += quadraticCount
}
} else {
numVerbs++
}
}
return numVerbs
}
/**
* [nextImpl] is called by [next] in the base class to do the work of actually getting the
* next segment, for which we defer to the platform iterator.
*/
override fun nextImpl(points: FloatArray, offset: Int): PathSegment.Type {
return platformToAndroidXSegmentType(platformIterator.next(points, offset))
}
override fun hasNext(): Boolean {
return platformIterator.hasNext()
}
override fun peek(): PathSegment.Type {
val platformType = platformIterator.peek()
return platformToAndroidXSegmentType(platformType)
}
/**
* Callers need the AndroidX segment types, so we must convert from the platform types.
*/
private fun platformToAndroidXSegmentType(platformType: Int): PathSegment.Type {
return when (platformType) {
PlatformPathIterator.VERB_CLOSE -> PathSegment.Type.Close
PlatformPathIterator.VERB_CONIC -> PathSegment.Type.Conic
PlatformPathIterator.VERB_CUBIC -> PathSegment.Type.Cubic
PlatformPathIterator.VERB_DONE -> PathSegment.Type.Done
PlatformPathIterator.VERB_LINE -> PathSegment.Type.Line
PlatformPathIterator.VERB_MOVE -> PathSegment.Type.Move
PlatformPathIterator.VERB_QUAD -> PathSegment.Type.Quadratic
else -> {
throw IllegalArgumentException("Unknown path segment type $platformType")
}
}
}
}
/**
* Most of the functionality for pre-34 iteration is handled in the native code. The only
* exception, similar to the API34 implementation, is the calculateSize(). There is a size()
* function in native code which is very quick (it simply tracks the number of verbs in the native
* structure). But if the caller wants conic conversion, then we need to iterate through
* and convert appropriately, counting as we iterate.
*/
@Suppress("IllegalExperimentalApiUsage")
@BuildCompat.PrereleaseSdkCheck
internal class PathIteratorPreApi34Impl(
path: Path,
conicEvaluation: ConicEvaluation = ConicEvaluation.AsQuadratics,
tolerance: Float = 0.25f
) : PathIteratorImpl(path, conicEvaluation, tolerance) {
@Suppress("KotlinJniMissingFunction")
private external fun createInternalPathIterator(
path: Path,
conicEvaluation: Int,
tolerance: Float
): Long
@Suppress("KotlinJniMissingFunction")
private external fun destroyInternalPathIterator(internalPathIterator: Long)
@Suppress("KotlinJniMissingFunction")
private external fun internalPathIteratorHasNext(internalPathIterator: Long): Boolean
@Suppress("KotlinJniMissingFunction")
private external fun internalPathIteratorNext(
internalPathIterator: Long,
points: FloatArray,
offset: Int
): Int
@Suppress("KotlinJniMissingFunction")
private external fun internalPathIteratorPeek(internalPathIterator: Long): Int
@Suppress("KotlinJniMissingFunction")
private external fun internalPathIteratorRawSize(internalPathIterator: Long): Int
@Suppress("KotlinJniMissingFunction")
private external fun internalPathIteratorSize(internalPathIterator: Long): Int
/**
* Defines the type of evaluation to apply to conic segments during iteration.
*/
private val internalPathIterator =
createInternalPathIterator(path, ConicEvaluation.AsConic.ordinal, tolerance)
/**
* Returns the number of verbs present in this iterator's path. If [includeConvertedConics]
* property is false and the path has any conic elements, the returned size might be smaller
* than the number of calls to [next] required to fully iterate over the path. An accurate
* size can be computed by setting the parameter to true instead, at a performance cost.
* Including converted conics requires iterating through the entire path, including converting
* any conics along the way, to calculate the true size.
*/
override fun calculateSize(includeConvertedConics: Boolean): Int {
var numVerbs = 0
if (!includeConvertedConics || conicEvaluation == ConicEvaluation.AsConic) {
numVerbs = internalPathIteratorSize(internalPathIterator)
} else {
val tempIterator =
createInternalPathIterator(path, ConicEvaluation.AsConic.ordinal, tolerance)
val tempFloats = FloatArray(8)
while (internalPathIteratorHasNext(tempIterator)) {
val segment = internalPathIteratorNext(tempIterator, tempFloats, 0)
when (pathSegmentTypes[segment]) {
PathSegment.Type.Conic -> {
conicConverter.convert(tempFloats, tempFloats[7], tolerance)
numVerbs += conicConverter.quadraticCount
}
else -> numVerbs++
}
}
}
return numVerbs
}
/**
* Returns `true` if the iteration has more elements.
*/
override fun hasNext(): Boolean = internalPathIteratorHasNext(internalPathIterator)
/**
* Returns the type of the current segment in the iteration, or [Done][PathSegment.Type.Done]
* if the iteration is finished.
*/
override fun peek() = pathSegmentTypes[internalPathIteratorPeek(internalPathIterator)]
/**
* This is where the actual work happens to get the next segment in the path, which happens
* in native code. This function is called by [next] in the base class, which then converts
* the resulting segment from conics to quadratics as necessary.
*/
override fun nextImpl(points: FloatArray, offset: Int): PathSegment.Type {
return pathSegmentTypes[internalPathIteratorNext(internalPathIterator, points, offset)]
}
protected fun finalize() {
destroyInternalPathIterator(internalPathIterator)
}
}