Get bitmap before using in a Composable with Coil
Asked Answered
T

1

2

I'm trying to get two images from an url and then I have a Composable that needs two bitmaps to draw them in a Canvas, I've tried it but the canvas don't get painted am I missing something?

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 overlayImageLoaded = rememberAsyncImagePainter(
            model = overlayImage,
        )
        val baseImageLoaded = rememberAsyncImagePainter(
            model = baseImage
        )

        var overlayBitmap = remember<Bitmap?> {
            null
        }
        var baseBitmap = remember<Bitmap?> {
            null
        }

        val overlayImageLoadedState = overlayImageLoaded.state
        if (overlayImageLoadedState is AsyncImagePainter.State.Success) {
            overlayBitmap = overlayImageLoadedState.result.drawable.toBitmap()
        }

        val baseImageLoadedState = baseImageLoaded.state
        if (baseImageLoadedState is AsyncImagePainter.State.Success) {
            baseBitmap = baseImageLoadedState.result.drawable.toBitmap()
        }

        MyCanvasComposable(baseBitmap, overlayBitmap)
Tradelast answered 5/12, 2022 at 7:30 Comment(1)
How are you using the image in the canvas? – Bless
T
3

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

enter image description here

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,
    )
}
Touber answered 5/12, 2022 at 10:25 Comment(7)
Yes, that works for me thanks Thracian, is there any way to hide the erased image at any point? – Tradelast
sure. create a if with matchPercent over image that being erased. if(matchPercent)>=70){Image(erasedBitmap)} that's it. You can put any flag. As long as it statement is true your Image composable is in composition when if cease to be true it exits composition – Touber
There's sometimes when dragging that the path is not drawn and once you do the onUp it does render the line, you know why? – Tradelast
You should use Modiifer.pointerInputFilter if works fine, i'm not sure. I posted gesture code for demonstration because it was easy to use. If you experience same issue with your gesture code try without comparing pixels. If that's the case invoke that function in another thread – Touber
Let me try it, and the quadratic stuff can ve changed to : val offset = Offset( previousPosition.x, previousPosition.y, ) erasePath.addOval( oval = Rect(offset, 100f) ) to instead of a quadratic draw an oval? – Tradelast
Sure you can change to anything. Anything you draw to that Canvas is applied to erasedImageBitmap – Touber
Let us continue this discussion in chat. – Touber

© 2022 - 2024 β€” McMap. All rights reserved.