diff --git a/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/ImageParser.kt b/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/ImageParser.kt index 4a30f3d..d7bd368 100644 --- a/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/ImageParser.kt +++ b/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/ImageParser.kt @@ -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 @@ -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) @@ -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() @@ -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)}") @@ -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" diff --git a/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/ImageVectorGenerator.kt b/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/ImageVectorGenerator.kt index 88b77d5..0ef8f55 100644 --- a/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/ImageVectorGenerator.kt +++ b/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/ImageVectorGenerator.kt @@ -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 @@ -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, @@ -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() + val memberList = mutableListOf() - 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, + memberList: List, +) { 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()) } diff --git a/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/Names.kt b/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/Names.kt index 12812b6..77525e1 100644 --- a/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/Names.kt +++ b/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/Names.kt @@ -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") @@ -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") } /** diff --git a/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/vector/StrokeCap.kt b/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/vector/StrokeCap.kt new file mode 100644 index 0000000..0f772da --- /dev/null +++ b/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/vector/StrokeCap.kt @@ -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, +} diff --git a/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/vector/Vector.kt b/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/vector/Vector.kt index 1a2aaa5..658cbf4 100644 --- a/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/vector/Vector.kt +++ b/compose-vectorize-gradle-plugin/src/main/kotlin/dev/sergiobelda/compose/vectorize/generator/vector/Vector.kt @@ -33,10 +33,13 @@ class Vector( sealed class VectorNode { class Group(val paths: MutableList = mutableListOf()) : VectorNode() class Path( - val strokeAlpha: Float, val fillAlpha: Float, val fillColor: String, val fillType: FillType, val nodes: List, + val strokeAlpha: Float, + val strokeCap: StrokeCap, + val strokeColor: String, + val strokeWidth: Float, ) : VectorNode() } diff --git a/sample-mpp/common/src/commonMain/kotlin/dev/sergiobelda/compose/vectorize/sample/MainScreen.kt b/sample-mpp/common/src/commonMain/kotlin/dev/sergiobelda/compose/vectorize/sample/MainScreen.kt index dd4a8bc..d0f7937 100644 --- a/sample-mpp/common/src/commonMain/kotlin/dev/sergiobelda/compose/vectorize/sample/MainScreen.kt +++ b/sample-mpp/common/src/commonMain/kotlin/dev/sergiobelda/compose/vectorize/sample/MainScreen.kt @@ -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() + } } } } diff --git a/sample-mpp/common/xml-images/illustrations/compose-multiplatform.xml b/sample-mpp/common/xml-images/illustrations/compose-multiplatform.xml new file mode 100644 index 0000000..9746be4 --- /dev/null +++ b/sample-mpp/common/xml-images/illustrations/compose-multiplatform.xml @@ -0,0 +1,19 @@ + + + + + + + + +