Merge "Optimize vector drawables parsing" into androidx-main
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index c0d6ad2..3aca759 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -1740,6 +1740,7 @@
method public androidx.compose.ui.graphics.vector.PathParser addPathNodes(java.util.List<? extends androidx.compose.ui.graphics.vector.PathNode> nodes);
method public void clear();
method public androidx.compose.ui.graphics.vector.PathParser parsePathString(String pathData);
+ method public java.util.ArrayList<androidx.compose.ui.graphics.vector.PathNode> pathStringToNodes(String pathData, optional java.util.ArrayList<androidx.compose.ui.graphics.vector.PathNode> nodes);
method public java.util.List<androidx.compose.ui.graphics.vector.PathNode> toNodes();
method public androidx.compose.ui.graphics.Path toPath(optional androidx.compose.ui.graphics.Path target);
}
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index 72309f3..8683f16 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -1800,6 +1800,7 @@
method public androidx.compose.ui.graphics.vector.PathParser addPathNodes(java.util.List<? extends androidx.compose.ui.graphics.vector.PathNode> nodes);
method public void clear();
method public androidx.compose.ui.graphics.vector.PathParser parsePathString(String pathData);
+ method public java.util.ArrayList<androidx.compose.ui.graphics.vector.PathNode> pathStringToNodes(String pathData, optional java.util.ArrayList<androidx.compose.ui.graphics.vector.PathNode> nodes);
method public java.util.List<androidx.compose.ui.graphics.vector.PathNode> toNodes();
method public androidx.compose.ui.graphics.Path toPath(optional androidx.compose.ui.graphics.Path target);
}
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt
index 41f582a..5a6746d 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt
@@ -151,18 +151,9 @@
when (this) {
RelativeCloseKey, CloseKey -> nodes.add(PathNode.Close)
- RelativeMoveToKey -> pathNodesFromArgs(
- nodes,
- args,
- count,
- NUM_MOVE_TO_ARGS
- ) { array, start ->
- PathNode.RelativeMoveTo(dx = array[start], dy = array[start + 1])
- }
+ RelativeMoveToKey -> pathRelativeMoveNodeFromArgs(nodes, args, count)
- MoveToKey -> pathNodesFromArgs(nodes, args, count, NUM_MOVE_TO_ARGS) { array, start ->
- PathNode.MoveTo(x = array[start], y = array[start + 1])
- }
+ MoveToKey -> pathMoveNodeFromArgs(nodes, args, count)
RelativeLineToKey -> pathNodesFromArgs(
nodes,
@@ -347,19 +338,47 @@
val end = count - numArgs
var index = 0
while (index <= end) {
- val node = nodeFor(args, index)
- nodes.add(when {
- // According to the spec, if a MoveTo is followed by multiple pairs of coordinates,
- // the subsequent pairs are treated as implicit corresponding LineTo commands.
- node is PathNode.MoveTo && index > 0 -> PathNode.LineTo(args[index], args[index + 1])
- node is PathNode.RelativeMoveTo && index > 0 ->
- PathNode.RelativeLineTo(args[index], args[index + 1])
- else -> node
- })
+ nodes.add(nodeFor(args, index))
index += numArgs
}
}
+// According to the spec, if a MoveTo is followed by multiple pairs of coordinates,
+// the subsequent pairs are treated as implicit corresponding LineTo commands.
+private fun pathMoveNodeFromArgs(
+ nodes: MutableList<PathNode>,
+ args: FloatArray,
+ count: Int
+) {
+ val end = count - NUM_MOVE_TO_ARGS
+ if (end >= 0) {
+ nodes.add(PathNode.MoveTo(args[0], args[1]))
+ var index = NUM_MOVE_TO_ARGS
+ while (index <= end) {
+ nodes.add(PathNode.LineTo(args[index], args[index + 1]))
+ index += NUM_MOVE_TO_ARGS
+ }
+ }
+}
+
+// According to the spec, if a RelativeMoveTo is followed by multiple pairs of coordinates,
+// the subsequent pairs are treated as implicit corresponding RelativeLineTo commands.
+private fun pathRelativeMoveNodeFromArgs(
+ nodes: MutableList<PathNode>,
+ args: FloatArray,
+ count: Int
+) {
+ val end = count - NUM_MOVE_TO_ARGS
+ if (end >= 0) {
+ nodes.add(PathNode.RelativeMoveTo(args[0], args[1]))
+ var index = NUM_MOVE_TO_ARGS
+ while (index <= end) {
+ nodes.add(PathNode.RelativeLineTo(args[index], args[index + 1]))
+ index += NUM_MOVE_TO_ARGS
+ }
+ }
+}
+
/**
* Constants used by [Char.addPathNodes] for creating [PathNode]s from parsed paths.
*/
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
index 5992ed6..e8dc93a 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
@@ -49,21 +49,44 @@
internal val EmptyArray = FloatArray(0)
class PathParser {
- private val nodes = ArrayList<PathNode>()
-
+ private var nodes: ArrayList<PathNode>? = null
private var nodeData = FloatArray(64)
+ /**
+ * Clears the collection of [PathNode] stored in this parser and returned by [toNodes].
+ */
fun clear() {
- nodes.clear()
+ nodes?.clear()
}
/**
- * Parses the path string to create a collection of PathNode instances with their corresponding
- * arguments
+ * Parses the SVG path string to extract [PathNode] instances for each path instruction
+ * (`lineTo`, `moveTo`, etc.). The [PathNode] are stored in this parser's internal list
+ * of nodes which can be queried by calling [toNodes]. Calling this method replaces any
+ * existing content in the current nodes list.
*/
fun parsePathString(pathData: String): PathParser {
- nodes.clear()
+ var dstNodes = nodes
+ if (dstNodes == null) {
+ dstNodes = ArrayList()
+ nodes = dstNodes
+ } else {
+ dstNodes.clear()
+ }
+ pathStringToNodes(pathData, dstNodes)
+ return this
+ }
+ /**
+ * Parses the path string and adds the corresponding [PathNode] instances to the
+ * specified [nodes] collection. This method returns [nodes].
+ */
+ @Suppress("ConcreteCollection")
+ fun pathStringToNodes(
+ pathData: String,
+ @Suppress("ConcreteCollection")
+ nodes: ArrayList<PathNode> = ArrayList()
+ ): ArrayList<PathNode> {
var start = 0
var end = pathData.length
@@ -120,11 +143,11 @@
} while (index < end && !value.isNaN())
}
- addNodes(command, nodeData, dataCount)
+ command.addPathNodes(nodes, nodeData, dataCount)
}
}
- return this
+ return nodes
}
@Suppress("NOTHING_TO_INLINE")
@@ -136,19 +159,31 @@
}
}
+ /**
+ * Adds the list of [PathNode] [nodes] to this parser's internal list of [PathNode].
+ * The resulting list can be obtained by calling [toNodes].
+ */
fun addPathNodes(nodes: List<PathNode>): PathParser {
- this.nodes.addAll(nodes)
+ var dstNodes = this.nodes
+ if (dstNodes == null) {
+ dstNodes = ArrayList()
+ this.nodes = dstNodes
+ }
+ dstNodes.addAll(nodes)
return this
}
- fun toNodes(): List<PathNode> = nodes
+ /**
+ * Returns this parser's list of [PathNode]. Note: this function does not return
+ * a copy of the list. The caller should make a copy when appropriate.
+ */
+ fun toNodes(): List<PathNode> = nodes ?: emptyList()
- fun toPath(target: Path = Path()) = nodes.toPath(target)
-
- @Suppress("NOTHING_TO_INLINE")
- private inline fun addNodes(cmd: Char, args: FloatArray, count: Int) {
- cmd.addPathNodes(nodes, args, count)
- }
+ /**
+ * Converts this parser's list of [PathNode] instances into a [Path]. A new
+ * [Path] is returned every time this method is invoked.
+ */
+ fun toPath(target: Path = Path()) = nodes?.toPath(target) ?: Path()
}
/**
diff --git a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt b/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
index 2b0aab9..50af6c4 100644
--- a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
+++ b/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
@@ -63,6 +63,44 @@
}
@Test
+ fun relativeMoveToBecomesRelativeLineTo() {
+ val linePath = object : TestPath() {
+ var lineToPoints = ArrayList<Offset>()
+
+ override fun relativeLineTo(dx: Float, dy: Float) {
+ lineToPoints.add(Offset(dx, dy))
+ }
+ }
+
+ val parser = PathParser()
+ parser.parsePathString("m0 0 2 5").toPath(linePath)
+
+ assertEquals(1, linePath.lineToPoints.size)
+ assertEquals(2.0f, linePath.lineToPoints[0].x)
+ assertEquals(5.0f, linePath.lineToPoints[0].y)
+ }
+
+ @Test
+ fun moveToBecomesLineTo() {
+ val linePath = object : TestPath() {
+ var lineToPoints = ArrayList<Offset>()
+
+ override fun lineTo(x: Float, y: Float) {
+ lineToPoints.add(Offset(x, y))
+ }
+ }
+
+ val parser = PathParser()
+ parser.parsePathString("M0 0 2 5 6 7").toPath(linePath)
+
+ assertEquals(2, linePath.lineToPoints.size)
+ assertEquals(2.0f, linePath.lineToPoints[0].x)
+ assertEquals(5.0f, linePath.lineToPoints[0].y)
+ assertEquals(6.0f, linePath.lineToPoints[1].x)
+ assertEquals(7.0f, linePath.lineToPoints[1].y)
+ }
+
+ @Test
fun relativeQuadToTest() {
val quadPath = object : TestPath() {
var lineToPoints = ArrayList<Offset>()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParser.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParser.android.kt
index 53c5b79..d707d26 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParser.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParser.android.kt
@@ -41,7 +41,7 @@
import androidx.compose.ui.graphics.vector.EmptyPath
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathNode
-import androidx.compose.ui.graphics.vector.addPathNodes
+import androidx.compose.ui.graphics.vector.PathParser
import androidx.compose.ui.unit.dp
import androidx.core.content.res.ComplexColorCompat
import androidx.core.content.res.TypedArrayUtils
@@ -279,8 +279,11 @@
) ?: ""
val pathStr = getString(a, AndroidVectorResources.STYLEABLE_VECTOR_DRAWABLE_PATH_PATH_DATA)
-
- val pathData: List<PathNode> = addPathNodes(pathStr)
+ val pathData: List<PathNode> = if (pathStr == null) {
+ EmptyPath
+ } else {
+ pathParser.pathStringToNodes(pathStr)
+ }
val fillColor = getNamedComplexColor(
a,
@@ -408,12 +411,11 @@
a,
AndroidVectorResources.STYLEABLE_VECTOR_DRAWABLE_CLIP_PATH_NAME
) ?: ""
- val pathData = addPathNodes(
- getString(
- a,
- AndroidVectorResources.STYLEABLE_VECTOR_DRAWABLE_CLIP_PATH_PATH_DATA
- )
+ val pathStr = getString(
+ a,
+ AndroidVectorResources.STYLEABLE_VECTOR_DRAWABLE_CLIP_PATH_PATH_DATA
)
+ val pathData = if (pathStr == null) EmptyPath else pathParser.pathStringToNodes(pathStr)
a.recycle()
// <clip-path> is parsed out as an additional VectorGroup.
@@ -527,6 +529,8 @@
val xmlParser: XmlPullParser,
var config: Int = 0
) {
+ @JvmField
+ internal val pathParser = PathParser()
private fun updateConfig(resConfig: Int) {
config = config or resConfig