Skip to content

Instantly share code, notes, and snippets.

@alexstyl
Forked from amarland/SvgDsl.kt
Created May 19, 2023 13:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexstyl/3af2b8b54fab1631f40e34fba7da0b8e to your computer and use it in GitHub Desktop.
Save alexstyl/3af2b8b54fab1631f40e34fba7da0b8e 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).
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 MainActivity : 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
}
}
}
}
}
}
}
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))
fun copy(min: Offset = this.min, size: Size = this.size) = ViewBox(min, size)
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
) {
SvgScopeInstance.block()
val (width, height) = size
val (viewBoxWidth, viewBoxHeight) = viewBox.size
val imageVector = ImageVector.Builder(
defaultWidth = width,
defaultHeight = height,
viewportWidth = viewBoxWidth,
viewportHeight = viewBoxHeight,
tintColor = SvgScopeInstance.tint
).apply {
val (minX, minY) = viewBox.min
if (minX != 0F || minY != 0F) {
addGroup(translationX = -minX, translationY = -minY)
}
for ((pathNodes, fill, fillType) in SvgScopeInstance.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) {
SvgPathScopeInstance.block()
val pathNodes = addPathNodes(SvgPathScopeInstance.d.takeUnless(String::isBlank))
_paths += Path(
pathNodes,
SvgPathScopeInstance.fill,
SvgPathScopeInstance.fillType
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment