I am using Jetpack Compose and I want to create a circle with custom shadow/gradient effects. As far as I know there is no way to create that with composable objects inside DrawScope and I have to use NativeCanvas instead. That works fine for my case, but as I remember when we use View and we write something in the onDraw() method, we SHOULD NOT INITIALIZE NEW OBJECTS there. Since the method is called on each 30/60fps when using animation and creating new objects for each call will lead to poor performance.
Where is the proper place to define those object BlurMaskFilter, RadialGradient, Paint so they could be re-initialized only when the size of the composable is changes?
I was wondering if I should define them as lateinit var outside the function and then use SideEffect, to initialize them?
I forgot to mention that I am using InfiniteTransition, and then using the state to change shapes that are drawn inside the NativeCanvas!
Box(
modifier = Modifier
.size(widthDp, widthDp)
.drawBehind {
drawIntoCanvas { canvas ->
canvas.nativeCanvas.apply {
val blurMask = BlurMaskFilter(
15f,
BlurMaskFilter.Blur.NORMAL
)
val radialGradient = android.graphics.RadialGradient(
100f, 100f, 50f,
intArrayOf(android.graphics.Color.WHITE, android.graphics.Color.BLACK),
floatArrayOf(0f, 0.9f), android.graphics.Shader.TileMode.CLAMP
)
val paint = Paint().asFrameworkPaint().apply {
shader = radialGradient
maskFilter = blurMask
color = android.graphics.Color.WHITE
}
drawCircle(100f, 100f, 50f, paint)
}
}
}
) {
}
There are two ways to keep some objects between recompositions in Compose - using remember or representation models. For this particular case remember is a better fit.
If you have a static size given by Modifier.size(widthDp, widthDp), it is easy to calculate everything in advance:
val density = LocalDensity.current
val paint = remember(widthDp) {
// in case you need to use width in your calculations
val widthPx = with(density) {
widthDp.toPx()
}
val blurMask = BlurMaskFilter(
15f,
BlurMaskFilter.Blur.NORMAL
)
val radialGradient = android.graphics.RadialGradient(
100f, 100f, 50f,
intArrayOf(android.graphics.Color.WHITE, android.graphics.Color.BLACK),
floatArrayOf(0f, 0.9f), android.graphics.Shader.TileMode.CLAMP
)
Paint().asFrameworkPaint().apply {
shader = radialGradient
maskFilter = blurMask
color = android.graphics.Color.WHITE
}
}
If you don't have a static size, for example you want to use Modifier.fillMaxSize, you can use Modifier.onSizeChanged to get the real size and update your Paint - that's why I pass size as key in the remember call - it will recalculate the value when the key changes.
val (size, updateSize) = remember { mutableStateOf<IntSize?>(null) }
val paint = remember(size) {
if (size == null) {
Paint()
} else {
Paint().apply {
// your code
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.onSizeChanged(updateSize)
.drawBehind {
// ...
}
)
While the accepted answer is correct, there is a more nice approach.
Just use modifier drawWithCache. In your case:
Modifier
.drawWithCache {
// setup calculations and paints
onDrawBehind {
// draw
}
}
Don't forget to read the documentation of the drawWithCache to be sure your code matches the conditions to reuse the cache (seems like it does).
Related
I looked in the the auto-complete list to find something, but wasn't successful.
Here is some docs on that: https://developer.android.com/jetpack/compose/graphics/draw/modifiers
Here is a code example:
#Composable
fun Drawing() {
val image = ImageBitmap(30, 30)
Canvas(modifier = Modifier.drawBehind {
drawImage(image, topLeft = Offset(x = 100f, y = 200f))
}) {
// Draw scope
}
}
Or to draw an image in the draw scope, here are some docs and an example:
fun drawImage(
image: ImageBitmap,
topLeft: Offset = Offset.Zero,
...
): Unit
I am trying to add and icon from resource in Canvas DrawScope.
The nearest solution I found is drawImage(), but it doesn't work for my case. Also I cannot use the normal Icon() composable inside the DrawScope. So is there any work around to display an icon inside canvas, similarly to the way we do it with composable:
import androidx.compose.material.Icon
Icon(Icons.Rounded.Menu, contentDescription = "Localized description")
Icons.Rounded.Menu is a VectorImage and you can wrap it into a VectorPainter.
You can use something like:
val painter = rememberVectorPainter(Icons.Rounded.Menu)
Canvas(modifier = Modifier.fillMaxSize()) {
with(painter) {
draw(painter.intrinsicSize)
}
}
Drawscope extension for abstract class Painter has size, alpha and colorFilter params. You can change these params. If you wish to change draw position of Icon, it' drawn top left (0,0) by default you can use translate function or other functions such as rotate or scale for further operations
val painter = rememberVectorPainter(Icons.Rounded.Menu)
Canvas(modifier = Modifier.fillMaxSize()) {
translate(left = 0f, top = 0f) {
with(painter) {
// draw(size = painter.intrinsicSize)
draw(
size = Size(40.dp.toPx(), 40.dp.toPx()),
alpha = 1f,
colorFilter = ColorFilter.tint(Color.Red)
)
}
}
}
I need to draw text onto Canvas in Compose, for this purpose I need a TextPaint with android.graphics.Typeface.
Is there a way to easily convert Compose TextStyle to a android.graphics.Typeface?
You can resolve android.graphics.Typeface object from a androidx.compose.ui.text.TextStyle object using LocalFontFamilyResolver.
val style: TextStyle = MaterialTheme.typography.body1
val resolver: FontFamily.Resolver = LocalFontFamilyResolver.current
val typeface: Typeface = remember(resolver, style) {
resolver.resolve(
fontFamily = style.fontFamily,
fontWeight = style.fontWeight ?: FontWeight.Normal,
fontStyle = style.fontStyle ?: FontStyle.Normal,
fontSynthesis = style.fontSynthesis ?: FontSynthesis.All,
)
}.value as Typeface
Currently the only workaround that i found is to provide with resources:
val textTypeface: android.graphics.Typeface? =
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) LocalContext.current.resources.getFont(R.font.quicksand_light) else null
However if android version < android oreo, idk how to provide the font, so i fallback to default fount.
try this
val textPaint = TextPaint()
val context = LocalContext.current
Canvas(modifier = Modifier.fillMaxWidth()) {
textPaint.apply {
typeface = Typeface.createFromAsset(context.assets, "fonts/yourfont.ttf")
...
}
drawContext.canvas.nativeCanvas.drawText("test", yourStart, yourEnd, X, Y, textPaint)
}
I ran into the same issue as you: Wanting to draw text to a Canvas in a TextStyle that is defined in my Compose Theme.
I found out you can use androidx.compose.ui.text.Paragraph.paint(canvas) to draw text to the Canvas.
That in itself has no way to set offsets for the paint job, so you can use drawContext.canvas.nativeCanvas.withTranslation() to move the drawing to a specified offset.
val paragraph = Paragraph(
text = "Hello",
style = MaterialTheme.typography.titleLarge,
constraints = Constraints(),
density = LocalDensity.current,
fontFamilyResolver = LocalFontFamilyResolver.current,
)
val colorOnSurface = MaterialTheme.colorScheme.onSurface
Canvas(
modifier = Modifier.fillMaxSize()
) {
//
drawContext.canvas.nativeCanvas.withTranslation(
100f,
100f
) {
paragraph.paint(
canvas = drawContext.canvas,
color = colorOnSurface,
)
}
}
This seems quite sketchy, but it's the best I've got ¯_(ツ)_/¯.
To draw text to canvas, you can do like this
#Composable
fun DrawText() {
val paint = Paint().asFrameworkPaint()
Canvas(modifier = Modifier.fillMaxSize()) {
paint.apply {
isAntiAlias = true
textSize = 24f
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) // your typeface
//other methods like color, dither, fontMetrics, shadow etc...are also available
}
drawIntoCanvas {
it.nativeCanvas.drawText("Hello World", size.width/2, size.height/2, paint)
}
}
}
I think to convert TextStyle(compose library) to typeface will be a pain since no support from android, If you want to draw text to canvas I think this will be enough
I am drawing a custom shape for a topbar in jetpack compose. I want to draw a shadow for the path.
val topBarShapePath = Path().apply {
moveTo(dpToPixels(leftPadding), 0f)
lineTo(dpToPixels(leftPadding), dpToPixels(dpValue = 110.dp))
arcTo(
Rect(
dpToPixels(leftPadding),
dpToPixels(dpValue = 110.dp),
dpToPixels(dpValue = 32.dp),
dpToPixels(dpValue = 135.dp)
), -180f, -90f, true)
lineTo(
dpToPixels(dpValue = triangleStartX),
dpToPixels(dpValue = rectHeight))
lineTo(
dpToPixels(dpValue = screenWidth),
dpToPixels(dpValue = triangleEndY)
)
lineTo(dpToPixels(dpValue = screenWidth), 0f)
lineTo(dpToPixels(dpValue = leftPadding), 0f)
}
Column(
modifier = Modifier
.fillMaxWidth()
.height(400.dp)
.drawBehind {
val finalWidth = 40.dp.toPx()
drawPath(
topBarShapePath,
color = topbarcolor)
drawOutline(
outline = Outline.Generic(
topBarShapePath),
brush = Brush.horizontalGradient(),
style = Stroke(
width = 1.dp.toPx(),
)
)
}
)
This is the code I am using to draw the shape, the "drawOutline" was to try and draw a shadow for the path, but I can't figure out how to blur the line.
Any help appreciated.
Here is a screenshot of the result I am looking for:
It's impossible to draw shadow in Canvas at the moment, but you can do it with Modifier.shadow, specifying the needed custom shape, like this:
class TopBarShape(/*some parameters*/): Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
) = Outline.Generic(Path().apply {
// your path code
})
}
Modifier.shadow(elevation = 10.dp, shape = TopBarShape(/*your parameters*/))
Sadly this modifier doesn't allow much modifications, it's one of the most starred Compose issues, so hopefully it'll change in future, but as it's not in the latest 1.1-beta I wouldn't expect it at least until 1.2.
If you still think that drawing shadow manually is a needed feature, you can create a feature request.
Based on the images and PorterDuffModes in this page
I downloaded images, initially even though they are png they had light and dark gray rectangles which were not transparent and removed them.
And checked out using this sample code, replacing drawables with the ones in original code with the ones below and i get result
As it seem it works as it should with Android View, but when i use Jetpack Canvas as
androidx.compose.foundation.Canvas(modifier = Modifier.size(500.dp),
onDraw = {
drawImage(imageBitmapDst)
drawImage(imageBitmapSrc, blendMode = BlendMode.SrcIn)
})
BlendMode.SrcIn draws blue rectangle over black rectangle, other modes do not return correct results either. BlendMode.SrcOut returns black screen.
And using 2 Images stacked on top of each other with Box
val imageBitmapSrc: ImageBitmap = imageResource(id = R.drawable.c_src)
val imageBitmapDst: ImageBitmap = imageResource(id = R.drawable.c_dst)
Box {
Image(bitmap = imageBitmapSrc)
Image(
bitmap = imageBitmapDst,
colorFilter = ColorFilter(color = Color.Unspecified, blendMode = BlendMode.SrcOut)
)
}
Only blue src rectangle is visible.
Also tried with Painter, and couldn't able to make it work either
val imageBitmapSrc: ImageBitmap = imageResource(id = R.drawable.c_src)
val imageBitmapDst: ImageBitmap = imageResource(id = R.drawable.c_dst)
val blendPainter = remember {
object : Painter() {
override val intrinsicSize: Size
get() = Size(imageBitmapSrc.width.toFloat(), imageBitmapSrc.height.toFloat())
override fun DrawScope.onDraw() {
drawImage(imageBitmapDst, blendMode = BlendMode.SrcOut)
drawImage(imageBitmapSrc)
}
}
}
Image(blendPainter)
How should Blend or PorterDuff mode be used with Jetpack Compose?
Easiest way to solve issue is to add
.graphicsLayer(alpha = 0.99f) to Modifier to make sure an offscreen buffer
#Composable
fun DrawWithBlendMode() {
val imageBitmapSrc = ImageBitmap.imageResource(
LocalContext.current.resources,
R.drawable.composite_src
)
val imageBitmapDst = ImageBitmap.imageResource(
LocalContext.current.resources,
R.drawable.composite_dst
)
Canvas(
modifier = Modifier
.fillMaxSize()
// Provide a slight opacity to for compositing into an
// offscreen buffer to ensure blend modes are applied to empty pixel information
// By default any alpha != 1.0f will use a compositing layer by default
.graphicsLayer(alpha = 0.99f)
) {
val dimension = (size.height.coerceAtMost(size.width) / 2f).toInt()
drawImage(
image = imageBitmapDst,
dstSize = IntSize(dimension, dimension)
)
drawImage(
image = imageBitmapSrc,
dstSize = IntSize(dimension, dimension),
blendMode = BlendMode.SrcOut
)
}
}
Result
Or adding a layer in Canvas does the trick
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawImage(
image = dstImage,
srcSize = IntSize(canvasWidth / 2, canvasHeight / 2),
dstSize = IntSize(canvasWidth, canvasHeight),
)
// Source
drawImage(
image = srcImage,
srcSize = IntSize(canvasWidth / 2, canvasHeight / 2),
dstSize = IntSize(canvasWidth, canvasHeight),
blendMode = blendMode
)
restoreToCount(checkPoint)
}
I created some tutorials for applying blend modes here
I was really frustrated for a whole week with similar problem, however your question helped me find the solution how to make it work.
EDIT1
I'm using compose 1.0.0
In my case I'm using something like double buffering instead of drawing directly on canva - just as a workaround.
Canvas(modifier = Modifier.fillMaxWidth().fillMaxHeight()) {
// First I create bitmap with real canva size
val bitmap = ImageBitmap(size.width.toInt(), size.height.toInt())
// here I'm creating canvas of my bitmap
Canvas(bitmap).apply {
// here I'm driving on canvas
}
// here I'm drawing my buffered image
drawImage(bitmap)
}
Inside Canvas(bitmap) I'm using drawPath, drawText, etc with paint:
val colorPaint = Paint().apply {
color = Color.Red
blendMode = BlendMode.SrcAtop
}
And in this way BlendMode works correctly - I've tried many of modes and everything worked as expected.
I don't know why this isn't working directly on canvas of Composable, but my workaround works fine for me.
EDIT2
After investigating Image's Painter's source code i saw that Android team also use alpha trick either to decide to create a layer or not
In Painter
private fun configureAlpha(alpha: Float) {
if (this.alpha != alpha) {
val consumed = applyAlpha(alpha)
if (!consumed) {
if (alpha == DefaultAlpha) {
// Only update the paint parameter if we had it allocated before
layerPaint?.alpha = alpha
useLayer = false
} else {
obtainPaint().alpha = alpha
useLayer = true
}
}
this.alpha = alpha
}
}
And applies here
fun DrawScope.draw(
size: Size,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
) {
configureAlpha(alpha)
configureColorFilter(colorFilter)
configureLayoutDirection(layoutDirection)
// b/156512437 to expose saveLayer on DrawScope
inset(
left = 0.0f,
top = 0.0f,
right = this.size.width - size.width,
bottom = this.size.height - size.height
) {
if (alpha > 0.0f && size.width > 0 && size.height > 0) {
if (useLayer) {
val layerRect = Rect(Offset.Zero, Size(size.width, size.height))
// TODO (b/154550724) njawad replace with RenderNode/Layer API usage
drawIntoCanvas { canvas ->
canvas.withSaveLayer(layerRect, obtainPaint()) {
onDraw()
}
}
} else {
onDraw()
}
}
}
}
}