Doing some canvas drawing and I need a vector imag...
# compose-android
c
Doing some canvas drawing and I need a vector image. Docs show how to use an image but it doesn't seem to work with vector drawables. Is there some simple api I'm missing here? https://developer.android.com/jetpack/compose/graphics/draw/overview#draw-image
e
if you have a
Painter
,
Copy code
with(painter) { draw(size) }
inside your
DrawScope
will draw it. if you have an
ImageVector
, convert it to a
Painter
with
rememberVectorPainter()
m
painterResource
works with vector drawables and you can draw it within a canvas using Painter#draw :
Copy code
val vector = painterResource(id = R.drawable.example)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    with(vector) {
        draw(size)
    }
})
Ah, @ephemient beat me to it 😅
e
but since a vector drawable is really just translated to a series of canvas commands, you could possibly do that at build time (similar to how material icons does)
@MR3Y by mere seconds, apparently blob sweat smile
c
Interesting! Threw me off that you would do it that way vs some premade method like drawImage. will give this a go. many thanks
e
drawing images is one of canvas's primitive operations while vector drawables are really more like a serialized
DrawScope.() -> Unit
function
c
Cool that worked. for some reason draw() doesn't have a blendMode param, but thats for another day
e
well yeah, it's applying to arbitrary calls so it requires more heavy lifting
Copy code
drawContext.canvas.saveLayer(Rect(Offset.Zero, size), Paint().apply { blendMode = ... })
draw()
drawContext.canvas.restore()
c
wait what?
lol
e
drawing a single image with blendmode, sure that makes sense for a primitive operation
draw() is any number of calls, and the only way to apply blendmode to that is to render to another layer and then blend it down
c
what docs you looking at for this by chance?
first time ive seen those kind of saveLayer and restore calls lol
c
Thank you 🙏
this has the effect i want, and so I'm basically trying to recreate it with an svg. drawCircle( color = Color.Blue, blendMode = BlendMode.Clear )
_with_(painter)*{* drawContext.canvas.saveLayer(_Rect_(Offset.Zero, size), _Paint_().apply { blendMode = BlendMode.Clear }) _draw_(painter.intrinsicSize) drawContext.canvas.restore() } did not work, so will try to debug from here
e
I think it would be more efficient to do that with a clip path rather than an SVG layer, but I guess it could depend on the shape
c
is it somehow possible to go from an svg to a path?
e
in principle yes, but I don't think there's anything built for that
c
bah
surprised hoenstly why something like this didn't work _draw_( painter.intrinsicSize, colorFilter = ColorFilter.tint(Color.Blue, BlendMode.Clear) )
Ended up drawing the vector as a bitmap and using drawImage instead. Some stuff doesn't look as perfect as it could (i think) if it was a vector, esp since i scale the vector up, but I'll circle back to it later.
e
I think you're just using the wrong blend mode
ugly colors but just for demonstration purposes
Copy code
@Preview(showBackground = true)
@Composable
private fun Example() {
    val painter = rememberVectorPainter(Icons.Default.Refresh)
    Canvas(
        modifier = Modifier
            .size(96.dp)
            .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen),
    ) {
        drawCircle(Brush.linearGradient(listOf(Color.Red, Color.Blue)))
        drawContext.canvas.saveLayer(Rect(Offset.Zero, size), Paint().apply { blendMode = BlendMode.DstOut })
        with(painter) { draw(size) }
        drawContext.canvas.restore()
    }
}
the
compositingStrategy
is to prevent it from punching a hole through all the parents
and if you turn the vector into a clip path, you don't need saveLayer or compositingStrategy (note that this isn't complete enough to handle all vectors, but does produce the same output as above for the material icons)
Copy code
@Preview(showBackground = true)
@Composable
private fun Example() {
    val icon = Icons.Default.Refresh
    Canvas(modifier = Modifier.size(96.dp)) {
        val parser = PathParser()
        fun VectorNode.parseTo(path: Path) {
            when (this) {
                is VectorGroup -> {
                    // doesn't handle rotation/scale/translation/clip
                    for (child in this) child.parseTo(path)
                }
                is VectorPath -> {
                    // doesn't handle stroke/fill
                    parser.clear()
                    parser.addPathNodes(pathData).toPath(path)
                }
            }
        }
        val vectorPath = Path().also { icon.root.parseTo(it) }
        scale(size.width / icon.viewportWidth, size.height / icon.viewportHeight, Offset.Zero) {
            val mask = Path.combine(
                PathOperation.Difference,
                Path().apply { addRect(Rect(0f, 0f, icon.viewportWidth, icon.viewportHeight)) },
                vectorPath,
            )
            clipPath(mask) {
                scale(icon.viewportWidth / size.width, icon.viewportHeight / size.height, Offset.Zero) {
                    drawCircle(Brush.linearGradient(listOf(Color.Red, Color.Blue)))
                }
            }
        }
    }
}
c
oh snap! that worked! I guess I was too focussed on the Clear blend mode strategy since I wanted that clipping behavior.
Don't know if im an idiot. but can't figure out how to draw the vector in the center. tried translate {} and just a barebones
drawContext.canvas.saveLayer(_Rect_(center, mySize), paint) but it keeps drawing in the top left
e
Copy code
translate(dx, dy) { ... }
or
Copy code
inset(dx, dy) { ... }
depending on what you want
c
I got it working but think i ended up taking some size and dividing by half. Maybe center.x/2 and center.y/2. but yeah. everything is looking pretty smooth ow. thanks @ephemient!
448 Views