1 /*
2  * Copyright 2019 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.compose.ui.graphics.vector
18 
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.ui.graphics.BlendMode
21 import androidx.compose.ui.graphics.Brush
22 import androidx.compose.ui.graphics.Color
23 import androidx.compose.ui.graphics.Path
24 import androidx.compose.ui.graphics.PathFillType
25 import androidx.compose.ui.graphics.StrokeCap
26 import androidx.compose.ui.graphics.StrokeJoin
27 import androidx.compose.ui.internal.checkPrecondition
28 import androidx.compose.ui.platform.makeSynchronizedObject
29 import androidx.compose.ui.platform.synchronized
30 import androidx.compose.ui.unit.Dp
31 
32 /**
33  * Vector graphics object that is generated as a result of [ImageVector.Builder] It can be composed
34  * and rendered by passing it as an argument to [rememberVectorPainter]
35  */
36 @Immutable
37 class ImageVector
38 internal constructor(
39 
40     /** Name of the Vector asset */
41     val name: String,
42 
43     /** Intrinsic width of the vector asset in [Dp] */
44     val defaultWidth: Dp,
45 
46     /** Intrinsic height of the vector asset in [Dp] */
47     val defaultHeight: Dp,
48 
49     /**
50      * Used to define the width of the viewport space. Viewport is basically the virtual canvas
51      * where the paths are drawn on.
52      */
53     val viewportWidth: Float,
54 
55     /**
56      * Used to define the height of the viewport space. Viewport is basically the virtual canvas
57      * where the paths are drawn on.
58      */
59     val viewportHeight: Float,
60 
61     /** Root group of the vector asset that contains all the child groups and paths */
62     val root: VectorGroup,
63 
64     /** Optional tint color to be applied to the vector graphic */
65     val tintColor: Color,
66 
67     /** Blend mode used to apply [tintColor] */
68     val tintBlendMode: BlendMode,
69 
70     /** Determines if the vector asset should automatically be mirrored for right to left locales */
71     val autoMirror: Boolean,
72 
73     /**
74      * Identifier used to disambiguate between different ImageVector instances in a more efficient
75      * manner than equality. This can be used as a key for caching instances of ImageVectors.
76      */
77     internal val genId: Int = generateImageVectorId(),
78 ) {
79     /**
80      * Builder used to construct a Vector graphic tree. This is useful for caching the result of
81      * expensive operations used to construct a vector graphic for compose. For example, the vector
82      * graphic could be serialized and downloaded from a server and represented internally in a
83      * ImageVector before it is composed through [rememberVectorPainter] The generated ImageVector
84      * is recommended to be memoized across composition calls to avoid doing redundant work
85      */
86     @Suppress("MissingGetterMatchingBuilder")
87     class Builder(
88 
89         /** Name of the vector asset */
90         private val name: String = DefaultGroupName,
91 
92         /** Intrinsic width of the Vector in [Dp] */
93         private val defaultWidth: Dp,
94 
95         /** Intrinsic height of the Vector in [Dp] */
96         private val defaultHeight: Dp,
97 
98         /**
99          * Used to define the width of the viewport space. Viewport is basically the virtual canvas
100          * where the paths are drawn on.
101          */
102         private val viewportWidth: Float,
103 
104         /**
105          * Used to define the height of the viewport space. Viewport is basically the virtual canvas
106          * where the paths are drawn on.
107          */
108         private val viewportHeight: Float,
109 
110         /** Optional color used to tint the entire vector image */
111         private val tintColor: Color = Color.Unspecified,
112 
113         /** Blend mode used to apply the tint color */
114         private val tintBlendMode: BlendMode = BlendMode.SrcIn,
115 
116         /**
117          * Determines if the vector asset should automatically be mirrored for right to left locales
118          */
119         private val autoMirror: Boolean = false
120     ) {
121 
122         // Secondary constructor to maintain API compatibility that defaults autoMirror to false
123         @Deprecated(
124             "Replace with ImageVector.Builder that consumes an optional auto " + "mirror parameter",
125             replaceWith =
126                 ReplaceWith(
127                     "Builder(name, defaultWidth, defaultHeight, viewportWidth, " +
128                         "viewportHeight, tintColor, tintBlendMode, false)",
129                     "androidx.compose.ui.graphics.vector"
130                 ),
131             DeprecationLevel.HIDDEN
132         )
133         constructor(
134             /** Name of the vector asset */
135             name: String = DefaultGroupName,
136 
137             /** Intrinsic width of the Vector in [Dp] */
138             defaultWidth: Dp,
139 
140             /** Intrinsic height of the Vector in [Dp] */
141             defaultHeight: Dp,
142 
143             /**
144              * Used to define the width of the viewport space. Viewport is basically the virtual
145              * canvas where the paths are drawn on.
146              */
147             viewportWidth: Float,
148 
149             /**
150              * Used to define the height of the viewport space. Viewport is basically the virtual
151              * canvas where the paths are drawn on.
152              */
153             viewportHeight: Float,
154 
155             /** Optional color used to tint the entire vector image */
156             tintColor: Color = Color.Unspecified,
157 
158             /** Blend mode used to apply the tint color */
159             tintBlendMode: BlendMode = BlendMode.SrcIn
160         ) : this(
161             name,
162             defaultWidth,
163             defaultHeight,
164             viewportWidth,
165             viewportHeight,
166             tintColor,
167             tintBlendMode,
168             false
169         )
170 
171         private val nodes = ArrayList<GroupParams>()
172 
173         private var root = GroupParams()
174         private var isConsumed = false
175 
176         private val currentGroup: GroupParams
177             get() = nodes.peek()
178 
179         init {
180             nodes.push(root)
181         }
182 
183         /**
184          * Create a new group and push it to the front of the stack of ImageVector nodes
185          *
186          * @param name the name of the group
187          * @param rotate the rotation of the group in degrees
188          * @param pivotX the x coordinate of the pivot point to rotate or scale the group
189          * @param pivotY the y coordinate of the pivot point to rotate or scale the group
190          * @param scaleX the scale factor in the X-axis to apply to the group
191          * @param scaleY the scale factor in the Y-axis to apply to the group
192          * @param translationX the translation in virtual pixels to apply along the x-axis
193          * @param translationY the translation in virtual pixels to apply along the y-axis
194          * @param clipPathData the path information used to clip the content within the group
195          * @return This ImageVector.Builder instance as a convenience for chaining calls
196          */
197         @Suppress("MissingGetterMatchingBuilder")
addGroupnull198         fun addGroup(
199             name: String = DefaultGroupName,
200             rotate: Float = DefaultRotation,
201             pivotX: Float = DefaultPivotX,
202             pivotY: Float = DefaultPivotY,
203             scaleX: Float = DefaultScaleX,
204             scaleY: Float = DefaultScaleY,
205             translationX: Float = DefaultTranslationX,
206             translationY: Float = DefaultTranslationY,
207             clipPathData: List<PathNode> = EmptyPath
208         ): Builder {
209             ensureNotConsumed()
210             val group =
211                 GroupParams(
212                     name,
213                     rotate,
214                     pivotX,
215                     pivotY,
216                     scaleX,
217                     scaleY,
218                     translationX,
219                     translationY,
220                     clipPathData
221                 )
222             nodes.push(group)
223             return this
224         }
225 
226         /**
227          * Pops the topmost VectorGroup from this ImageVector.Builder. This is used to indicate that
228          * no additional ImageVector nodes will be added to the current VectorGroup
229          *
230          * @return This ImageVector.Builder instance as a convenience for chaining calls
231          */
clearGroupnull232         fun clearGroup(): Builder {
233             ensureNotConsumed()
234             val popped = nodes.pop()
235             currentGroup.children.add(popped.asVectorGroup())
236             return this
237         }
238 
239         /**
240          * Add a path to the ImageVector graphic. This represents a leaf node in the ImageVector
241          * graphics tree structure
242          *
243          * @param pathData path information to render the shape of the path
244          * @param pathFillType rule to determine how the interior of the path is to be calculated
245          * @param name the name of the path
246          * @param fill specifies the [Brush] used to fill the path
247          * @param fillAlpha the alpha to fill the path
248          * @param stroke specifies the [Brush] used to fill the stroke
249          * @param strokeAlpha the alpha to stroke the path
250          * @param strokeLineWidth the width of the line to stroke the path
251          * @param strokeLineCap specifies the linecap for a stroked path
252          * @param strokeLineJoin specifies the linejoin for a stroked path
253          * @param strokeLineMiter specifies the miter limit for a stroked path
254          * @param trimPathStart specifies the fraction of the path to trim from the start in the
255          *   range from 0 to 1. Values outside the range will wrap around the length of the path.
256          *   Default is 0.
257          * @param trimPathEnd specifies the fraction of the path to trim from the end in the range
258          *   from 0 to 1. Values outside the range will wrap around the length of the path. Default
259          *   is 1.
260          * @param trimPathOffset specifies the fraction to shift the path trim region in the range
261          *   from 0 to 1. Values outside the range will wrap around the length of the path. Default
262          *   is 0.
263          * @return This ImageVector.Builder instance as a convenience for chaining calls
264          */
265         @Suppress("MissingGetterMatchingBuilder")
addPathnull266         fun addPath(
267             pathData: List<PathNode>,
268             pathFillType: PathFillType = DefaultFillType,
269             name: String = DefaultPathName,
270             fill: Brush? = null,
271             fillAlpha: Float = 1.0f,
272             stroke: Brush? = null,
273             strokeAlpha: Float = 1.0f,
274             strokeLineWidth: Float = DefaultStrokeLineWidth,
275             strokeLineCap: StrokeCap = DefaultStrokeLineCap,
276             strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin,
277             strokeLineMiter: Float = DefaultStrokeLineMiter,
278             trimPathStart: Float = DefaultTrimPathStart,
279             trimPathEnd: Float = DefaultTrimPathEnd,
280             trimPathOffset: Float = DefaultTrimPathOffset
281         ): Builder {
282             ensureNotConsumed()
283             currentGroup.children.add(
284                 VectorPath(
285                     name,
286                     pathData,
287                     pathFillType,
288                     fill,
289                     fillAlpha,
290                     stroke,
291                     strokeAlpha,
292                     strokeLineWidth,
293                     strokeLineCap,
294                     strokeLineJoin,
295                     strokeLineMiter,
296                     trimPathStart,
297                     trimPathEnd,
298                     trimPathOffset
299                 )
300             )
301             return this
302         }
303 
304         /**
305          * Construct a ImageVector. This concludes the creation process of a ImageVector graphic
306          * This builder cannot be re-used to create additional ImageVector instances
307          *
308          * @return The newly created ImageVector instance
309          */
buildnull310         fun build(): ImageVector {
311             ensureNotConsumed()
312             // pop all groups except for the root
313             while (nodes.size > 1) {
314                 clearGroup()
315             }
316 
317             val vectorImage =
318                 ImageVector(
319                     name,
320                     defaultWidth,
321                     defaultHeight,
322                     viewportWidth,
323                     viewportHeight,
324                     root.asVectorGroup(),
325                     tintColor,
326                     tintBlendMode,
327                     autoMirror
328                 )
329 
330             isConsumed = true
331 
332             return vectorImage
333         }
334 
335         /** Throws IllegalStateException if the ImageVector.Builder has already been consumed */
ensureNotConsumednull336         private fun ensureNotConsumed() {
337             checkPrecondition(!isConsumed) {
338                 "ImageVector.Builder is single use, create a new instance " +
339                     "to create a new ImageVector"
340             }
341         }
342 
343         /**
344          * Helper method to create an immutable VectorGroup object from an set of GroupParams which
345          * represent a group that is in the middle of being constructed
346          */
asVectorGroupnull347         private fun GroupParams.asVectorGroup(): VectorGroup =
348             VectorGroup(
349                 name,
350                 rotate,
351                 pivotX,
352                 pivotY,
353                 scaleX,
354                 scaleY,
355                 translationX,
356                 translationY,
357                 clipPathData,
358                 children
359             )
360 
361         /**
362          * Internal helper class to help assist with in progress creation of a vector group before
363          * creating the immutable result
364          */
365         private class GroupParams(
366             var name: String = DefaultGroupName,
367             var rotate: Float = DefaultRotation,
368             var pivotX: Float = DefaultPivotX,
369             var pivotY: Float = DefaultPivotY,
370             var scaleX: Float = DefaultScaleX,
371             var scaleY: Float = DefaultScaleY,
372             var translationX: Float = DefaultTranslationX,
373             var translationY: Float = DefaultTranslationY,
374             var clipPathData: List<PathNode> = EmptyPath,
375             var children: MutableList<VectorNode> = mutableListOf()
376         )
377     }
378 
379     companion object {
380         private var imageVectorCount = 0
381         private val lock = makeSynchronizedObject(this)
382 
383         internal fun generateImageVectorId(): Int {
384             synchronized(lock) {
385                 return imageVectorCount++
386             }
387         }
388     }
389 
equalsnull390     override fun equals(other: Any?): Boolean {
391         if (this === other) return true
392         if (other !is ImageVector) return false
393 
394         if (name != other.name) return false
395         if (defaultWidth != other.defaultWidth) return false
396         if (defaultHeight != other.defaultHeight) return false
397         if (viewportWidth != other.viewportWidth) return false
398         if (viewportHeight != other.viewportHeight) return false
399         if (root != other.root) return false
400         if (tintColor != other.tintColor) return false
401         if (tintBlendMode != other.tintBlendMode) return false
402         if (autoMirror != other.autoMirror) return false
403         return true
404     }
405 
hashCodenull406     override fun hashCode(): Int {
407         var result = name.hashCode()
408         result = 31 * result + defaultWidth.hashCode()
409         result = 31 * result + defaultHeight.hashCode()
410         result = 31 * result + viewportWidth.hashCode()
411         result = 31 * result + viewportHeight.hashCode()
412         result = 31 * result + root.hashCode()
413         result = 31 * result + tintColor.hashCode()
414         result = 31 * result + tintBlendMode.hashCode()
415         result = 31 * result + autoMirror.hashCode()
416         return result
417     }
418 }
419 
420 sealed class VectorNode
421 
422 /**
423  * Defines a group of paths or subgroups, plus transformation information. The transformations are
424  * defined in the same coordinates as the viewport. The transformations are applied in the order of
425  * scale, rotate then translate.
426  *
427  * This is constructed as part of the result of [ImageVector.Builder] construction
428  */
429 @Immutable
430 class VectorGroup
431 internal constructor(
432     /** Name of the corresponding group */
433     val name: String = DefaultGroupName,
434 
435     /** Rotation of the group in degrees */
436     val rotation: Float = DefaultRotation,
437 
438     /** X coordinate of the pivot point to rotate or scale the group */
439     val pivotX: Float = DefaultPivotX,
440 
441     /** Y coordinate of the pivot point to rotate or scale the group */
442     val pivotY: Float = DefaultPivotY,
443 
444     /** Scale factor in the X-axis to apply to the group */
445     val scaleX: Float = DefaultScaleX,
446 
447     /** Scale factor in the Y-axis to apply to the group */
448     val scaleY: Float = DefaultScaleY,
449 
450     /** Translation in virtual pixels to apply along the x-axis */
451     val translationX: Float = DefaultTranslationX,
452 
453     /** Translation in virtual pixels to apply along the y-axis */
454     val translationY: Float = DefaultTranslationY,
455 
456     /** Path information used to clip the content within the group */
457     val clipPathData: List<PathNode> = EmptyPath,
458 
459     /** Child Vector nodes that are part of this group, this can contain paths or other groups */
460     private val children: List<VectorNode> = emptyList()
461 ) : VectorNode(), Iterable<VectorNode> {
462 
463     val size: Int
464         get() = children.size
465 
getnull466     operator fun get(index: Int): VectorNode {
467         return children[index]
468     }
469 
iteratornull470     override fun iterator(): Iterator<VectorNode> {
471         return object : Iterator<VectorNode> {
472 
473             val it = children.iterator()
474 
475             override fun hasNext(): Boolean = it.hasNext()
476 
477             override fun next(): VectorNode = it.next()
478         }
479     }
480 
equalsnull481     override fun equals(other: Any?): Boolean {
482         if (this === other) return true
483         if (other == null || other !is VectorGroup) return false
484 
485         if (name != other.name) return false
486         if (rotation != other.rotation) return false
487         if (pivotX != other.pivotX) return false
488         if (pivotY != other.pivotY) return false
489         if (scaleX != other.scaleX) return false
490         if (scaleY != other.scaleY) return false
491         if (translationX != other.translationX) return false
492         if (translationY != other.translationY) return false
493         if (clipPathData != other.clipPathData) return false
494         if (children != other.children) return false
495 
496         return true
497     }
498 
hashCodenull499     override fun hashCode(): Int {
500         var result = name.hashCode()
501         result = 31 * result + rotation.hashCode()
502         result = 31 * result + pivotX.hashCode()
503         result = 31 * result + pivotY.hashCode()
504         result = 31 * result + scaleX.hashCode()
505         result = 31 * result + scaleY.hashCode()
506         result = 31 * result + translationX.hashCode()
507         result = 31 * result + translationY.hashCode()
508         result = 31 * result + clipPathData.hashCode()
509         result = 31 * result + children.hashCode()
510         return result
511     }
512 }
513 
514 /**
515  * Leaf node of a Vector graphics tree. This specifies a path shape and parameters to color and
516  * style the shape itself
517  *
518  * This is constructed as part of the result of [ImageVector.Builder] construction
519  */
520 @Immutable
521 class VectorPath
522 internal constructor(
523     /** Name of the corresponding path */
524     val name: String = DefaultPathName,
525 
526     /** Path information to render the shape of the path */
527     val pathData: List<PathNode>,
528 
529     /** Rule to determine how the interior of the path is to be calculated */
530     val pathFillType: PathFillType,
531 
532     /** Specifies the color or gradient used to fill the path */
533     val fill: Brush? = null,
534 
535     /** Opacity to fill the path */
536     val fillAlpha: Float = 1.0f,
537 
538     /** Specifies the color or gradient used to fill the stroke */
539     val stroke: Brush? = null,
540 
541     /** Opacity to stroke the path */
542     val strokeAlpha: Float = 1.0f,
543 
544     /** Width of the line to stroke the path */
545     val strokeLineWidth: Float = DefaultStrokeLineWidth,
546 
547     /**
548      * Specifies the linecap for a stroked path, either butt, round, or square. The default is butt.
549      */
550     val strokeLineCap: StrokeCap = DefaultStrokeLineCap,
551 
552     /**
553      * Specifies the linejoin for a stroked path, either miter, round or bevel. The default is miter
554      */
555     val strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin,
556 
557     /** Specifies the miter limit for a stroked path, the default is 4 */
558     val strokeLineMiter: Float = DefaultStrokeLineMiter,
559 
560     /**
561      * Specifies the fraction of the path to trim from the start, in the range from 0 to 1. The
562      * default is 0.
563      */
564     val trimPathStart: Float = DefaultTrimPathStart,
565 
566     /**
567      * Specifies the fraction of the path to trim from the end, in the range from 0 to 1. The
568      * default is 1.
569      */
570     val trimPathEnd: Float = DefaultTrimPathEnd,
571 
572     /**
573      * Specifies the offset of the trim region (allows showed region to include the start and end),
574      * in the range from 0 to 1. The default is 0.
575      */
576     val trimPathOffset: Float = DefaultTrimPathOffset
577 ) : VectorNode() {
578 
equalsnull579     override fun equals(other: Any?): Boolean {
580         if (this === other) return true
581         if (other == null || this::class != other::class) return false
582 
583         other as VectorPath
584 
585         if (name != other.name) return false
586         if (fill != other.fill) return false
587         if (fillAlpha != other.fillAlpha) return false
588         if (stroke != other.stroke) return false
589         if (strokeAlpha != other.strokeAlpha) return false
590         if (strokeLineWidth != other.strokeLineWidth) return false
591         if (strokeLineCap != other.strokeLineCap) return false
592         if (strokeLineJoin != other.strokeLineJoin) return false
593         if (strokeLineMiter != other.strokeLineMiter) return false
594         if (trimPathStart != other.trimPathStart) return false
595         if (trimPathEnd != other.trimPathEnd) return false
596         if (trimPathOffset != other.trimPathOffset) return false
597         if (pathFillType != other.pathFillType) return false
598         if (pathData != other.pathData) return false
599 
600         return true
601     }
602 
hashCodenull603     override fun hashCode(): Int {
604         var result = name.hashCode()
605         result = 31 * result + pathData.hashCode()
606         result = 31 * result + (fill?.hashCode() ?: 0)
607         result = 31 * result + fillAlpha.hashCode()
608         result = 31 * result + (stroke?.hashCode() ?: 0)
609         result = 31 * result + strokeAlpha.hashCode()
610         result = 31 * result + strokeLineWidth.hashCode()
611         result = 31 * result + strokeLineCap.hashCode()
612         result = 31 * result + strokeLineJoin.hashCode()
613         result = 31 * result + strokeLineMiter.hashCode()
614         result = 31 * result + trimPathStart.hashCode()
615         result = 31 * result + trimPathEnd.hashCode()
616         result = 31 * result + trimPathOffset.hashCode()
617         result = 31 * result + pathFillType.hashCode()
618         return result
619     }
620 }
621 
622 /**
623  * DSL extension for adding a [VectorPath] to [this].
624  *
625  * See [ImageVector.Builder.addPath] for the corresponding builder function.
626  *
627  * @param name the name for this path
628  * @param fill specifies the [Brush] used to fill the path
629  * @param fillAlpha the alpha to fill the path
630  * @param stroke specifies the [Brush] used to fill the stroke
631  * @param strokeAlpha the alpha to stroke the path
632  * @param strokeLineWidth the width of the line to stroke the path
633  * @param strokeLineCap specifies the linecap for a stroked path
634  * @param strokeLineJoin specifies the linejoin for a stroked path
635  * @param strokeLineMiter specifies the miter limit for a stroked path
636  * @param pathFillType specifies the winding rule that decides how the interior of a [Path] is
637  *   calculated.
638  * @param pathBuilder [PathBuilder] lambda for adding [PathNode]s to this path.
639  */
pathnull640 inline fun ImageVector.Builder.path(
641     name: String = DefaultPathName,
642     fill: Brush? = null,
643     fillAlpha: Float = 1.0f,
644     stroke: Brush? = null,
645     strokeAlpha: Float = 1.0f,
646     strokeLineWidth: Float = DefaultStrokeLineWidth,
647     strokeLineCap: StrokeCap = DefaultStrokeLineCap,
648     strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin,
649     strokeLineMiter: Float = DefaultStrokeLineMiter,
650     pathFillType: PathFillType = DefaultFillType,
651     pathBuilder: PathBuilder.() -> Unit
652 ) =
653     addPath(
654         PathData(pathBuilder),
655         pathFillType,
656         name,
657         fill,
658         fillAlpha,
659         stroke,
660         strokeAlpha,
661         strokeLineWidth,
662         strokeLineCap,
663         strokeLineJoin,
664         strokeLineMiter
665     )
666 
667 /**
668  * DSL extension for adding a [VectorGroup] to [this].
669  *
670  * See [ImageVector.Builder.pushGroup] for the corresponding builder function.
671  *
672  * @param name the name of the group
673  * @param rotate the rotation of the group in degrees
674  * @param pivotX the x coordinate of the pivot point to rotate or scale the group
675  * @param pivotY the y coordinate of the pivot point to rotate or scale the group
676  * @param scaleX the scale factor in the X-axis to apply to the group
677  * @param scaleY the scale factor in the Y-axis to apply to the group
678  * @param translationX the translation in virtual pixels to apply along the x-axis
679  * @param translationY the translation in virtual pixels to apply along the y-axis
680  * @param clipPathData the path information used to clip the content within the group
681  * @param block builder lambda to add children to this group
682  */
683 inline fun ImageVector.Builder.group(
684     name: String = DefaultGroupName,
685     rotate: Float = DefaultRotation,
686     pivotX: Float = DefaultPivotX,
687     pivotY: Float = DefaultPivotY,
688     scaleX: Float = DefaultScaleX,
689     scaleY: Float = DefaultScaleY,
690     translationX: Float = DefaultTranslationX,
691     translationY: Float = DefaultTranslationY,
692     clipPathData: List<PathNode> = EmptyPath,
693     block: ImageVector.Builder.() -> Unit
694 ) = apply {
695     addGroup(name, rotate, pivotX, pivotY, scaleX, scaleY, translationX, translationY, clipPathData)
696     block()
697     clearGroup()
698 }
699 
pushnull700 private fun <T> ArrayList<T>.push(value: T): Boolean = add(value)
701 
702 private fun <T> ArrayList<T>.pop(): T = this.removeAt(size - 1)
703 
704 private fun <T> ArrayList<T>.peek(): T = this[size - 1]
705