coil-kt / coil

Image loading for Android and Compose Multiplatform.
https://coil-kt.github.io/coil/
Apache License 2.0
10.8k stars 661 forks source link

rememberAsyncImagePainter renders nothing when using dragAndDropSource #2150

Open vjxDev opened 8 months ago

vjxDev commented 8 months ago

Describe the bug The image is simply rendering nothing. There is nothing in the logs. When I remove the dragAndDropSource it works fine.

To Reproduce

                Box(
                    modifier = Modifier
                        .aspectRatio(1f)
                        .dragAndDropSource {
                            detectTapGestures (
                                onLongPress = {
                                    startTransfer(
                                        DragAndDropTransferData(
                                            clipData = ClipData.newPlainText("text/","${photo.id}"),
                                            flags = View.DRAG_FLAG_GLOBAL,
                                        )
                                    )
                                }
                            )
                        }
                ) {
                    Image(
                        painter = rememberAsyncImagePainter(photo.url),
                        contentDescription = photo.description,
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.fillMaxSize()
                    )
                    Text(text = "${photo.id}")
                }

Logs/Screenshots Screenshot_20240229-013851 Screenshot_20240229-013928

Version I am using a google pixel 6, android 14. coil-compose = "2.6.0" activity-compose = "1.7.0"

colinrtwhite commented 8 months ago

If you use AsyncImage does it fix it? rememberAsyncImagePainter isn't easily able to determine the output bounds which can cause issues.

vjxDev commented 8 months ago

I just tried it with AsyncImage . Its blank until I long press on a box, only then does it render the images. I only now noticed it does the same thing with rememberAsyncImagePainter.

It seams like it only renders the images when a recomposition happens.

https://github.com/coil-kt/coil/assets/60753340/aca3fb98-0696-4cff-95bc-b9f685412763

colinrtwhite commented 8 months ago

Interesting - if it also happens with AsyncImage then it might not be related to the constraints. Just to confirm if you set a fixed size on the image request (e.g. size(Size(500, 500))) does it work-around the issue?

vjxDev commented 8 months ago

Its still blank after adding modifier size until the 1st state update.

AsyncImage(
  model = photo.url,
  contentDescription = photo.description,
  contentScale = ContentScale.Crop,
   modifier = Modifier.size(20.dp, 20.dp),
)

The dragAndDropSource and dragAndDropTarget are under ExperimentalFoundationApi they might be the problem.

TinyHai commented 7 months ago

I reproduce this issue by applying Modifier#drawWithCache to AsyncImage

Modifier.drawWithCache {
    val picture = Picture()
    onDrawWithContent {
        val canvas = Canvas(picture.beginRecording(size.width.toInt(), size.height.toInt()))
        draw(this, layoutDirection, canvas, size) {
            this@onDrawWithContent.drawContent()
        }
        picture.endRecording()
        drawIntoCanvas { it.nativeCanvas.drawPicture(picture) }
    }
}

the code is copy from androidx.compose.foundation.draganddrop.CacheDrawScopeDragShadowCallback.

I suppose it cause this issue, but I don't know why.

Hope it's useful for you.

TinyHai commented 7 months ago

I reproduce this issue by applying Modifier#drawWithCache to AsyncImage

Modifier.drawWithCache {
    val picture = Picture()
    onDrawWithContent {
        val canvas = Canvas(picture.beginRecording(size.width.toInt(), size.height.toInt()))
        draw(this, layoutDirection, canvas, size) {
            this@onDrawWithContent.drawContent()
        }
        picture.endRecording()
        drawIntoCanvas { it.nativeCanvas.drawPicture(picture) }
    }
}

the code is copy from androidx.compose.foundation.draganddrop.CacheDrawScopeDragShadowCallback.

I suppose it cause this issue, but I don't know why.

Hope it's useful for you.

In dragAndDropSource, it uses Picture to capture a snapshot by calling drawContent, but it doesn't recall it when inner content changed. It seems like drawing phase is localized.

As a result, it will draw the empty snapshot until next recompose comes.

TinyHai commented 7 months ago

I found a similar issue on issue tracker

Wardroben commented 2 months ago

Noticed that the problem is observed with some images. Some images consistently fail to load, others on the contrary load. I'm writing an app that allows you to select images from the web (google images) and images from your device. All images taken from the network and saved as follows are displayed stably:

val request = ImageRequest.Builder(context = context)
                        .data(url)
                        .crossfade(true)
                        .build()
                    AsyncImage(
                        //some parameters
                        modifier = Modifier
                            //some modifiers
                            .clickable {
                                scope.launch {
                                    val result = (loader.execute(request) as SuccessResult).drawable
                                    val bitmap = (result as BitmapDrawable).bitmap
                                    onImageAdd(index, bitmap ?: return@launch)
                                }
                            }

The bitmap is then saved to disk via FileOutputStream and I get a Uri on it:

private fun saveCompressedImage(bitmap: Bitmap): Uri {
        val fileName = getRandomName()
        checkAndCreateOutputDir()

        val imageFile = File(outputDirectory, fileName)
        try {
            FileOutputStream(imageFile).use { outputStream ->
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                    bitmap.compress(CompressFormat.WEBP_LOSSY, 50, outputStream)
                } else {
                    bitmap.compress(CompressFormat.JPEG, 65, outputStream)
                }
            }
        } catch (e: IOException) {
            Log.e(TAG, e.message.toString())
            throw IOException(e.message)
        }
        return imageFile.toUri()
    }

Images from the device memory are saved through the same function with FileOutputStream, only I read bitmap through content resolver beforehand.

private fun readImage(uri: Uri): Bitmap {
        try {
            appContext.contentResolver.openInputStream(uri).use { inputStream ->
                return BitmapFactory.decodeStream(inputStream)
            }
        } //catches

I get Uri on these images via rememberLauncherForActivityResult with PickMultipleVisualMedia contract.

For example, I'm having trouble with this image saved from device memory: