glide icon indicating copy to clipboard operation
glide copied to clipboard

Consider implementing contentScale in Compose without using Transformations

Open tunjid opened this issue 2 months ago • 0 comments

Glide compose version used: 1.0.0-beta01.

In GlideImage, the implementation of ContentScale is with a contentScaleTransform.

This causes a few issues:

  • The same image with different content scales applied will have multiple memory cache entries.
  • It is currently not possible to interpolate a content scale change.

Ideally the application of a content scale should not require the creation of separate images. The same image should be properly displayed in the bounds of the container and not have any extra processing applied to it for different content scales.

The bug is best demonstrated visually:

glide-bug.webm

It is possible to interpolate ContentScale as described in this blog post with the following code:

@Composable
fun ContentScale.interpolate(): ContentScale {
    var interpolation by remember {
        mutableFloatStateOf(1f)
    }
    var previousScale by remember {
        mutableStateOf(this)
    }

    val currentScale by remember {
        mutableStateOf(this)
    }.apply {
        if (value != this@interpolate) previousScale = when {
            interpolation == 1f -> value
            else -> CapturedContentScale(
                capturedInterpolation = interpolation,
                previousScale = previousScale,
                currentScale = value
            )
        }.also { interpolation = 0f }
        value = this@interpolate
    }

    LaunchedEffect(currentScale) {
        animate(
            initialValue = 0f,
            targetValue = 1f,
            animationSpec = spring(
                stiffness = 10f

            ),
            block = { progress, _ ->
                interpolation = progress
            },
        )
    }

    return remember {
        object : ContentScale {
            override fun computeScaleFactor(
                srcSize: Size,
                dstSize: Size
            ): ScaleFactor {
                val start = previousScale.computeScaleFactor(
                    srcSize = srcSize,
                    dstSize = dstSize
                )
                val stop = currentScale.computeScaleFactor(
                    srcSize = srcSize,
                    dstSize = dstSize
                )

                return if (start == stop) stop
                else lerp(
                    start = start,
                    stop = stop,
                    fraction = interpolation
                )
            }
        }
    }
}


private class CapturedContentScale(
    private val capturedInterpolation: Float,
    private val previousScale: ContentScale,
    private val currentScale: ContentScale,

    ) : ContentScale {
    override fun computeScaleFactor(
        srcSize: Size,
        dstSize: Size
    ): ScaleFactor = lerp(
        start = previousScale.computeScaleFactor(
            srcSize = srcSize,
            dstSize = dstSize
        ),
        stop = currentScale.computeScaleFactor(
            srcSize = srcSize,
            dstSize = dstSize
        ),
        fraction = capturedInterpolation
    )
}

In a comparison with Coil and a manual painter, Glide is unable to actually apply this interpolation. Consider the following:

                    Column(
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        var contentScale by remember { mutableStateOf(ContentScale.Crop) }
                        ImageComparison(
                            imageUrl = IMAGE_URL,
                            contentDescription = null,
                            center = Alignment.Center,
                            crop = contentScale.interpolate(),
                            modifier = Modifier.size(200.dp),
                        )

                        val scales = remember {
                            listOf(
                                ContentScale.Crop to "Crop",
                                ContentScale.Fit to "Fit",
                                ContentScale.None to "None",
                                ContentScale.FillHeight to "FillHeight",
                                ContentScale.FillWidth to "FillWidth",
                                ContentScale.FillBounds to "Fill",
                            )
                        }
                        Row {
                            scales.forEach { (scale, label) ->
                                Text(
                                    text = label,
                                    textDecoration = if (contentScale == scale) TextDecoration.Underline else TextDecoration.None,
                                    modifier = Modifier
                                        .padding(8.dp)
                                        .clickable {
                                            contentScale = scale
                                        }
                                )
                            }
                        }
                    }

Where ImageComparison is:

@OptIn(ExperimentalGlideComposeApi::class)
@Composable
private fun ImageComparison(
    imageUrl: String,
    contentDescription: String?,
    center: Alignment,
    crop: ContentScale,
    modifier: Modifier
) {
    AsyncImage(
        modifier = modifier,
        model = imageUrl,
        contentDescription = contentDescription,
        alignment = center,
        contentScale = crop,
    )
    Text(text = "Coil")

    GlideImage(
        modifier = modifier,
        model = imageUrl,
        contentDescription = contentDescription,
        alignment = center,
        contentScale = crop,
    )
    Text(text = "Glide")

    ImageByUrl(
        modifier = modifier,
        imageUrl = imageUrl,
        contentDescription = contentDescription,
        alignment = center,
        contentScale = crop,
    )
    Text(text = "Manual Painter")
}

tunjid avatar Mar 28 '24 16:03 tunjid