Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@amarland
Last active June 3, 2023 17:38
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save amarland/8b10acde7ae2a6a9107a8c9ddbcc66e9 to your computer and use it in GitHub Desktop.
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).
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)
}
}
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
}
}
}
}
}
}
}
@amarland
Copy link
Author

Screenshot_20230519_042532

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment