-
-
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).
This file contains 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 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 | |
} | |
} | |
} | |
} | |
} | |
} | |
} |
This file contains 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)) | |
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