Skip to content

Commit

Permalink
Add stroke properties to generated Path
Browse files Browse the repository at this point in the history
Also, refactor ImageVectorGenerator code
  • Loading branch information
serbelga committed Feb 3, 2024
1 parent af45a26 commit 3c6f019
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package dev.sergiobelda.compose.vectorize.generator

import dev.sergiobelda.compose.vectorize.generator.vector.FillType
import dev.sergiobelda.compose.vectorize.generator.vector.PathParser
import dev.sergiobelda.compose.vectorize.generator.vector.StrokeCap
import dev.sergiobelda.compose.vectorize.generator.vector.Vector
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode
import org.xmlpull.v1.XmlPullParser
Expand Down Expand Up @@ -68,21 +69,32 @@ class ImageParser(private val image: Image) {
PATH_DATA,
)
val fillAlpha = parser.getValueAsFloat(FILL_ALPHA)
val strokeAlpha = parser.getValueAsFloat(STROKE_ALPHA)
val fillColor = parser.getValueAsString(FILL_COLOR).processFillColor()

val fillColor = parser.getValueAsString(FILL_COLOR).processColor()
val fillType = when (parser.getAttributeValue(null, FILL_TYPE)) {
// evenOdd and nonZero are the only supported values here, where
// nonZero is the default if no values are defined.
EVEN_ODD -> FillType.EvenOdd
else -> FillType.NonZero
}
val strokeAlpha = parser.getValueAsFloat(STROKE_ALPHA)
val strokeCap = when (parser.getAttributeValue(null, "android:strokeLineCap")) {
BUTT -> StrokeCap.Butt
ROUND -> StrokeCap.Round
SQUARE -> StrokeCap.Square
else -> StrokeCap.Butt
}
val strokeColor = parser.getValueAsString(STROKE_COLOR).processColor()
val strokeWidth = parser.getValueAsFloat(STROKE_WIDTH)

