You should assign size for painter to be created, otherwise it returns error
val overlayPainter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(overlayImage)
.size(coil.size.Size.ORIGINAL) // Set the target size to load the image at.
.build()
)
val basePainter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(baseImage)
.size(coil.size.Size.ORIGINAL) // Set the target size to load the image at.
.build()
)
Result
You can send both of the ImageBitmaps when both states are success with
@Composable
private fun MyComposable() {
val overlayImage =
"https://st2.depositphotos.com/1400069/5999/i/600/depositphotos_59995765-stock-photo-abstract-galaxy-background.jpg"
val baseImage =
"https://www.vitrinesdocomercio.com/uploads/1/3/9/4/13943900/1278180_orig.jpg"
val overlayPainter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(overlayImage)
.size(coil.size.Size.ORIGINAL) // Set the target size to load the image at.
.build()
)
val basePainter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(baseImage)
.size(coil.size.Size.ORIGINAL) // Set the target size to load the image at.
.build()
)
val overlayImageLoadedState = overlayPainter.state
val baseImageLoadedState = basePainter.state
if (
baseImageLoadedState is AsyncImagePainter.State.Success &&
overlayImageLoadedState is AsyncImagePainter.State.Success
) {
SideEffect {
println("π₯ COMPOSING...")
}
val baseImageBitmap =
baseImageLoadedState.result.drawable.toBitmap()
.asImageBitmap()
val overlayImageBitmap =
overlayImageLoadedState.result.drawable
.toBitmap()
.asImageBitmap()
EraseBitmapSample(
baseImageBitmap = baseImageBitmap,
overlayImageBitmap = overlayImageBitmap,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(4 / 3f)
)
}
}
And what you wish to achieve
@Composable
fun EraseBitmapSample(
overlayImageBitmap: ImageBitmap,
baseImageBitmap: ImageBitmap,
modifier: Modifier
) {
var matchPercent by remember {
mutableStateOf(100f)
}
BoxWithConstraints(modifier) {
// Path used for erasing. In this example erasing is faked by drawing with canvas color
// above draw path.
val erasePath = remember { Path() }
var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
// This is our motion event we get from touch motion
var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
// This is previous motion event before next touch is saved into this current position
var previousPosition by remember { mutableStateOf(Offset.Unspecified) }
val imageWidth = constraints.maxWidth
val imageHeight = constraints.maxHeight
val drawImageBitmap = remember {
Bitmap.createScaledBitmap(
overlayImageBitmap.asAndroidBitmap(),
imageWidth,
imageHeight,
false
)
.asImageBitmap()
}
// Pixels of scaled bitmap, we scale it to composable size because we will erase
// from Composable on screen
val originalPixels: IntArray = remember {
val buffer = IntArray(imageWidth * imageHeight)
drawImageBitmap
.readPixels(
buffer = buffer,
startX = 0,
startY = 0,
width = imageWidth,
height = imageHeight
)
buffer
}
val erasedBitmap: ImageBitmap = remember {
Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888).asImageBitmap()
}
val canvas: Canvas = remember {
Canvas(erasedBitmap)
}
val paint = remember {
Paint()
}
val erasePaint = remember {
Paint().apply {
blendMode = BlendMode.Clear
this.style = PaintingStyle.Stroke
strokeWidth = 30f
}
}
canvas.apply {
val nativeCanvas = this.nativeCanvas
val canvasWidth = nativeCanvas.width.toFloat()
val canvasHeight = nativeCanvas.height.toFloat()
when (motionEvent) {
MotionEvent.Down -> {
erasePath.moveTo(currentPosition.x, currentPosition.y)
previousPosition = currentPosition
}
MotionEvent.Move -> {
erasePath.quadraticBezierTo(
previousPosition.x,
previousPosition.y,
(previousPosition.x + currentPosition.x) / 2,
(previousPosition.y + currentPosition.y) / 2
)
previousPosition = currentPosition
}
MotionEvent.Up -> {
erasePath.lineTo(currentPosition.x, currentPosition.y)
currentPosition = Offset.Unspecified
previousPosition = currentPosition
motionEvent = MotionEvent.Idle
matchPercent = compareBitmaps(
originalPixels,
erasedBitmap,
imageWidth,
imageHeight
)
}
else -> Unit
}
with(canvas.nativeCanvas) {
drawColor(android.graphics.Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
drawImageRect(
image = drawImageBitmap,
dstSize = IntSize(canvasWidth.toInt(), canvasHeight.toInt()),
paint = paint
)
drawPath(
path = erasePath,
paint = erasePaint
)
}
}
val canvasModifier = Modifier.pointerMotionEvents(
Unit,
onDown = { pointerInputChange ->
motionEvent = MotionEvent.Down
currentPosition = pointerInputChange.position
pointerInputChange.consume()
},
onMove = { pointerInputChange ->
motionEvent = MotionEvent.Move
currentPosition = pointerInputChange.position
pointerInputChange.consume()
},
onUp = { pointerInputChange ->
motionEvent = MotionEvent.Up
pointerInputChange.consume()
},
delayAfterDownInMillis = 20
)
Image(
bitmap = baseImageBitmap,
contentDescription = null
)
Image(
modifier = canvasModifier
.clipToBounds()
.matchParentSize()
.border(2.dp, Color.Green),
bitmap = erasedBitmap,
contentDescription = null,
contentScale = ContentScale.FillBounds
)
}
Text(
text = "Bitmap match ${matchPercent}%",
color = Color.Red,
fontSize = 22.sp,
)
}