I'm migrating from XML layouts to Jetpack Compose and noticed that the Image composable does not scale an image as good as ImageView when the image gets cropped.
The image on the top is added via Jetpack Compose, the one at the bottom is an ImageView.
For comparison, I added them next to each other like this:
Column {
Image(
painter = painterResource(id = R.drawable.my_image),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(280.dp)
)
Spacer(
modifier = Modifier.height(4.dp)
)
AndroidView(
factory = { context ->
ImageView(context).apply {
scaleType = ImageView.ScaleType.CENTER_CROP
setImageResource(R.drawable.my_image)
}
},
modifier = Modifier.size(280.dp)
)
}
The effect is depending on the image which is used and I noticed it's mostly promiment on low-end devices, however I would expect Jetpack Compose to have the same or higher image quality, as it is the new framework for building UI on Android.
Is there something I'm missing which can give me the same quality as ImageView?
Related
Currently, I'm a bit stuck on having a resizable drawable in the background of a card.
Card(
modifier,
shape = RoundedCornerShape(8.dp),
elevation = 6.dp
) {
Image(
painterResource(...),
contentScale = ContentScale.FillWidth,
contentDescription = null,
modifier = Modifier.fillMaxWidth()
)
Box() {
Surface(
color = Color.Transparent,
contentColor = Color.White
) {
Column(Modifier.padding(24.dp)) {
Spacer(Modifier.height(16.dp))
Text(...)
Spacer(Modifier.height(16.dp))
Text(...)
Spacer(Modifier.height(8.dp))
Text(...)
Spacer(Modifier.height(16.dp))
Button(...))
}
}
}
}
What I really am trying to get is the image to resize accordingly to the contents of the card...but not doing that. Basically, it doesn't matter if the image is cropped, but I'm trying to make it fill the card. Right now, since I have FillWidth, on bigger screens like tablets, there is a large unused space at the bottom of the card. Any help would be much appreciated!
Tried replacing the whole card with a box instead, didn't work, different permutations of contentscale also did not help.
I have a pager (Accompanist) with image that are get from web with Coil in Compose.
The rememberPainter() seem to only call the request when the Image composable is shown for the first time.
So when I slide page in the pager, the Image show only at that moment and so we have to wait a moment.
Any way to force the rememberPainter (Coil) to preload ?
Edit 1 :
Here is a simple version of my implementation (with lots of stuff around removed but that had no impact on the result) :
#Composable
private fun Foo(imageList: List<ImageHolder>) {
if (imageList.isEmpty())
return
val painterList = imageList.map {
rememberImagePainter(
data = it.getDataForPainter(),
builder = {
crossfade(true)
})
}
ImageLayout(imageList, painterList)
}
#Composable
fun ImageLayout(imageList: List<ImageHolder>, painterList: List<Painter>) {
HorizontalPager(
count = imageList.size,
modifier = Modifier.fillMaxWidth(),
) { page ->
Image(
painter = painterList[page],
"",
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(
imageList[page].colorAverage
),
contentScale = ContentScale.Crop
)
}
}
I tried with having directly the rememberImagePainter at the Image too just in case.
But the problem is clearly that Image() of page 2, 3, 4 isn't rendered so Image don't do the call to the painter. I tried to look how it work inside but couldn't find.
Edit 2 : I found a workaround but it's not clean
for (painter in painterList)
Image(painter = painter, contentDescription = "", modifier = Modifier.size(0.001.dp))
It force coil to load image and with a size really small like 0.001.dp (with 0 it don't load).
Also the problem is that in the builder you need to force a size or it will just load one pixel, so I force the full size of image as I don't know what size would have been available for the image.
In the Coil documentation there is a section about preloading. Depending on your architecture, you can do this in different places, the easiest is to use LaunchedEffect:
val context = LocalContext.current
LaunchedEffect(Unit) {
val request = ImageRequest.Builder(context)
.data("https://www.example.com/image.jpg")
// Optional, but setting a ViewSizeResolver will conserve memory by limiting the size the image should be preloaded into memory at.
// For example you can set static size, or size resolved with Modifier.onSizeChanged
// .size(coil.size.PixelSize(width = 100, height = 100))
// or resolve display size if your images are full screen
// .size(DisplaySizeResolver(context))
.build()
context.imageLoader.enqueue(request)
}
Before version 2.0.0 use LocalImageLoader.current instead of context.imageLoader to get the image loader.
Here is full code for accompanist pager with coil preloading and indicator with video example
I am trying to show an image with fill width and auto height using Jetpack Compose Coil. I want the image to take the full width and auto height. But the image only showing when I specify a fixed height.
Image(
painter = rememberImagePainter(
data = post.image
),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
I tried with .fillMaxHeight() and .fillMaxSize() but it's not working. Is there any way I could achieve this?
This happens when one of view width/height is calculated as zero, which means it shouldn't be displayed and no need to download it. Check out more about the reasons in this issue on compose tracker.
You should make your size not being equal to zero. Depending you your layout it can be done with variations of width/height/aspect ratio modifiers.
If you'd like to get your image in original size, you can to add size(OriginalSize) to painter builder. This will force image to start loading. This parameter makes your view download and put into the memory full size of the image, without optimizing it for the current view. So use it carefully, only if you're sure the image won't be too big and you really can't add use size modifiers.
Image(
painter = rememberImagePainter(
data = post.image,
builder = {
size(OriginalSize)
},
),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
With latest Coil 2.1.0 you can do like this:
val model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.size(Size.ORIGINAL)
.crossfade(true)
.build()
val painter = rememberAsyncImagePainter(model)
Image(
modifier = Modifier.fillMaxWidth(),
painter = painter,
contentDescription = null,
contentScale = ContentScale.FillWidth
)
If your image OriginalSize is to small for your composable you can also use scale(Scale.FIT) in your builder.
Your image will max fit your composable while keeping it's aspect ratio.
Image(
rememberImagePainter(
data = user?.photoUrl?:"",
builder = {
size(OriginalSize)
scale(Scale.FIT)
transformations(CircleCropTransformation())
}
),
contentDescription = "Picture",
contentScale = ContentScale.FillWidth
)
Instead of using size(OriginalSize) that impacts image optimization, you can set min height/width for image (for example 1.dp). This will force image to start loading:
Image(
painter = rememberImagePainter(url),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 1.dp),
)
I would like to display an image with the following rules:
The image is remote and needs to load on runtime.
We don't know what the image size is until it's loaded.
The Image view should take the full width of the parent, and it should automatically adjust its height to match the remotely loaded image, so that no cropping/stretching occurs.
I tried using the Coil dependency and I have this code:
Image(
painter = rememberImagePainter(viewModel.fullImageUrl),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(3.21428f) // TODO: Figure out how to set height/ratio based on image itself
)
When I remove the Image's contentScale and modifier, its height always seems to be zero. I'm not sure what I should do to make Image fill the max width of its parent while determining its own size after Coil loads the image file.
As you set flexible width with fillMaxWidth, you need to use ContentScale.FillWidth for your contentScale:
Image(
painter = rememberImagePainter(
"https://via.placeholder.com/300.png",
),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
)
The following will work fine if your image has enough priority. But in some cases coil won't start loading because it's gonna think that image is zero size so no need to display it. For such case you can add size(OriginalSize) parameter to the builder:
rememberImagePainter(
"https://via.placeholder.com/300.png",
builder = {
size(OriginalSize)
}
)
This parameter makes your view download and put into the memory full size of the image, without optimizing it for the current view. So use it carefully, only if you're sure the image won't be too big and you really can't add use size modifiers.
If image is getting loaded but still not takes enough space, you can set aspect ratio like this:
val painter = rememberImagePainter("https://via.placeholder.com/300.png")
Image(
painter = painter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.fillMaxWidth()
.then(
(painter.state as? ImagePainter.State.Success)
?.painter
?.intrinsicSize
?.let { intrinsicSize ->
Modifier.aspectRatio(intrinsicSize.width / intrinsicSize.height)
} ?: Modifier
)
)
If none of these helped and painter.state is not changing from Empty, try hardcode height ?: Modifier.height(10.dp) to let image start loading
I'm migrating my app and for the grid view I used LazyVerticalGrid that loads images using coilPainter but the scrolling stutters a lot and it feels laggy. Does anyone have a solution or a better implementation for this?
val photos: List<Photo>? by photosViewModel.photosLiveData.observeAsState(null)
Surface(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier.fillMaxSize()
) {
LazyVerticalGrid(
cells = GridCells.Fixed(3)
) {
photos?.let { photosList ->
items(photosList) { photo ->
Image(
modifier = Modifier.padding(2.dp).clickable {
photosViewModel.onPhotoClicked(photo)
},
painter = rememberCoilPainter(photo.url),
contentDescription = stringResource(R.string.cd_details_photo),
contentScale = ContentScale.Fit,
alignment = Alignment.Center,
)
}
}
}
}
}
Update
Trying to move rememberCoilPainter above Image like val painter = rememberCoilPainter(photo.url) and use the painter inside the Image did not work
items takes an additional parameter key, assign this parameter to a unique value like the index of each photo of your list, If you don't know how does that relate to scrolling jank? Or what the heck am I talking about? check out the following documentation for a detailed explanation:
https://developer.android.com/jetpack/compose/lifecycle#add-info-smart-recomposition
After updating my compose image loading library to coil-compose I was forced to set a size to either the request builder or the image itself. The problem of the stuttering was caused by allowing the photo to load the original size and thus recomposing was done multiple times and loading the image had to wait for the picture to load. By setting e fixed height to the Image fixed the issue because the Images() were created and the recomposition was made only for the picture inside not for the Image itself with different height.