/*
* Copyright 2019 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.compose.ui.graphics.vector
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.unit.Dp
/**
* Vector graphics object that is generated as a result of [ImageVector.Builder]
* It can be composed and rendered by passing it as an argument to [rememberVectorPainter]
*/
@Immutable
class ImageVector internal constructor(
/**
* Name of the Vector asset
*/
val name: String,
/**
* Intrinsic width of the vector asset in [Dp]
*/
val defaultWidth: Dp,
/**
* Intrinsic height of the vector asset in [Dp]
*/
val defaultHeight: Dp,
/**
* Used to define the width of the viewport space. Viewport is basically the virtual canvas
* where the paths are drawn on.
*/
val viewportWidth: Float,
/**
* Used to define the height of the viewport space. Viewport is basically the virtual canvas
* where the paths are drawn on.
*/
val viewportHeight: Float,
/**
* Root group of the vector asset that contains all the child groups and paths
*/
val root: VectorGroup,
/**
* Optional tint color to be applied to the vector graphic
*/
val tintColor: Color,
/**
* Blend mode used to apply [tintColor]
*/
val tintBlendMode: BlendMode
) {
/**
* Builder used to construct a Vector graphic tree.
* This is useful for caching the result of expensive operations used to construct
* a vector graphic for compose.
* For example, the vector graphic could be serialized and downloaded from a server and represented
* internally in a ImageVector before it is composed through [rememberVectorPainter]
* The generated ImageVector is recommended to be memoized across composition calls to avoid
* doing redundant work
*/
@Suppress("MissingGetterMatchingBuilder")
class Builder(
/**
* Name of the vector asset
*/
private val name: String = DefaultGroupName,
/**
* Intrinsic width of the Vector in [Dp]
*/
private val defaultWidth: Dp,
/**
* Intrinsic height of the Vector in [Dp]
*/
private val defaultHeight: Dp,
/**
* Used to define the width of the viewport space. Viewport is basically the virtual canvas
* where the paths are drawn on.
*/
private val viewportWidth: Float,
/**
* Used to define the height of the viewport space. Viewport is basically the virtual canvas
* where the paths are drawn on.
*/
private val viewportHeight: Float,
/**
* Optional color used to tint the entire vector image
*/
private val tintColor: Color = Color.Unspecified,
/**
* Blend mode used to apply the tint color
*/
private val tintBlendMode: BlendMode = BlendMode.SrcIn
) {
private val nodes = Stack<GroupParams>()
private var root = GroupParams()
private var isConsumed = false
private val currentGroup: GroupParams
get() = nodes.peek()
init {
nodes.push(root)
}
/**
* Create a new group and push it to the front of the stack of ImageVector nodes
*
* @param name the name of the group
* @param rotate the rotation of the group in degrees
* @param pivotX the x coordinate of the pivot point to rotate or scale the group
* @param pivotY the y coordinate of the pivot point to rotate or scale the group
* @param scaleX the scale factor in the X-axis to apply to the group
* @param scaleY the scale factor in the Y-axis to apply to the group
* @param translationX the translation in virtual pixels to apply along the x-axis
* @param translationY the translation in virtual pixels to apply along the y-axis
* @param clipPathData the path information used to clip the content within the group
*
* @return This ImageVector.Builder instance as a convenience for chaining calls
*/
@Suppress("MissingGetterMatchingBuilder")
fun addGroup(
name: String = DefaultGroupName,
rotate: Float = DefaultRotation,
pivotX: Float = DefaultPivotX,
pivotY: Float = DefaultPivotY,
scaleX: Float = DefaultScaleX,
scaleY: Float = DefaultScaleY,
translationX: Float = DefaultTranslationX,
translationY: Float = DefaultTranslationY,
clipPathData: List<PathNode> = EmptyPath
): Builder {
ensureNotConsumed()
val group = GroupParams(
name,
rotate,
pivotX,
pivotY,
scaleX,
scaleY,
translationX,
translationY,
clipPathData
)
nodes.push(group)
return this
}
/**
* Create a new group and push it to the front of the stack of ImageVector nodes
*
* @param name the name of the group
* @param rotate the rotation of the group in degrees
* @param pivotX the x coordinate of the pivot point to rotate or scale the group
* @param pivotY the y coordinate of the pivot point to rotate or scale the group
* @param scaleX the scale factor in the X-axis to apply to the group
* @param scaleY the scale factor in the Y-axis to apply to the group
* @param translationX the translation in virtual pixels to apply along the x-axis
* @param translationY the translation in virtual pixels to apply along the y-axis
* @param clipPathData the path information used to clip the content within the group
*
* @return This ImageVector.Builder instance as a convenience for chaining calls
*/
@Deprecated(
"Use addGroup instead",
ReplaceWith(
"addGroup(" +
"name, " +
"rotate, " +
"pivotX, " +
"pivotY, " +
"scaleX, " +
"scaleY, " +
"translationX, " +
"translationY, " +
"clipPathData",
"androidx.compose.ui.graphics.vector"
)
)
@Suppress("BuilderSetStyle")
fun pushGroup(
name: String = DefaultGroupName,
rotate: Float = DefaultRotation,
pivotX: Float = DefaultPivotX,
pivotY: Float = DefaultPivotY,
scaleX: Float = DefaultScaleX,
scaleY: Float = DefaultScaleY,
translationX: Float = DefaultTranslationX,
translationY: Float = DefaultTranslationY,
clipPathData: List<PathNode> = EmptyPath
): Builder =
addGroup(
name,
rotate,
pivotX,
pivotY,
scaleX,
scaleY,
translationX,
translationY,
clipPathData
)
/**
* Pops the topmost VectorGroup from this ImageVector.Builder. This is used to indicate
* that no additional ImageVector nodes will be added to the current VectorGroup
* @return This ImageVector.Builder instance as a convenience for chaining calls
*/
fun clearGroup(): Builder {
ensureNotConsumed()
val popped = nodes.pop()
currentGroup.children.add(popped.asVectorGroup())
return this
}
/**
* Pops the topmost VectorGroup from this ImageVector.Builder. This is used to indicate
* that no additional ImageVector nodes will be added to the current VectorGroup
* @return This ImageVector.Builder instance as a convenience for chaining calls
*/
@Suppress("BuilderSetStyle")
@Deprecated(
"Use clearGroup instead",
ReplaceWith(
"clearGroup()",
"androidx.compose.ui.graphics.vector"
)
)
fun popGroup(): Builder = clearGroup()
/**
* Add a path to the ImageVector graphic. This represents a leaf node in the ImageVector graphics
* tree structure
*
* @param pathData path information to render the shape of the path
* @param pathFillType rule to determine how the interior of the path is to be calculated
* @param name the name of the path
* @param fill specifies the [Brush] used to fill the path
* @param fillAlpha the alpha to fill the path
* @param stroke specifies the [Brush] used to fill the stroke
* @param strokeAlpha the alpha to stroke the path
* @param strokeLineWidth the width of the line to stroke the path
* @param strokeLineCap specifies the linecap for a stroked path
* @param strokeLineJoin specifies the linejoin for a stroked path
* @param strokeLineMiter specifies the miter limit for a stroked path
* @param trimPathStart specifies the fraction of the path to trim from the start in the
* range from 0 to 1. Values outside the range will wrap around the length of the path.
* Default is 0.
* @param trimPathStart specifies the fraction of the path to trim from the end in the
* range from 0 to 1. Values outside the range will wrap around the length of the path.
* Default is 1.
* @param trimPathOffset specifies the fraction to shift the path trim region in the range
* from 0 to 1. Values outside the range will wrap around the length of the path. Default is 0.
*
* @return This ImageVector.Builder instance as a convenience for chaining calls
*/
@Suppress("MissingGetterMatchingBuilder")
fun addPath(
pathData: List<PathNode>,
pathFillType: PathFillType = DefaultFillType,
name: String = DefaultPathName,
fill: Brush? = null,
fillAlpha: Float = 1.0f,
stroke: Brush? = null,
strokeAlpha: Float = 1.0f,
strokeLineWidth: Float = DefaultStrokeLineWidth,
strokeLineCap: StrokeCap = DefaultStrokeLineCap,
strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin,
strokeLineMiter: Float = DefaultStrokeLineMiter,
trimPathStart: Float = DefaultTrimPathStart,
trimPathEnd: Float = DefaultTrimPathEnd,
trimPathOffset: Float = DefaultTrimPathOffset
): Builder {
ensureNotConsumed()
currentGroup.children.add(
VectorPath(
name,
pathData,
pathFillType,
fill,
fillAlpha,
stroke,
strokeAlpha,
strokeLineWidth,
strokeLineCap,
strokeLineJoin,
strokeLineMiter,
trimPathStart,
trimPathEnd,
trimPathOffset
)
)
return this
}
/**
* Construct a ImageVector. This concludes the creation process of a ImageVector graphic
* This builder cannot be re-used to create additional ImageVector instances
* @return The newly created ImageVector instance
*/
fun build(): ImageVector {
ensureNotConsumed()
// pop all groups except for the root
while (nodes.size > 1) {
clearGroup()
}
val vectorImage = ImageVector(
name,
defaultWidth,
defaultHeight,
viewportWidth,
viewportHeight,
root.asVectorGroup(),
tintColor,
tintBlendMode
)
isConsumed = true
return vectorImage
}
/**
* Throws IllegalStateException if the ImageVector.Builder has already been consumed
*/
private fun ensureNotConsumed() {
check(!isConsumed) {
"ImageVector.Builder is single use, create a new instance " +
"to create a new ImageVector"
}
}
/**
* Helper method to create an immutable VectorGroup object
* from an set of GroupParams which represent a group
* that is in the middle of being constructed
*/
private fun GroupParams.asVectorGroup(): VectorGroup =
VectorGroup(
name,
rotate,
pivotX,
pivotY,
scaleX,
scaleY,
translationX,
translationY,
clipPathData,
children
)
/**
* Internal helper class to help assist with in progress creation of
* a vector group before creating the immutable result
*/
private class GroupParams(
var name: String = DefaultGroupName,
var rotate: Float = DefaultRotation,
var pivotX: Float = DefaultPivotX,
var pivotY: Float = DefaultPivotY,
var scaleX: Float = DefaultScaleX,
var scaleY: Float = DefaultScaleY,
var translationX: Float = DefaultTranslationX,
var translationY: Float = DefaultTranslationY,
var clipPathData: List<PathNode> = EmptyPath,
var children: MutableList<VectorNode> = mutableListOf()
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ImageVector) return false
if (name != other.name) return false
if (defaultWidth != other.defaultWidth) return false
if (defaultHeight != other.defaultHeight) return false
if (viewportWidth != other.viewportWidth) return false
if (viewportHeight != other.viewportHeight) return false
if (root != other.root) return false
if (tintColor != other.tintColor) return false
if (tintBlendMode != other.tintBlendMode) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + defaultWidth.hashCode()
result = 31 * result + defaultHeight.hashCode()
result = 31 * result + viewportWidth.hashCode()
result = 31 * result + viewportHeight.hashCode()
result = 31 * result + root.hashCode()
result = 31 * result + tintColor.hashCode()
result = 31 * result + tintBlendMode.hashCode()
return result
}
}
sealed class VectorNode
/**
* Defines a group of paths or subgroups, plus transformation information.
* The transformations are defined in the same coordinates as the viewport.
* The transformations are applied in the order of scale, rotate then translate.
*
* This is constructed as part of the result of [ImageVector.Builder] construction
*/
@Immutable
class VectorGroup internal constructor(
/**
* Name of the corresponding group
*/
val name: String = DefaultGroupName,
/**
* Rotation of the group in degrees
*/
val rotation: Float = DefaultRotation,
/**
* X coordinate of the pivot point to rotate or scale the group
*/
val pivotX: Float = DefaultPivotX,
/**
* Y coordinate of the pivot point to rotate or scale the group
*/
val pivotY: Float = DefaultPivotY,
/**
* Scale factor in the X-axis to apply to the group
*/
val scaleX: Float = DefaultScaleX,
/**
* Scale factor in the Y-axis to apply to the group
*/
val scaleY: Float = DefaultScaleY,
/**
* Translation in virtual pixels to apply along the x-axis
*/
val translationX: Float = DefaultTranslationX,
/**
* Translation in virtual pixels to apply along the y-axis
*/
val translationY: Float = DefaultTranslationY,
/**
* Path information used to clip the content within the group
*/
val clipPathData: List<PathNode> = EmptyPath,
/**
* Child Vector nodes that are part of this group, this can contain
* paths or other groups
*/
private val children: List<VectorNode> = emptyList()
) : VectorNode(), Iterable<VectorNode> {
val size: Int
get() = children.size
operator fun get(index: Int): VectorNode {
return children[index]
}
override fun iterator(): Iterator<VectorNode> {
return object : Iterator<VectorNode> {
val it = children.iterator()
override fun hasNext(): Boolean = it.hasNext()
override fun next(): VectorNode = it.next()
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || other !is VectorGroup) return false
if (name != other.name) return false
if (rotation != other.rotation) return false
if (pivotX != other.pivotX) return false
if (pivotY != other.pivotY) return false
if (scaleX != other.scaleX) return false
if (scaleY != other.scaleY) return false
if (translationX != other.translationX) return false
if (translationY != other.translationY) return false
if (clipPathData != other.clipPathData) return false
if (children != other.children) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + rotation.hashCode()
result = 31 * result + pivotX.hashCode()
result = 31 * result + pivotY.hashCode()
result = 31 * result + scaleX.hashCode()
result = 31 * result + scaleY.hashCode()
result = 31 * result + translationX.hashCode()
result = 31 * result + translationY.hashCode()
result = 31 * result + clipPathData.hashCode()
result = 31 * result + children.hashCode()
return result
}
}
/**
* Leaf node of a Vector graphics tree. This specifies a path shape and parameters
* to color and style the shape itself
*
* This is constructed as part of the result of [ImageVector.Builder] construction
*/
@Immutable
class VectorPath internal constructor(
/**
* Name of the corresponding path
*/
val name: String = DefaultPathName,
/**
* Path information to render the shape of the path
*/
val pathData: List<PathNode>,
/**
* Rule to determine how the interior of the path is to be calculated
*/
val pathFillType: PathFillType,
/**
* Specifies the color or gradient used to fill the path
*/
val fill: Brush? = null,
/**
* Opacity to fill the path
*/
val fillAlpha: Float = 1.0f,
/**
* Specifies the color or gradient used to fill the stroke
*/
val stroke: Brush? = null,
/**
* Opacity to stroke the path
*/
val strokeAlpha: Float = 1.0f,
/**
* Width of the line to stroke the path
*/
val strokeLineWidth: Float = DefaultStrokeLineWidth,
/**
* Specifies the linecap for a stroked path, either butt, round, or square. The default is butt.
*/
val strokeLineCap: StrokeCap = DefaultStrokeLineCap,
/**
* Specifies the linejoin for a stroked path, either miter, round or bevel. The default is miter
*/
val strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin,
/**
* Specifies the miter limit for a stroked path, the default is 4
*/
val strokeLineMiter: Float = DefaultStrokeLineMiter,
/**
* Specifies the fraction of the path to trim from the start, in the range from 0 to 1.
* The default is 0.
*/
val trimPathStart: Float = DefaultTrimPathStart,
/**
* Specifies the fraction of the path to trim from the end, in the range from 0 to 1.
* The default is 1.
*/
val trimPathEnd: Float = DefaultTrimPathEnd,
/**
* Specifies the offset of the trim region (allows showed region to include the start and end),
* in the range from 0 to 1. The default is 0.
*/
val trimPathOffset: Float = DefaultTrimPathOffset
) : VectorNode() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as VectorPath
if (name != other.name) return false
if (fill != other.fill) return false
if (fillAlpha != other.fillAlpha) return false
if (stroke != other.stroke) return false
if (strokeAlpha != other.strokeAlpha) return false
if (strokeLineWidth != other.strokeLineWidth) return false
if (strokeLineCap != other.strokeLineCap) return false
if (strokeLineJoin != other.strokeLineJoin) return false
if (strokeLineMiter != other.strokeLineMiter) return false
if (trimPathStart != other.trimPathStart) return false
if (trimPathEnd != other.trimPathEnd) return false
if (trimPathOffset != other.trimPathOffset) return false
if (pathFillType != other.pathFillType) return false
if (pathData != other.pathData) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + pathData.hashCode()
result = 31 * result + (fill?.hashCode() ?: 0)
result = 31 * result + fillAlpha.hashCode()
result = 31 * result + (stroke?.hashCode() ?: 0)
result = 31 * result + strokeAlpha.hashCode()
result = 31 * result + strokeLineWidth.hashCode()
result = 31 * result + strokeLineCap.hashCode()
result = 31 * result + strokeLineJoin.hashCode()
result = 31 * result + strokeLineMiter.hashCode()
result = 31 * result + trimPathStart.hashCode()
result = 31 * result + trimPathEnd.hashCode()
result = 31 * result + trimPathOffset.hashCode()
result = 31 * result + pathFillType.hashCode()
return result
}
}
/**
* DSL extension for adding a [VectorPath] to [this].
*
* See [ImageVector.Builder.addPath] for the corresponding builder function.
*
* @param name the name for this path
* @param fill specifies the [Brush] used to fill the path
* @param fillAlpha the alpha to fill the path
* @param stroke specifies the [Brush] used to fill the stroke
* @param strokeAlpha the alpha to stroke the path
* @param strokeLineWidth the width of the line to stroke the path
* @param strokeLineCap specifies the linecap for a stroked path
* @param strokeLineJoin specifies the linejoin for a stroked path
* @param strokeLineMiter specifies the miter limit for a stroked path
* @param pathBuilder [PathBuilder] lambda for adding [PathNode]s to this path.
*/
inline fun ImageVector.Builder.path(
name: String = DefaultPathName,
fill: Brush? = null,
fillAlpha: Float = 1.0f,
stroke: Brush? = null,
strokeAlpha: Float = 1.0f,
strokeLineWidth: Float = DefaultStrokeLineWidth,
strokeLineCap: StrokeCap = DefaultStrokeLineCap,
strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin,
strokeLineMiter: Float = DefaultStrokeLineMiter,
pathFillType: PathFillType = DefaultFillType,
pathBuilder: PathBuilder.() -> Unit
) = addPath(
PathData(pathBuilder),
pathFillType,
name,
fill,
fillAlpha,
stroke,
strokeAlpha,
strokeLineWidth,
strokeLineCap,
strokeLineJoin,
strokeLineMiter
)
/**
* DSL extension for adding a [VectorGroup] to [this].
*
* See [ImageVector.Builder.pushGroup] for the corresponding builder function.
*
* @param name the name of the group
* @param rotate the rotation of the group in degrees
* @param pivotX the x coordinate of the pivot point to rotate or scale the group
* @param pivotY the y coordinate of the pivot point to rotate or scale the group
* @param scaleX the scale factor in the X-axis to apply to the group
* @param scaleY the scale factor in the Y-axis to apply to the group
* @param translationX the translation in virtual pixels to apply along the x-axis
* @param translationY the translation in virtual pixels to apply along the y-axis
* @param clipPathData the path information used to clip the content within the group
* @param block builder lambda to add children to this group
*/
inline fun ImageVector.Builder.group(
name: String = DefaultGroupName,
rotate: Float = DefaultRotation,
pivotX: Float = DefaultPivotX,
pivotY: Float = DefaultPivotY,
scaleX: Float = DefaultScaleX,
scaleY: Float = DefaultScaleY,
translationX: Float = DefaultTranslationX,
translationY: Float = DefaultTranslationY,
clipPathData: List<PathNode> = EmptyPath,
block: ImageVector.Builder.() -> Unit
) = apply {
addGroup(
name,
rotate,
pivotX,
pivotY,
scaleX,
scaleY,
translationX,
translationY,
clipPathData
)
block()
clearGroup()
}
@Deprecated(
"Use ImageVector.Builder instead",
ReplaceWith(
"ImageVector.Builder",
"androidx.compose.ui.graphics.vector"
)
)
typealias VectorAssetBuilder = ImageVector.Builder
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
private inline class Stack<T>(private val backing: ArrayList<T> = ArrayList<T>()) {
val size: Int get() = backing.size
fun push(value: T) = backing.add(value)
fun pop(): T = backing.removeAt(size - 1)
fun peek(): T = backing[size - 1]
fun isEmpty() = backing.isEmpty()
fun isNotEmpty() = !isEmpty()
fun clear() = backing.clear()
}