Reputation: 1051
I have a canvas where I draw two images of the same size, and I've implemented a touch listener where I "erase" one of them and I'd like to know if there's any possibility to know the % of visibility of the one that I'm "erasing".
val overlayImageLoaded = rememberAsyncImagePainter(
model = overlayImage,
)
val baseImageLoaded = rememberAsyncImagePainter(
model = baseImage
)
Canvas(modifier = modifier
.size(220.dp)
.clip(RoundedCornerShape(size = 16.dp))
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
currentPath.moveTo(it.x, it.y)
}
MotionEvent.ACTION_MOVE -> {
onMovedOffset(it.x, it.y)
}
}
true
}) {
with(overlayImageLoaded) {
draw(size = Size(size.width, size.height))
}
movedOffset?.let {
currentPath.addOval(oval = Rect(it, currentPathThickness))
}
clipPath(path = currentPath, clipOp = ClipOp.Intersect) {
with(baseImageLoaded) {
draw(size = Size(size.width, size.height))
}
}
}
I have some ideas :
Since what I want is to know if the image have been erased at least 70% let's say I've thought about store the onMovedOffset
into a list and since I know the size of my canvas and the path thickness I can do a calculus of what user have seen, but perhaps it's a bit overkill.
Also I've thought about getting the canvas draw as a bitmap every-time user moves and then have a method that compares bitmap with bitmap and check the % of equality.
The goal is to know wether the user have erased at least 70% of the image and it's not visible anymore.
Upvotes: 3
Views: 1078
Reputation: 67209
Answer of this question is quite complex and involves many layers. There can be some optimization for threading to comparing pixels in background thread.
Step one create copy of original ImageBitmap to fit in Composable on screen. Since we erase pixels on screen we should use sc
// 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)
Bitmap.createScaledBitmap(imageBitmap.asAndroidBitmap(), imageWidth, imageHeight, false)
.asImageBitmap()
.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()
}
Second step is to create a androidx.compose.ui.graphics.Canvas(imageBitmap)
to apply changes on imageBitmap as in this answer. Check this out to be familiar with how to manipulate Bitmap that is drawn to empty Bitmap
val canvas: Canvas = remember {
Canvas(erasedBitmap)
}
Third step create paints to erase from Bitmap
val paint = remember {
Paint()
}
val erasePaint = remember {
Paint().apply {
blendMode = BlendMode.Clear
this.style = PaintingStyle.Stroke
strokeWidth = 30f
}
}
Forth step is to erase from Bitmap with gesture and calculate difference of original pixels with currently erased bitmap
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 = imageBitmap,
dstSize = IntSize(canvasWidth.toInt(), canvasHeight.toInt()),
paint = paint
)
drawPath(
path = erasePath,
paint = erasePaint
)
}
}
Fifth step is to compare original pixels with erased bitmap to determine in which percent they match
private fun compareBitmaps(
originalPixels: IntArray,
erasedBitmap: ImageBitmap,
imageWidth: Int,
imageHeight: Int
): Float {
var match = 0f
val size = imageWidth * imageHeight
val erasedBitmapPixels = IntArray(size)
erasedBitmap.readPixels(
buffer = erasedBitmapPixels,
startX = 0,
startY = 0,
width = imageWidth,
height = imageHeight
)
erasedBitmapPixels.forEachIndexed { index, pixel: Int ->
if (originalPixels[index] == pixel) {
match++
}
}
return 100f * match / size
}
Full Implementation
@Composable
fun EraseBitmapSample(imageBitmap: 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(imageBitmap.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(
modifier = canvasModifier
.clipToBounds()
.drawBehind {
val width = this.size.width
val height = this.size.height
val checkerWidth = 10.dp.toPx()
val checkerHeight = 10.dp.toPx()
val horizontalSteps = (width / checkerWidth).toInt()
val verticalSteps = (height / checkerHeight).toInt()
for (y in 0..verticalSteps) {
for (x in 0..horizontalSteps) {
val isGrayTile = ((x + y) % 2 == 1)
drawRect(
color = if (isGrayTile) Color.LightGray else Color.White,
topLeft = Offset(x * checkerWidth, y * checkerHeight),
size = Size(checkerWidth, checkerHeight)
)
}
}
}
.matchParentSize()
.border(2.dp, Color.Green),
bitmap = erasedBitmap,
contentDescription = null,
contentScale = ContentScale.FillBounds
)
}
Text("Original Bitmap")
Image(
modifier = modifier,
bitmap = imageBitmap,
contentDescription = null,
contentScale = ContentScale.FillBounds
)
Text("Bitmap match $matchPercent", color = Color.Red, fontSize = 22.sp)
}
Result
Upvotes: 4