glide
glide copied to clipboard
Consider implementing contentScale in Compose without using Transformations
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:
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")
}