You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

274 lines
10 KiB

/*
* Copyright 2020 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 org.jetbrains.compose.resources.vector
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.vector.DefaultPivotX
import androidx.compose.ui.graphics.vector.DefaultPivotY
import androidx.compose.ui.graphics.vector.DefaultRotation
import androidx.compose.ui.graphics.vector.DefaultScaleX
import androidx.compose.ui.graphics.vector.DefaultScaleY
import androidx.compose.ui.graphics.vector.DefaultTranslationX
import androidx.compose.ui.graphics.vector.DefaultTranslationY
import androidx.compose.ui.graphics.vector.EmptyPath
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.addPathNodes
import androidx.compose.ui.unit.Density
import org.jetbrains.compose.resources.vector.BuildContext.Group
import org.jetbrains.compose.resources.vector.xmldom.Element
import org.jetbrains.compose.resources.vector.xmldom.Node
// Parsing logic is the same as in Android implementation
// (compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParser.kt)
//
// Except there is no support for linking with external resources
// (for example, we can't reference to color defined in another file)
//
// Specification:
// https://developer.android.com/reference/android/graphics/drawable/VectorDrawable
private const val ANDROID_NS = "http://schemas.android.com/apk/res/android"
private const val AAPT_NS = "http://schemas.android.com/aapt"
private class BuildContext {
val currentGroups = mutableListOf<Group>()
enum class Group {
/**
* Group that exists in xml file
*/
Real,
/**
* Group that doesn't exist in xml file. We add it manually when we see <clip-path> node.
* It will be automatically popped when the real group will be popped.
*/
Virtual
}
}
internal fun Element.toImageVector(density: Density): ImageVector {
val context = BuildContext()
val builder = ImageVector.Builder(
defaultWidth = attributeOrNull(ANDROID_NS, "width").parseDp(density),
defaultHeight = attributeOrNull(ANDROID_NS, "height").parseDp(density),
viewportWidth = attributeOrNull(ANDROID_NS, "viewportWidth")?.toFloat() ?: 0f,
viewportHeight = attributeOrNull(ANDROID_NS, "viewportHeight")?.toFloat() ?: 0f
)
parseVectorNodes(builder, context)
return builder.build()
}
private fun Element.parseVectorNodes(builder: ImageVector.Builder, context: BuildContext) {
childrenSequence
.filterIsInstance<Element>()
.forEach {
it.parseVectorNode(builder, context)
}
}
private fun Element.parseVectorNode(builder: ImageVector.Builder, context: BuildContext) {
when (nodeName) {
"path" -> parsePath(builder)
"clip-path" -> parseClipPath(builder, context)
"group" -> parseGroup(builder, context)
}
}
private fun Element.parsePath(builder: ImageVector.Builder) {
builder.addPath(
pathData = addPathNodes(attributeOrNull(ANDROID_NS, "pathData")),
pathFillType = attributeOrNull(ANDROID_NS, "fillType")
?.let(::parseFillType) ?: PathFillType.NonZero,
name = attributeOrNull(ANDROID_NS, "name") ?: "",
fill = attributeOrNull(ANDROID_NS, "fillColor")?.let(::parseStringBrush)
?: apptAttr(ANDROID_NS, "fillColor")?.let(Element::parseElementBrush),
fillAlpha = attributeOrNull(ANDROID_NS, "fillAlpha")?.toFloat() ?: 1.0f,
stroke = attributeOrNull(ANDROID_NS, "strokeColor")?.let(::parseStringBrush)
?: apptAttr(ANDROID_NS, "strokeColor")?.let(Element::parseElementBrush),
strokeAlpha = attributeOrNull(ANDROID_NS, "strokeAlpha")?.toFloat() ?: 1.0f,
strokeLineWidth = attributeOrNull(ANDROID_NS, "strokeWidth")?.toFloat() ?: 1.0f,
strokeLineCap = attributeOrNull(ANDROID_NS, "strokeLineCap")
?.let(::parseStrokeCap) ?: StrokeCap.Butt,
strokeLineJoin = attributeOrNull(ANDROID_NS, "strokeLineJoin")
?.let(::parseStrokeJoin) ?: StrokeJoin.Miter,
strokeLineMiter = attributeOrNull(ANDROID_NS, "strokeMiterLimit")?.toFloat() ?: 1.0f,
trimPathStart = attributeOrNull(ANDROID_NS, "trimPathStart")?.toFloat() ?: 0.0f,
trimPathEnd = attributeOrNull(ANDROID_NS, "trimPathEnd")?.toFloat() ?: 1.0f,
trimPathOffset = attributeOrNull(ANDROID_NS, "trimPathOffset")?.toFloat() ?: 0.0f
)
}
private fun Element.parseClipPath(builder: ImageVector.Builder, context: BuildContext) {
builder.addGroup(
name = attributeOrNull(ANDROID_NS, "name") ?: "",
clipPathData = addPathNodes(attributeOrNull(ANDROID_NS, "pathData"))
)
context.currentGroups.add(Group.Virtual)
}
private fun Element.parseGroup(builder: ImageVector.Builder, context: BuildContext) {
builder.addGroup(
attributeOrNull(ANDROID_NS, "name") ?: "",
attributeOrNull(ANDROID_NS, "rotation")?.toFloat() ?: DefaultRotation,
attributeOrNull(ANDROID_NS, "pivotX")?.toFloat() ?: DefaultPivotX,
attributeOrNull(ANDROID_NS, "pivotY")?.toFloat() ?: DefaultPivotY,
attributeOrNull(ANDROID_NS, "scaleX")?.toFloat() ?: DefaultScaleX,
attributeOrNull(ANDROID_NS, "scaleY")?.toFloat() ?: DefaultScaleY,
attributeOrNull(ANDROID_NS, "translateX")?.toFloat() ?: DefaultTranslationX,
attributeOrNull(ANDROID_NS, "translateY")?.toFloat() ?: DefaultTranslationY,
EmptyPath
)
context.currentGroups.add(Group.Real)
parseVectorNodes(builder, context)
do {
val removedGroup = context.currentGroups.removeLastOrNull()
builder.clearGroup()
} while (removedGroup == Group.Virtual)
}
private fun parseStringBrush(str: String) = SolidColor(Color(parseColorValue(str)))
private fun Element.parseElementBrush(): Brush? =
childrenSequence
.filterIsInstance<Element>()
.find { it.nodeName == "gradient" }
?.parseGradient()
private fun Element.parseGradient(): Brush? {
return when (attributeOrNull(ANDROID_NS, "type")) {
"linear" -> parseLinearGradient()
"radial" -> parseRadialGradient()
"sweep" -> parseSweepGradient()
else -> null
}
}
private fun Element.parseLinearGradient() = Brush.linearGradient(
colorStops = parseColorStops(),
start = Offset(
attributeOrNull(ANDROID_NS, "startX")?.toFloat() ?: 0f,
attributeOrNull(ANDROID_NS, "startY")?.toFloat() ?: 0f
),
end = Offset(
attributeOrNull(ANDROID_NS, "endX")?.toFloat() ?: 0f,
attributeOrNull(ANDROID_NS, "endY")?.toFloat() ?: 0f
),
tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp
)
private fun Element.parseRadialGradient() = Brush.radialGradient(
colorStops = parseColorStops(),
center = Offset(
attributeOrNull(ANDROID_NS, "centerX")?.toFloat() ?: 0f,
attributeOrNull(ANDROID_NS, "centerY")?.toFloat() ?: 0f
),
radius = attributeOrNull(ANDROID_NS, "gradientRadius")?.toFloat() ?: 0f,
tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp
)
private fun Element.parseSweepGradient() = Brush.sweepGradient(
colorStops = parseColorStops(),
center = Offset(
attributeOrNull(ANDROID_NS, "centerX")?.toFloat() ?: 0f,
attributeOrNull(ANDROID_NS, "centerY")?.toFloat() ?: 0f,
)
)
private fun Element.parseColorStops(): Array<Pair<Float, Color>> {
val items = childrenSequence
.filterIsInstance<Element>()
.filter { it.nodeName == "item" }
.toList()
val colorStops = items.mapIndexedNotNullTo(mutableListOf()) { index, item ->
item.parseColorStop(defaultOffset = index.toFloat() / items.lastIndex.coerceAtLeast(1))
}
if (colorStops.isEmpty()) {
val startColor = attributeOrNull(ANDROID_NS, "startColor")?.let(::parseColorValue)
val centerColor = attributeOrNull(ANDROID_NS, "centerColor")?.let(::parseColorValue)
val endColor = attributeOrNull(ANDROID_NS, "endColor")?.let(::parseColorValue)
if (startColor != null) {
colorStops.add(0f to Color(startColor))
}
if (centerColor != null) {
colorStops.add(0.5f to Color(centerColor))
}
if (endColor != null) {
colorStops.add(1f to Color(endColor))
}
}
return colorStops.toTypedArray()
}
private fun Element.parseColorStop(defaultOffset: Float): Pair<Float, Color>? {
val offset = attributeOrNull(ANDROID_NS, "offset")?.toFloat() ?: defaultOffset
val color = attributeOrNull(ANDROID_NS, "color")?.let(::parseColorValue) ?: return null
return offset to Color(color)
}
private fun Element.attributeOrNull(namespace: String, name: String): String? {
val value = getAttributeNS(namespace, name)
return if (value.isNotBlank()) value else null
}
/**
* Attribute of an element can be represented as a separate child:
*
* <path ...>
* <aapt:attr name="android:fillColor">
* <gradient ...
* ...
* </gradient>
* </aapt:attr>
* </path>
*
* instead of:
*
* <path android:fillColor="red" ... />
*/
private fun Element.apptAttr(
namespace: String,
name: String
): Element? {
val prefix = lookupPrefix(namespace)
return childrenSequence
.filterIsInstance<Element>()
.find {
it.namespaceURI == AAPT_NS && it.localName == "attr" &&
it.getAttribute("name") == "$prefix:$name"
}
}
private val Element.childrenSequence get() = sequence<Node> {
for (i in 0 until childNodes.length) {
yield(childNodes.item(i))
}
}