Skip to content

Commit 69e4d45

Browse files
t-regbsegorikftp
authored andcommitted
feat: add support for parsing and generating vector drawable gradients (linear and radial)
1 parent 8d9d6c8 commit 69e4d45

File tree

7 files changed

+646
-24
lines changed

7 files changed

+646
-24
lines changed

components/parser/kmp/xml/src/commonMain/kotlin/io/github/composegears/valkyrie/parser/kmp/xml/XmlToImageVectorParser.kt

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@ object XmlToImageVectorParser {
3737
}
3838

3939
private fun VectorDrawable.Path.toIrPath(): IrVectorNode.IrPath {
40+
val gradientFill = aaptAttr?.gradient?.toIrFill()
41+
val colorFill = fillColor?.toIrColor()?.let { IrFill.Color(it) }
42+
4043
return IrVectorNode.IrPath(
4144
name = name.orEmpty(),
42-
fill = fillColor?.toIrColor()?.let { IrFill.Color(it) },
45+
fill = gradientFill ?: colorFill,
4346
fillAlpha = alpha,
4447
stroke = strokeColor?.toIrColor()?.let { IrStroke.Color(it) },
4548
strokeAlpha = strokeAlpha?.toFloatOrNull() ?: 1f,
@@ -84,4 +87,46 @@ object XmlToImageVectorParser {
8487
private fun String.asFillType(): IrPathFillType = IrPathFillType.entries.find { it.name.equals(this, ignoreCase = true) } ?: IrPathFillType.NonZero
8588

8689
private fun String.toIrColor(): IrColor? = AndroidColorParser.parse(this) ?: IrColor(this).takeUnless { it.isTransparent() }
90+
91+
private fun VectorDrawable.Gradient.toIrFill(): IrFill? {
92+
return when (type.lowercase()) {
93+
"linear" -> {
94+
IrFill.LinearGradient(
95+
startX = startX ?: 0f,
96+
startY = startY ?: 0f,
97+
endX = endX ?: 0f,
98+
endY = endY ?: 0f,
99+
colorStops = buildColorStops(),
100+
)
101+
}
102+
"radial" -> {
103+
IrFill.RadialGradient(
104+
radius = gradientRadius ?: 0f,
105+
centerX = centerX ?: 0f,
106+
centerY = centerY ?: 0f,
107+
colorStops = buildColorStops(),
108+
)
109+
}
110+
else -> null
111+
}
112+
}
113+
114+
private fun VectorDrawable.Gradient.buildColorStops(): MutableList<IrFill.ColorStop> {
115+
return if (items.isNotEmpty()) {
116+
items.mapNotNull { item ->
117+
item.color.toIrColor()?.let { color ->
118+
IrFill.ColorStop(item.offset, color)
119+
}
120+
}
121+
} else {
122+
buildList {
123+
startColor?.toIrColor()?.let { color ->
124+
add(IrFill.ColorStop(0f, color))
125+
}
126+
endColor?.toIrColor()?.let { color ->
127+
add(IrFill.ColorStop(1f, color))
128+
}
129+
}
130+
}.toMutableList()
131+
}
87132
}

components/parser/kmp/xml/src/commonTest/kotlin/io/github/composegears/valkyrie/parser/kmp/xml/XmlToImageVectorParserTest.kt

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import io.github.composegears.valkyrie.sdk.ir.core.IrColor
44
import io.github.composegears.valkyrie.sdk.ir.core.IrFill
55
import io.github.composegears.valkyrie.sdk.ir.core.IrImageVector
66
import io.github.composegears.valkyrie.sdk.ir.core.IrPathFillType
7+
import io.github.composegears.valkyrie.sdk.ir.core.IrPathNode.Close
8+
import io.github.composegears.valkyrie.sdk.ir.core.IrPathNode.LineTo
9+
import io.github.composegears.valkyrie.sdk.ir.core.IrPathNode.MoveTo
710
import io.github.composegears.valkyrie.sdk.ir.core.IrStroke
811
import io.github.composegears.valkyrie.sdk.ir.core.IrStrokeLineCap
912
import io.github.composegears.valkyrie.sdk.ir.core.IrStrokeLineJoin
@@ -167,6 +170,292 @@ class XmlToImageVectorParserTest {
167170
}
168171
}
169172

173+
@Test
174+
fun parse_path_with_linear_gradient() {
175+
val vector = vectorWithGradient(
176+
"""
177+
<path android:pathData="M0,0 L10,0 L10,10 L0,10 Z">
178+
<aapt:attr name="android:fillColor">
179+
<gradient
180+
android:type="linear"
181+
android:startX="0"
182+
android:startY="0"
183+
android:endX="10"
184+
android:endY="10"
185+
android:startColor="#FF0000"
186+
android:endColor="#0000FF"/>
187+
</aapt:attr>
188+
</path>
189+
""".trimIndent(),
190+
)
191+
192+
assertEquals(
193+
expected = imageVector(
194+
nodes = listOf(
195+
IrVectorNode.IrPath(
196+
pathFillType = IrPathFillType.NonZero,
197+
fill = IrFill.LinearGradient(
198+
startX = 0f,
199+
startY = 0f,
200+
endX = 10f,
201+
endY = 10f,
202+
colorStops = mutableListOf(
203+
IrFill.ColorStop(0f, IrColor(0xFFFF0000)),
204+
IrFill.ColorStop(1f, IrColor(0xFF0000FF)),
205+
),
206+
),
207+
fillAlpha = 1f,
208+
paths = listOf(MoveTo(x=0f, y=0f), LineTo(x=10f, y=0f), LineTo(x=10f, y=10f), LineTo(x=0f, y=10f), Close),
209+
),
210+
),
211+
),
212+
actual = XmlToImageVectorParser.parse(vector),
213+
)
214+
}
215+
216+
@Test
217+
fun parse_path_with_radial_gradient() {
218+
val vector = vectorWithGradient(
219+
"""
220+
<path android:pathData="M0,0 L10,0 L10,10 L0,10 Z">
221+
<aapt:attr name="android:fillColor">
222+
<gradient
223+
android:type="radial"
224+
android:centerX="5"
225+
android:centerY="5"
226+
android:gradientRadius="7.5"
227+
android:startColor="#00FF00"
228+
android:endColor="#FFFF00"/>
229+
</aapt:attr>
230+
</path>
231+
""".trimIndent(),
232+
)
233+
234+
assertEquals(
235+
expected = imageVector(
236+
nodes = listOf(
237+
IrVectorNode.IrPath(
238+
pathFillType = IrPathFillType.NonZero,
239+
fill = IrFill.RadialGradient(
240+
centerX = 5f,
241+
centerY = 5f,
242+
radius = 7.5f,
243+
colorStops = mutableListOf(
244+
IrFill.ColorStop(0f, IrColor(0xFF00FF00)),
245+
IrFill.ColorStop(1f, IrColor(0xFFFFFF00)),
246+
),
247+
),
248+
fillAlpha = 1f,
249+
paths = listOf(
250+
MoveTo(x = 0f, y = 0f),
251+
LineTo(x = 10f, y = 0f),
252+
LineTo(x = 10f, y = 10f),
253+
LineTo(x = 0f, y = 10f),
254+
Close
255+
),
256+
),
257+
),
258+
),
259+
actual = XmlToImageVectorParser.parse(vector),
260+
)
261+
}
262+
263+
@Test
264+
fun parse_path_with_linear_gradient_and_color_stops() {
265+
val vector = vectorWithGradient(
266+
"""
267+
<path android:pathData="M0,0 L10,0 L10,10 L0,10 Z">
268+
<aapt:attr name="android:fillColor">
269+
<gradient
270+
android:type="linear"
271+
android:startX="0"
272+
android:startY="0"
273+
android:endX="10"
274+
android:endY="0">
275+
<item android:color="#FF0000" android:offset="0"/>
276+
<item android:color="#00FF00" android:offset="0.5"/>
277+
<item android:color="#0000FF" android:offset="1"/>
278+
</gradient>
279+
</aapt:attr>
280+
</path>
281+
""".trimIndent(),
282+
)
283+
284+
assertEquals(
285+
expected = imageVector(
286+
nodes = listOf(
287+
IrVectorNode.IrPath(
288+
pathFillType = IrPathFillType.NonZero,
289+
fill = IrFill.LinearGradient(
290+
startX = 0f,
291+
startY = 0f,
292+
endX = 10f,
293+
endY = 0f,
294+
colorStops = mutableListOf(
295+
IrFill.ColorStop(0f, IrColor(0xFFFF0000)),
296+
IrFill.ColorStop(0.5f, IrColor(0xFF00FF00)),
297+
IrFill.ColorStop(1f, IrColor(0xFF0000FF)),
298+
),
299+
),
300+
fillAlpha = 1f,
301+
paths = listOf(MoveTo(x=0f, y=0f), LineTo(x=10f, y=0f), LineTo(x=10f, y=10f), LineTo(x=0f, y=10f), Close),
302+
),
303+
),
304+
),
305+
actual = XmlToImageVectorParser.parse(vector),
306+
)
307+
}
308+
309+
@Test
310+
fun parse_path_with_radial_gradient_and_color_stops() {
311+
val vector = vectorWithGradient(
312+
"""
313+
<path android:pathData="M0,0 L10,0 L10,10 L0,10 Z">
314+
<aapt:attr name="android:fillColor">
315+
<gradient
316+
android:type="radial"
317+
android:centerX="5"
318+
android:centerY="5"
319+
android:gradientRadius="10">
320+
<item android:color="#FFFFFF" android:offset="0"/>
321+
<item android:color="#888888" android:offset="0.7"/>
322+
<item android:color="#000000" android:offset="1"/>
323+
</gradient>
324+
</aapt:attr>
325+
</path>
326+
""".trimIndent(),
327+
)
328+
329+
assertEquals(
330+
expected = imageVector(
331+
nodes = listOf(
332+
IrVectorNode.IrPath(
333+
pathFillType = IrPathFillType.NonZero,
334+
fill = IrFill.RadialGradient(
335+
centerX = 5f,
336+
centerY = 5f,
337+
radius = 10f,
338+
colorStops = mutableListOf(
339+
IrFill.ColorStop(0f, IrColor(0xFFFFFFFF)),
340+
IrFill.ColorStop(0.7f, IrColor(0xFF888888)),
341+
IrFill.ColorStop(1f, IrColor(0xFF000000)),
342+
),
343+
),
344+
fillAlpha = 1f,
345+
paths = listOf(MoveTo(x=0f, y=0f), LineTo(x=10f, y=0f), LineTo(x=10f, y=10f), LineTo(x=0f, y=10f), Close),
346+
),
347+
),
348+
),
349+
actual = XmlToImageVectorParser.parse(vector),
350+
)
351+
}
352+
353+
@Test
354+
fun parse_multiple_paths_with_gradients() {
355+
val vector = vectorWithGradient(
356+
"""
357+
<path android:pathData="M0,0 L5,0 L5,5 L0,5 Z">
358+
<aapt:attr name="android:fillColor">
359+
<gradient
360+
android:type="linear"
361+
android:startX="0"
362+
android:startY="0"
363+
android:endX="5"
364+
android:endY="0"
365+
android:startColor="#FF0000"
366+
android:endColor="#00FF00"/>
367+
</aapt:attr>
368+
</path>
369+
<path android:pathData="M6,0 L10,0 L10,5 L6,5 Z">
370+
<aapt:attr name="android:fillColor">
371+
<gradient
372+
android:type="radial"
373+
android:centerX="8"
374+
android:centerY="2.5"
375+
android:gradientRadius="2.5"
376+
android:startColor="#0000FF"
377+
android:endColor="#FFFF00"/>
378+
</aapt:attr>
379+
</path>
380+
""".trimIndent(),
381+
)
382+
val result = XmlToImageVectorParser.parse(vector)
383+
384+
assertEquals(2, result.nodes.size)
385+
val firstPath = result.nodes[0] as IrVectorNode.IrPath
386+
val secondPath = result.nodes[1] as IrVectorNode.IrPath
387+
388+
assertEquals(
389+
expected = IrFill.LinearGradient(
390+
startX = 0f,
391+
startY = 0f,
392+
endX = 5f,
393+
endY = 0f,
394+
colorStops = mutableListOf(
395+
IrFill.ColorStop(0f, IrColor(0xFFFF0000)),
396+
IrFill.ColorStop(1f, IrColor(0xFF00FF00)),
397+
),
398+
),
399+
actual = firstPath.fill,
400+
)
401+
402+
assertEquals(
403+
expected = IrFill.RadialGradient(
404+
centerX = 8f,
405+
centerY = 2.5f,
406+
radius = 2.5f,
407+
colorStops = mutableListOf(
408+
IrFill.ColorStop(0f, IrColor(0xFF0000FF)),
409+
IrFill.ColorStop(1f, IrColor(0xFFFFFF00)),
410+
),
411+
),
412+
actual = secondPath.fill,
413+
)
414+
}
415+
416+
@Test
417+
fun parse_path_with_gradient_in_group() {
418+
val vector = vectorWithGradient(
419+
"""
420+
<group android:name="gradientGroup">
421+
<path android:pathData="M0,0 L10,0 L10,10 L0,10 Z">
422+
<aapt:attr name="android:fillColor">
423+
<gradient
424+
android:type="linear"
425+
android:startX="0"
426+
android:startY="0"
427+
android:endX="10"
428+
android:endY="10"
429+
android:startColor="#FFFFFF"
430+
android:endColor="#000000"/>
431+
</aapt:attr>
432+
</path>
433+
</group>
434+
""".trimIndent(),
435+
)
436+
val result = XmlToImageVectorParser.parse(vector)
437+
438+
assertEquals(1, result.nodes.size)
439+
val group = result.nodes[0] as IrVectorNode.IrGroup
440+
assertEquals("gradientGroup", group.name)
441+
assertEquals(1, group.nodes.size)
442+
443+
val path = group.nodes[0] as IrVectorNode.IrPath
444+
assertEquals(
445+
expected = IrFill.LinearGradient(
446+
startX = 0f,
447+
startY = 0f,
448+
endX = 10f,
449+
endY = 10f,
450+
colorStops = mutableListOf(
451+
IrFill.ColorStop(0f, IrColor(0xFFFFFFFF)),
452+
IrFill.ColorStop(1f, IrColor(0xFF000000)),
453+
),
454+
),
455+
actual = path.fill,
456+
)
457+
}
458+
170459
private fun vector(
171460
content: String,
172461
width: String = "24dp",
@@ -185,6 +474,25 @@ class XmlToImageVectorParserTest {
185474
appendLine("</vector>")
186475
}
187476

477+
private fun vectorWithGradient(
478+
content: String,
479+
width: String = "24dp",
480+
height: String = "24dp",
481+
): String = buildString {
482+
appendLine(
483+
"""
484+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
485+
xmlns:aapt="http://schemas.android.com/aapt"
486+
android:width="$width"
487+
android:height="$height"
488+
android:viewportWidth="24"
489+
android:viewportHeight="24">
490+
""".trimIndent(),
491+
)
492+
appendLine(content)
493+
appendLine("</vector>")
494+
}
495+
188496
private fun imageVector(nodes: List<IrVectorNode> = emptyList()): IrImageVector = IrImageVector(
189497
defaultWidth = 24f,
190498
defaultHeight = 24f,

0 commit comments

Comments
 (0)