val path = VectorNode.Path(
strokeAlpha = strokeAlpha ?: 1f,
fillAlpha = fillAlpha ?: 1f,
fillColor = fillColor.uppercase(),
fillType = fillType,
nodes = PathParser.parsePathString(pathData),
strokeAlpha = strokeAlpha ?: 1f,
strokeCap = strokeCap,
strokeColor = strokeColor,
strokeWidth = strokeWidth ?: 0f,
)
if (currentGroup != null) {
currentGroup.paths.add(path)
Expand Down Expand Up @@ -125,7 +137,7 @@ private fun XmlPullParser.getValueAsFloat(name: String): Float? =
* @return the string value for the attribute [name], or null if it couldn't be found
*/
private fun XmlPullParser.getValueAsString(name: String): String =
getAttributeValue(null, name)
getAttributeValue(null, name)?.toString() ?: ""

private fun XmlPullParser.seekToStartTag(): XmlPullParser {
var type = next()
Expand All @@ -145,7 +157,7 @@ private fun XmlPullParser.isAtEnd() =
private fun String.processDpDimension(): String =
this.replace("dp", "")

private fun String.processFillColor(): String {
private fun String.processColor(): String {
val diff = ARGB_HEXADECIMAL_COLOR_LENGTH - this.length
return if (diff > 0) {
this.replace("#", "#${"F".repeat(diff)}")
Expand All @@ -169,9 +181,14 @@ private const val FILL_TYPE = "android:fillType"
private const val HEIGHT = "android:height"
private const val PATH_DATA = "android:pathData"
private const val STROKE_ALPHA = "android:strokeAlpha"
private const val STROKE_COLOR = "android:strokeColor"
private const val STROKE_WIDTH = "android:strokeWidth"
private const val VIEWPORT_HEIGHT = "android:viewportHeight"
private const val VIEWPORT_WIDTH = "android:viewportWidth"
private const val WIDTH = "android:width"

// XML attribute values
private const val BUTT = "butt"
private const val EVEN_ODD = "evenOdd"
private const val ROUND = "round"
private const val SQUARE = "square"
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.buildCodeBlock
import dev.sergiobelda.compose.vectorize.generator.utils.setIndent
import dev.sergiobelda.compose.vectorize.generator.vector.FillType
import dev.sergiobelda.compose.vectorize.generator.vector.StrokeCap
import dev.sergiobelda.compose.vectorize.generator.vector.Vector
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode
import java.util.Locale
Expand Down Expand Up @@ -103,17 +105,24 @@ class ImageVectorGenerator(
)
.addCode(
buildCodeBlock {
val controlFlow = buildString {
append("%N = %M(\n")
append(" name = \"%N\",\n")
append(" width = ${vector.width}f,\n")
append(" height = ${vector.height}f,\n")
append(" viewportWidth = ${vector.viewportWidth}f,\n")
append(" viewportHeight = ${vector.viewportHeight}f\n")
append(")")
val parameterList = listOfNotNull(
"name = \"%N\"",
"width = ${vector.width}f",
"height = ${vector.height}f",
"viewportWidth = ${vector.viewportWidth}f",
"viewportHeight = ${vector.viewportHeight}f",
)
val parameters = if (parameterList.isNotEmpty()) {
parameterList.joinToString(
prefix = "%N = %M(\n\t",
postfix = "\n)",
separator = ",\n\t",
)
} else {
""
}
beginControlFlow(
controlFlow,
parameters,
backingProperty,
MemberNames.ImageVector,
imageName,
Expand Down Expand Up @@ -167,40 +176,60 @@ private fun CodeBlock.Builder.addPath(
path: VectorNode.Path,
pathBody: CodeBlock.Builder.() -> Unit,
) {
// Only set the fill type if it is EvenOdd - otherwise it will just be the default.
val setFillType = path.fillType == FillType.EvenOdd
val parameterList = mutableListOf<String>()
val memberList = mutableListOf<MemberName>()

val parameterList = with(path) {
listOfNotNull(
"fill = %M(%M(${fillColor.replace("#", "0x")}))",
"fillAlpha = ${fillAlpha}f".takeIf { fillAlpha != 1f },
"strokeAlpha = ${strokeAlpha}f".takeIf { strokeAlpha != 1f },
"pathFillType = %M".takeIf { setFillType },
)
with(path) {
memberList.add(MemberNames.Path)

parameterList.add("fill = %M(%M(${fillColor.replace("#", "0x")}))")
memberList.add(MemberNames.SolidColor)
memberList.add(MemberNames.Color)
fillAlpha.takeIf { it != 1f }?.let {
parameterList.add("fillAlpha = ${fillAlpha}f")
}
fillType.takeIf { it != FillType.NonZero }?.let {
parameterList.add("pathFillType = %M")
memberList.add(MemberNames.EvenOdd)
}
strokeAlpha.takeIf { it != 1f }?.let {
parameterList.add("strokeAlpha = ${strokeAlpha}f")
}
strokeColor.takeIf { it.isNotEmpty() }?.let {
parameterList.add("stroke = %M(%M(${strokeColor.replace("#", "0x")}))")
memberList.add(MemberNames.SolidColor)
memberList.add(MemberNames.Color)
}
strokeCap.takeIf { it != StrokeCap.Butt }?.let {
parameterList.add("strokeLineCap = %M")
when (strokeCap) {
StrokeCap.Round -> memberList.add(MemberNames.Round)
StrokeCap.Square -> memberList.add(MemberNames.Square)
else -> memberList.add(MemberNames.Butt)
}
}
strokeWidth.takeIf { it != 0f }?.let {
parameterList.add("strokeLineWidth = ${strokeWidth}f")
}
}
addPathParameters(parameterList, memberList)
pathBody()
endControlFlow()
}

private fun CodeBlock.Builder.addPathParameters(
parameterList: List<String>,
memberList: List<MemberName>,
) {
val parameters = if (parameterList.isNotEmpty()) {
parameterList.joinToString(prefix = "(", postfix = ")")
parameterList.joinToString(
prefix = "(\n\t",
postfix = "\n)",
separator = ",\n\t",
)
} else {
""
}

if (setFillType) {
beginControlFlow(
"%M$parameters",
MemberNames.Path,
MemberNames.SolidColor,
MemberNames.Color,
MemberNames.EvenOdd,
)
} else {
beginControlFlow(
"%M$parameters",
MemberNames.Path,
MemberNames.SolidColor,
MemberNames.Color,
)
}
pathBody()
endControlFlow()
beginControlFlow("%M$parameters", *memberList.toTypedArray())
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ enum class PackageNames(val packageName: String) {
object ClassNames {
val ImageVector = PackageNames.VectorPackage.className("ImageVector")
val PathFillType = PackageNames.GraphicsPackage.className("PathFillType", "Companion")
val StrokeCapType = PackageNames.GraphicsPackage.className("StrokeCap", "Companion")
}

/**
* [MemberName]s used for image generation.
*/
object MemberNames {
val Butt = MemberName(ClassNames.StrokeCapType, "Butt")
val Color = MemberName(PackageNames.GraphicsPackage.packageName, "Color")
val EvenOdd = MemberName(ClassNames.PathFillType, "EvenOdd")
val Group = MemberName(PackageNames.VectorPackage.packageName, "group")
Expand All @@ -48,7 +50,9 @@ object MemberNames {
"imageVector",
)
val Path = MemberName(PackageNames.VectorPackage.packageName, "path")
val Round = MemberName(ClassNames.StrokeCapType, "Round")
val SolidColor = MemberName(PackageNames.GraphicsPackage.packageName, "SolidColor")
val Square = MemberName(ClassNames.StrokeCapType, "Square")
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2024 Sergio Belda
*
* 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 dev.sergiobelda.compose.vectorize.generator.vector

/**
* Styles to use for line endings.
*
* This maps to [androidx.compose.ui.graphics.StrokeCap] used in the framework, and can be defined in XML
* via `android:strokeLineCap`.
*/
enum class StrokeCap {
Butt,
Round,
Square,
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ class Vector(
sealed class VectorNode {
class Group(val paths: MutableList<Path> = mutableListOf()) : VectorNode()
class Path(
val strokeAlpha: Float,
val fillAlpha: Float,
val fillColor: String,
val fillType: FillType,
val nodes: List<PathNode>,
val strokeAlpha: Float,
val strokeCap: StrokeCap,
val strokeColor: String,
val strokeWidth: Float,
) : VectorNode()
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,66 @@

package dev.sergiobelda.compose.vectorize.sample

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.sergiobelda.compose.vectorize.sample.common.images.Images
import dev.sergiobelda.compose.vectorize.sample.common.images.icons.Add
import dev.sergiobelda.compose.vectorize.sample.common.images.illustrations.ComposeMultiplatform

@Composable
fun MainScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
Column(
modifier = Modifier.fillMaxSize().padding(top = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
IconButton(onClick = {}) {
Icon(Images.Icons.Add, contentDescription = null)
SampleCard(
title = "Images.Icons.Add"
) {
Icon(
imageVector = Images.Icons.Add,
contentDescription = null
)
}
SampleCard(
title = "Images.Illustrations.ComposeMultiplatform"
) {
Image(
imageVector = Images.Illustrations.ComposeMultiplatform,
contentDescription = null,
modifier = Modifier.size(120.dp)
)
}
}
}

@Composable
fun SampleCard(
title: String,
content: @Composable () -> Unit
) {
Card(modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp)) {
Column(modifier = Modifier.padding(12.dp)) {
Text(title)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxWidth().padding(12.dp)
) {
content()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="600dp"
android:height="600dp" android:viewportWidth="600" android:viewportHeight="600">
<path android:fillColor="#041619" android:fillType="nonZero"
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z" />
<path android:fillColor="#37BF6E" android:fillType="nonZero"
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z" />
<path android:fillColor="#3870B2" android:fillType="nonZero"
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z" />
<path android:fillColor="#00000000" android:fillType="nonZero"
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
android:strokeWidth="10" android:strokeColor="#083042" />
<path android:fillColor="#00000000" android:fillType="nonZero"
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
android:strokeWidth="10" android:strokeColor="#083042" />
<path android:fillColor="#00000000" android:fillType="nonZero"
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
android:strokeWidth="10" android:strokeColor="#083042" />
</vector>

0 comments on commit 3c6f019

Please sign in to comment.