I'm attempting to apply blend mode to two shapes within Jetpack compose's canvas. Based on this blog I know roughly what the expected output should look like though I am not getting similar results.
For example, with the following simple Box + Canvas with two shapes, with the blend mode SrcIn
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(290.dp)
) {
val sizeInPx = with(LocalDensity.current) { 150.dp.toPx() }
Canvas(
modifier = Modifier.fillMaxSize()
) {
drawCircle(
color = Color.Red,
radius = sizeInPx,
)
drawRect(
color = Color.Blue,
size = Size(sizeInPx, sizeInPx),
blendMode = BlendMode.SrcIn
)
}
}
I would expect a red circle, and a blue square clipped to the shape of the red circle. Yet the output UI is as if no blend mode has been added at all
What am I doing wrong?
Changing alpha less then 1f creates a layer as buffer that's why it works. Other way of achieving this is to use layer directly if you don't want to change alpha. You can see my answere about it here
Canvas(modifier = canvasModifier) {
val canvasWidth = size.width.roundToInt()
val canvasHeight = size.height.roundToInt()
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
drawCircle(
color = Color.Red,
radius = sizeInPx,
)
drawRect(
color = Color.Blue,
size = Size(sizeInPx, sizeInPx),
blendMode = BlendMode.SrcIn
)
restoreToCount(checkPoint)
}
}
In painter code Android team uses it as
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 check alpha to apply layer
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()
}
}
}
}
}
Related
I'm building an image cropper. I'm using rectangle to draw dynamic overlay. When overlay is out of image bounds i move it back to image bounds when pointer is up.
What i build
open var overlayRect: Rect =
Rect(offset = Offset.Zero, size = Size(size.width.toFloat(), size.height.toFloat()))
and i get final position using this function to move back to valid bounds
internal fun moveIntoBounds(rectBounds: Rect, rectCurrent: Rect): Rect {
var width = rectCurrent.width
var height = rectCurrent.height
if (width > rectBounds.width) {
width = rectBounds.width
}
if (height > rectBounds.height) {
height = rectBounds.height
}
var rect = Rect(offset = rectCurrent.topLeft, size = Size(width, height))
if (rect.left < rectBounds.left) {
rect = rect.translate(rectBounds.left - rect.left, 0f)
}
if (rect.top < rectBounds.top) {
rect = rect.translate(0f, rectBounds.top - rect.top)
}
if (rect.right > rectBounds.right) {
rect = rect.translate(rectBounds.right - rect.right, 0f)
}
if (rect.bottom > rectBounds.bottom) {
rect = rect.translate(0f, rectBounds.bottom - rect.bottom)
}
return rect
}
And set it on pointer up as
override fun onUp(change: PointerInputChange) {
touchRegion = TouchRegion.None
overlayRect = moveIntoBounds(rectBounds, overlayRect)
// Calculate crop rectangle
cropRect = calculateRectBounds()
rectTemp = overlayRect.copy()
}
How can i animate this rect to valid bounds? Is there way to use Animatable to animate a rect?
I checked official document for animation and suggestion is using transition and
transition.animateRect from one state to another but i don't have states i want to animate to a dynamic target from current dynamic value and this is a non-Composable class called DynamicCropState that extends a class like zoom state here. Need to animate using Animatable or non-Composable apis.
I solved this creating an AnimationVector4D that converts between Float and Rect by
val RectToVector = TwoWayConverter(
convertToVector = { rect: Rect ->
AnimationVector4D(rect.left, rect.top, rect.width, rect.height)
},
convertFromVector = { vector: AnimationVector4D ->
Rect(
offset = Offset(vector.v1, vector.v2),
size = Size(vector.v3, vector.v4)
)
}
)
For demonstration, created a class to animate internally and return current value of Rect
class RectWrapper {
private val animatableRect = Animatable(
Rect(
offset = Offset.Zero,
size = Size(300f, 300f)
),
RectToVector
)
val rect: Rect
get() = animatableRect.value
suspend fun animateRectTo(rect: Rect) {
animatableRect.animateTo(rect)
}
}
And a demonstration to show how to use it
#Composable
private fun AnimateRectWithAnimatable() {
val coroutineScope = rememberCoroutineScope()
val rectWrapper = remember {
RectWrapper()
}
Column(modifier = Modifier.fillMaxSize()) {
Button(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
onClick = {
coroutineScope.launch {
rectWrapper.animateRectTo(
Rect(
topLeft = Offset(200f, 200f),
bottomRight = Offset(800f, 800f)
)
)
}
}
) {
Text("Animate")
}
Canvas(
modifier = Modifier
.fillMaxSize()
) {
drawRect(
color = Color.Red,
topLeft = rectWrapper.rect.topLeft,
size = rectWrapper.rect.size
)
}
}
}
If you wish to animate a Rect from a class you can implement it as above. I generally pass these classes to modifiers as State and observe and trigger changes inside Modifier.composed and return result to any class that uses that modifier.
I have a BLURRED background Bitmap and a horizontal scrollable row. What I want to achieve is that the background image must be visible only in the row item area...
I've tried to apply DST_IN with blendMode but no success. Any ideas how to do it in jetpack compose?
What I'm trying to achieve is this
I want to have a blurred version of the background Image only visible though the row items, so that it will seem like a gloss mirror background on row items when scrolled
You either need to set alpha something less than 1f
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.DstIn
)
}
}
or use a layer inside canvas
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.DstIn
)
restoreToCount(checkPoint)
}
You can check this tutorial for more about Blend modes.
Setting alpha less than 1f might look like a hack but Image's Painter source code uses it too to decide whether to create layer or not
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 creates a layer as for Image
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()
}
}
}
}
}
Is there a way to make a box with a wavy top with Canvas?
I would like to know if this effect can be achieved directly with a Canvas, it is not necessary to have a scrolling animation.
It's not quite clear why you're talking about Canvas. To crop a view like this, you can use a custom Shape and apply it to your view with Modifier.clip. Here's a shape you can use:
class WavyShape(
private val period: Dp,
private val amplitude: Dp,
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
) = Outline.Generic(Path().apply {
val wavyPath = Path().apply {
val halfPeriod = with(density) { period.toPx() } / 2
val amplitude = with(density) { amplitude.toPx() }
moveTo(x = -halfPeriod / 2, y = amplitude)
repeat(ceil(size.width / halfPeriod + 1).toInt()) { i ->
relativeQuadraticBezierTo(
dx1 = halfPeriod / 2,
dy1 = 2 * amplitude * (if (i % 2 == 0) 1 else -1),
dx2 = halfPeriod,
dy2 = 0f,
)
}
lineTo(size.width, size.height)
lineTo(0f, size.height)
}
val boundsPath = Path().apply {
addRect(Rect(offset = Offset.Zero, size = size))
}
op(wavyPath, boundsPath, PathOperation.Intersect)
})
}
If you really need to use this inside Canvas for some reason, you can pass the same Path that I create inside WavyShape to DrawScope.clipPath, so that the contents of the clipPath block will be clipped.
Apply custom shape to your Image or any other view:
Image(
painter = painterResource(id = R.drawable.my_image_1),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.clip(WavyShape(period = 100.dp, amplitude = 50.dp))
)
Result:
I'm trying to create a rounded triangle using Canvas in Jetpack Compose.
I try this code for drawing triangle:
#Composable
fun RoundedTriangle() {
Canvas(modifier = Modifier.size(500.dp)) {
val trianglePath = Path().apply {
val height = size.height
val width = size.width
moveTo(width / 2.0f, 0f)
lineTo(width, height)
lineTo(0f, height)
}
drawPath(trianglePath, color = Color.Blue)
}
}
But I don't know how to round the triangle corners. I also tried to use arcTo, but I was unable to get a suitable result.
How can I draw something like the figure below?
For Stroke you can specify rounding like this:
drawPath(
...
style = Stroke(
width = 2.dp.toPx(),
pathEffect = PathEffect.cornerPathEffect(4.dp.toPx())
)
)
Yet Fill seems lack of support rounding. I've created a feature request, please star it.
But Canvas has drawOutline function, which accepts both Outline, which can wrap a Path, and Paint, for which you can specify pathEffect:
Canvas(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) {
val rect = Rect(Offset.Zero, size)
val trianglePath = Path().apply {
moveTo(rect.topCenter)
lineTo(rect.bottomRight)
lineTo(rect.bottomLeft)
close()
}
drawIntoCanvas { canvas ->
canvas.drawOutline(
outline = Outline.Generic(trianglePath),
paint = Paint().apply {
color = Color.Black
pathEffect = PathEffect.cornerPathEffect(rect.maxDimension / 3)
}
)
}
}
Path helpers:
fun Path.moveTo(offset: Offset) = moveTo(offset.x, offset.y)
fun Path.lineTo(offset: Offset) = lineTo(offset.x, offset.y)
Result:
Based on #philip-dukhov answer, if anyone is interested in appliying this to a square
#Composable
fun SquirclePath(
modifier: Modifier,
smoothingFactor: Int = 60,
color: Color,
strokeWidth: Float,
) {
Canvas(
modifier = modifier
) {
val rect = Rect(Offset.Zero, size)
val percent = smoothingFactor.percentOf(rect.minDimension)
val squirclePath = Path().apply {
with(rect) {
lineTo(topRight)
lineTo(bottomRight)
lineTo(bottomLeft)
lineTo(topLeft)
// this is where the path is finally linked together
close()
}
}
drawIntoCanvas { canvas ->
canvas.drawOutline(
outline = Outline.Generic(squirclePath),
paint = Paint().apply {
this.color = color
this.style = PaintingStyle.Fill
this.strokeWidth = strokeWidth
pathEffect = PathEffect.cornerPathEffect(percent)
}
)
}
}
}
fun Int.percentOf(target:Float) = (this.toFloat() / 100) * target
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()
}
}
}
}
}