Search code examples
androidandroid-jetpack-composecoilandroid-jetpack-compose-canvas

Get bitmap before using in a Composable with Coil


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)

Solution

  • 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,
        )
    }