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