Last active
June 3, 2023 17:38
-
-
Save amarland/8b10acde7ae2a6a9107a8c9ddbcc66e9 to your computer and use it in GitHub Desktop.
A (very) minimal SVG DSL for Jetpack Compose on Android (inspired by https://twitter.com/alexstyl/status/1659043650238844928).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.amarland.simplesvgdsl | |
import androidx.compose.foundation.Image | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.Immutable | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.geometry.Offset | |
import androidx.compose.ui.geometry.Size | |
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.vector.DefaultFillType | |
import androidx.compose.ui.graphics.vector.ImageVector | |
import androidx.compose.ui.graphics.vector.PathNode | |
import androidx.compose.ui.graphics.vector.addPathNodes | |
import androidx.compose.ui.unit.DpSize | |
import androidx.compose.ui.unit.dp | |
@Immutable | |
class ViewBox(val min: Offset, val size: Size) { | |
constructor(size: Size) : this(Offset.Zero, size) | |
constructor( | |
minX: Float = 0F, | |
minY: Float = 0F, | |
width: Float, | |
height: Float | |
) : this(Offset(minX, minY), Size(width, height)) | |
operator fun component1() = min.x | |
operator fun component2() = min.y | |
operator fun component3() = size.width | |
operator fun component4() = size.height | |
} | |
interface SvgPathScope { | |
var d: String | |
var fill: Brush | |
var fillType: PathFillType | |
} | |
interface SvgScope { | |
var tint: Color | |
fun path(block: SvgPathScope.() -> Unit) | |
} | |
@Composable | |
fun Svg( | |
contentDescription: String, | |
viewBox: ViewBox, | |
modifier: Modifier = Modifier, | |
size: DpSize = DpSize(viewBox.size.width.dp, viewBox.size.height.dp), | |
block: @Composable SvgScope.() -> Unit | |
) { | |
with(SvgScopeInstance) { | |
block() | |
val (width, height) = size | |
val (viewBoxMinX, viewBoxMinY, viewBoxWidth, viewBoxHeight) = viewBox | |
val imageVector = ImageVector.Builder( | |
defaultWidth = width, | |
defaultHeight = height, | |
viewportWidth = viewBoxWidth, | |
viewportHeight = viewBoxHeight, | |
tintColor = tint | |
).apply { | |
if (viewBoxMinX != 0F || viewBoxMinY != 0F) { | |
addGroup(translationX = -viewBoxMinX, translationY = -viewBoxMinY) | |
} | |
for ((pathNodes, fill, fillType) in paths) { | |
addPath( | |
pathData = pathNodes, | |
pathFillType = fillType, | |
fill = fill | |
) | |
} | |
}.build() | |
Image( | |
imageVector = imageVector, | |
contentDescription = contentDescription, | |
modifier = modifier | |
) | |
} | |
} | |
private object SvgPathScopeInstance : SvgPathScope { | |
override var d: String = "" | |
override var fill: Brush = SolidColor(Color.Black) | |
override var fillType: PathFillType = DefaultFillType | |
} | |
@Immutable | |
private data class Path( | |
val nodes: List<PathNode>, | |
val fill: Brush, | |
val fillType: PathFillType | |
) | |
private object SvgScopeInstance : SvgScope { | |
override var tint: Color = Color.Unspecified | |
private val _paths = mutableListOf<Path>() | |
val paths: Iterable<Path> = _paths | |
override fun path(block: SvgPathScope.() -> Unit) = | |
with(SvgPathScopeInstance) { | |
block() | |
val pathNodes = addPathNodes(d.takeUnless(String::isBlank)) | |
_paths += Path(pathNodes, fill, fillType) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.amarland.simplesvgdsl | |
import android.os.Bundle | |
import androidx.activity.ComponentActivity | |
import androidx.activity.compose.setContent | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.material3.MaterialTheme | |
import androidx.compose.material3.darkColorScheme | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.graphics.PathFillType | |
import androidx.compose.ui.graphics.SolidColor | |
import androidx.compose.ui.unit.DpSize | |
import androidx.compose.ui.unit.dp | |
class SvgDslActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContent { | |
MaterialTheme(colorScheme = darkColorScheme()) { | |
Box( | |
modifier = Modifier | |
.fillMaxSize() | |
.background(MaterialTheme.colorScheme.background) | |
) { | |
Svg( | |
contentDescription = "", | |
viewBox = ViewBox(0F, 0F, 100F, 100F), | |
modifier = Modifier.align(Alignment.Center), | |
size = DpSize(64.dp, 64.dp) | |
) { | |
val onBackgroundColorAsBrush = | |
SolidColor(MaterialTheme.colorScheme.onBackground) | |
path { | |
d = "M0 50a50 50 0 1 0 100 0a50 50 0 1 0 -100 0" | |
fill = onBackgroundColorAsBrush | |
} | |
val primaryColorAsBrush = SolidColor(MaterialTheme.colorScheme.primary) | |
path { | |
d = "M50 0L21 90L98 35L2 35L79 90z" | |
fill = primaryColorAsBrush | |
fillType = PathFillType.EvenOdd | |
} | |
} | |
} | |
} | |
} | |
} | |
} |
Author
amarland
commented
May 19, 2023
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment