This question already has answers here:
Auto Height Jetpack Compose Coil Image
(4 answers)
Closed 1 year ago.
Am trying to load multiple images inside a LazyRow composable , the painter state is always giving me an empty state, i dont know what am missing.
The code of the composable
fun NetworkImage(
url: String,
contentDescription: String = "",
modifier: Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
placeholderDrawableRes: Int? = null,
crossFade: Int? = null,
transformations: List<Transformation>? = null,
onLoading: #Composable () -> Unit,
onError: #Composable () -> Unit
) {
Box(
modifier = modifier
){
val painter = rememberImagePainter(
data = url,
builder = {
placeholderDrawableRes?.let {
placeholder( drawableResId = it )
}
error(
R.drawable.ic_warning
)
crossFade?.let {
crossfade(durationMillis = it)
}
transformations?.let {
transformations(transformations = it)
}
}
)
val imageState = painter.state
if(imageState is ImagePainter.State.Loading){
onLoading()
}
if(imageState is ImagePainter.State.Error){
onError()
}
Image(
painter = painter,
contentDescription = contentDescription,
contentScale = contentScale,
alignment = alignment,
alpha = alpha,
colorFilter = colorFilter
)
}
}
Inside The LazyRow
LazyRow(modifier = modifier) {
items(items = urls){ url ->
NetworkImage(
url = url,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
onLoading = {
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
val indicator = createRef()
CircularProgressIndicator(
modifier = Modifier.constrainAs(indicator) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
}
},
onError = {
Icon(
painter = painterResource(id = R.drawable.ic_warning),
contentDescription = "",
tint = Color.Red
)
}
)
}
}
When i use the NetworkImage Composable only once it works fine , but when i use it inside a loop or a LazyRow , it renders an empty Box.
Any hints.
You better use Accompanists Pager(https://google.github.io/accompanist/pager/)
add Pagers liberary to app level module
implementation "com.google.accompanist:accompanist-pager:0.18.0"
then use this code as a sample
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
val pagerState = rememberPagerState(pageCount = urls.size)
HorizontalPager(state = pagerState) { index ->
NetworkImage(
url = urls[index],
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(300.dp),
onLoading = {
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
val indicator = createRef()
CircularProgressIndicator(
modifier = Modifier.constrainAs(indicator) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
}
},
onError = {
Icon(
painter = painterResource(id = R.drawable.ic_warning),
contentDescription = "",
tint = Color.Red
)
}
)
}
}
use NetworkImage composable as it is.
Related
I created a basic column with 10 LEDS and a custom cursor that is able to move up or down. I already changed the zIndex to 1f so its always on top.
Why is my custom cursor changing its brightness if I change its offset position by dragging it up or down?
My goal is to achieve the same amount of brightness no matter where the cursor currently is.
#Preview
#Composable
fun Airflow(climateViewModel: ClimateViewModel = viewModel()) {
var ypos by remember { mutableStateOf(0f) }
Column(modifier = Modifier
.background(Color.Black)) {
Box() {
Image(
modifier = Modifier
.alpha(1f)
.zIndex(1f)
.offset(y = PixelToDp(pixelSize = ypos.toInt())),
painter = painterResource(id = R.drawable.climate_slider_cursor),
contentDescription = null
)
Column(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
ypos = it.y
},
)
}
.pointerInput(Unit) {
detectVerticalDragGestures(
onDragStart = {
ypos = it.y
},
onVerticalDrag = { change, dragAmount ->
ypos = change.position.y
},
onDragEnd = {
},
)
}) {
for (i in 9 downTo 0) {
Image(
modifier = Modifier
.align(Alignment.CenterHorizontally),
painter = painterResource(id = climateViewModel.leds[i]),
contentDescription = null
)
}
Image(
modifier = Modifier
.align(Alignment.CenterHorizontally),
painter = painterResource(id = R.drawable.climate_slider_off),
contentDescription = null
)
}
}
Image(
modifier = Modifier
.padding(top = PixelToDp(pixelSize = 27))
.align(Alignment.CenterHorizontally),
painter = painterResource(id = R.drawable.climate_slider_fan),
contentDescription = null
)
}
}
I want to display web images by URL links. I want to display progress bar until image will be loaded and then hide progress bar and display image.
How to detect when painter loaded image?
HorizontalPager(count = photoUrlList.size, state = photoPagerState) { page ->
var shouldBeProgressBarDisplayed by mutableStateOf(true)
val painter = rememberImagePainter(photoUrlList[page], builder = {
this.listener(
onSuccess = { request, ex ->
shouldBeProgressBarDisplayed = false
})
})
if (shouldBeProgressBarDisplayed) {
ProgressBar()
} else {
Image(
painter = painter,
modifier = Modifier
.padding(vertical = 100.dp)
.fillMaxSize(),
contentScale = ContentScale.Crop,
contentDescription = ""
)
}
}
You can use painter.state to achieve this
HorizontalPager(count = photoUrlList.size, state = photoPagerState) { page ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 100.dp)
) {
val painter = rememberImagePainter(
data = photoUrlList[page],
builder = {
crossfade(500)
})
val painterState = painter.state
if (painterState is ImagePainter.State.Loading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colors.secondary
)
} else {
Image(
painter = painter,
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.Crop,
contentDescription = ""
)
}
}
}
I recently migrated from Accompanist's ImagePainter to Coil's, below is the pertinent code after my updates.
val painter = rememberImagePainter(DRAWABLE_RESOURCE_ID)
when (painter.state) {
is ImagePainter.State.Empty -> Timber.w("Empty")
is ImagePainter.State.Loading -> {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.wrapContentSize()
) {
CircularProgressIndicator()
}
}
is ImagePainter.State.Success -> {
Image(
painter = painter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.padding(8.dp)
.size(84.dp)
.clip(RoundedCornerShape(corner = CornerSize(16.dp)))
)
}
is ImagePainter.State.Error -> Timber.e("Error")
}
Now those images don't render and painter.state is always Empty. My legacy Accompanist implementation displayed images by this point in the code. It also works if I use the stock painterResource(resId) from Compose.
What am I missing to execute Coil's new painter through its states?
As suggested by #Philip Dukhov you don't need coil to load local resources.
If you want to use it, you can simply your code using:
val painter = rememberImagePainter(R.drawable.xxx)
val state = painter.state
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.wrapContentSize()
) {
AnimatedVisibility(visible = (state is ImagePainter.State.Loading)) {
CircularProgressIndicator()
}
Image(
painter = painter,
contentDescription = null,
modifier = Modifier.size(128.dp)
)
}
You can use the drawable's resource ID as model for the coil's AsyncImage composable.
AsyncImage(
model = R.drawable.bg_gradient,
contentDescription = "Gradient background",
)
There's a significant performance difference between the composables - Image and AsyncImage. So Coil's AsyncImage loading is very handy at times.
You don't need coil to load local resources. You can use system painterResource:
Image(
painter = painterResource(id = R.drawable.test),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.padding(8.dp)
.size(84.dp)
.clip(RoundedCornerShape(corner = CornerSize(16.dp)))
)
If you would use it for remove image loading: since move from accompanist to coil, painter won't start loading unless Image is in the view tree hierarchy. So you can move Image into a Box with your while:
Box(contentAlignment = Alignment.Center) {
val painter = rememberImagePainter(R.drawable.test)
Image(
painter = painter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.padding(8.dp)
.size(84.dp)
.clip(RoundedCornerShape(corner = CornerSize(16.dp)))
)
when (painter.state) {
is ImagePainter.State.Empty -> Timber.w("Empty")
is ImagePainter.State.Loading -> {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.wrapContentSize()
) {
CircularProgressIndicator()
}
}
is ImagePainter.State.Success -> {
}
is ImagePainter.State.Error -> Timber.e("Error")
}
}
Also it may not start loading when you're not providing enough size modifiers(that's not your case, just for you to know). Check out this answer for more information.
val context = LocalContext.current
val imageLoader = ImageLoader(context)
val request = ImageRequest.Builder(context)
.data(thumbnailUrl)
.build()
val painter = rememberImagePainter(
request = request,
imageLoader = imageLoader
)
val state = painter.state
Image(
painter = painter,
contentDescription = "thumbnail image",
modifier = Modifier
.fillMaxSize()
.placeholder(
visible = state is ImagePainter.State.Loading,
color = PlaceholderDefaults.color(
backgroundColor = SMXTheme.colors.shimmer.copy(0.1f),
),
highlight = PlaceholderHighlight.shimmer(),
),
contentScale = ContentScale.Crop
)
I wanna have this layout in Jetpack Compose. How can I place the image at the bottom of the card and have it a bit outside the card borders? Also how to make sure that the content above won't collide with the image, because the content above can be longer. Thanks very much.
A simple way is to use a Box and apply an Offset to the Image
Box() {
val overlayBoxHeight = 40.dp
Card(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
) {
//...
}
Image(
painterResource(id = R.drawable.xxxx),
contentDescription = "",
modifier = Modifier
.align(BottomCenter)
.offset(x = 0.dp, y = overlayBoxHeight )
)
}
If you want to calculate the offset you can use a Layout composable.
Something like:
Layout( content = {
Card(
elevation = 10.dp,
modifier = Modifier
.layoutId("card")
.fillMaxWidth()
.height(300.dp)
) {
//...
}
Image(
painterResource(id = R.drawable.conero),
contentDescription = "",
modifier = Modifier
.layoutId("image")
)
}){ measurables, incomingConstraints ->
val constraints = incomingConstraints.copy(minWidth = 0, minHeight = 0)
val cardPlaceable =
measurables.find { it.layoutId == "card" }?.measure(constraints)
val imagePlaceable =
measurables.find { it.layoutId == "image" }?.measure(constraints)
//align the icon on the top/end edge
layout(width = widthOrZero(cardPlaceable),
height = heightOrZero(cardPlaceable)+ heightOrZero(imagePlaceable)/2){
cardPlaceable?.placeRelative(0, 0)
imagePlaceable?.placeRelative(widthOrZero(cardPlaceable)/2 - widthOrZero(imagePlaceable)/2,
heightOrZero(cardPlaceable) - heightOrZero(imagePlaceable)/2)
}
}
You can use Card for main view layout.
To have an icon placed like this, you need to place it with Card into a box, apply Alignment.BottomCenter alignment to image and add paddings.
To make image be under your text, I've added Spacer with size calculated on image size.
#Composable
fun TestView() {
CompositionLocalProvider(
LocalContentColor provides Color.White
) {
Column {
LazyRow {
items(10) {
Item()
}
}
Box(Modifier.fillMaxWidth().background(Color.DarkGray)) {
Text("next view")
}
}
}
}
#Composable
fun Item() {
Box(
Modifier
.padding(10.dp)
) {
val imagePainter = painterResource(id = R.drawable.test2)
val imageOffset = 20.dp
Card(
shape = RoundedCornerShape(0.dp),
backgroundColor = Color.DarkGray,
modifier = Modifier
.padding(bottom = imageOffset)
.width(200.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(20.dp)
) {
Text(
"Title",
modifier = Modifier.align(Alignment.Start)
)
Spacer(modifier = Modifier.size(15.dp))
Text("PM2.5")
Text(
"10",
fontSize = 35.sp,
)
Text("m/s")
Spacer(modifier = Modifier.size(15.dp))
Text(
"Very Good",
fontSize = 25.sp,
)
Spacer(
modifier = Modifier
.size(
with(LocalDensity.current) { imagePainter.intrinsicSize.height.toDp() - imageOffset }
)
)
}
}
Image(
painter = imagePainter,
contentDescription = "",
Modifier
.align(Alignment.BottomCenter)
)
}
}
Result:
I want to build a dropdown menu with items that contain not only text, but also an image. The image should be loaded from an url. I'm using Dropdown Menu and Accompanist to load the image.
But when I try to open the Dropdown Menu, I get java.lang.IllegalStateException: Intrinsic measurements are not currently supported by SubcomposeLayout.
I've tried to play around with Intrinsics in my Composables like here https://developer.android.com/codelabs/jetpack-compose-layouts#10, but it didn't work. If I don't use CoilImage, but get a painter from resources with Image, everything works fine.
Is there a way to solve it?
#Composable
fun DropdownChildren(
items: List<ChildUiModel>,
chosenChild: ChildUiModel?,
onChildChosen: (ChildUiModel) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
var selectedIndex by remember { mutableStateOf(0) }
Box(modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.TopStart)) {
Row(modifier = Modifier
.fillMaxSize()
.clickable(onClick = { expanded = true })) {
ChildrenDropdownMenuItem(
imageUrl = "https://oneyearwithjesus.files.wordpress.com/2014/09/shutterstock_20317516.jpg",
text = items[selectedIndex].name?: "No name",
chosen = false)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.fillMaxWidth()
.requiredSizeIn(maxHeight = 500.dp)
) {
items.forEachIndexed { index, child ->
DropdownMenuItem(
modifier = Modifier
.background(color = if (child.id == chosenChild?.id) appColors.secondary else appColors.primary),
onClick = {
expanded = false
selectedIndex = index
onChildChosen(items[selectedIndex])
}) {
ChildrenDropdownMenuItem(
imageUrl = "https://oneyearwithjesus.files.wordpress.com/2014/09/shutterstock_20317516.jpg",
text = child.name,
chosen = child.id == chosenChild?.id)
}
}
}
}
}
#Composable
fun ChildrenDropdownMenuItem(
imageUrl: String,
text: String,
chosen: Boolean
){
Row(){
Avatar(url = imageUrl)
Text(text = text,
style = AppTheme.typography.h4,
color = if (chosen) appColors.primary else appColors.secondary,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterVertically))
}
}
#Composable
fun Avatar(
url: String
){
val contentPadding = PaddingValues(8.dp, 8.dp, 12.dp, 8.dp)
CoilImage(
data = url,
) { imageState ->
when (imageState) {
is ImageLoadState.Success -> {
MaterialLoadingImage(
result = imageState,
contentDescription = "avatar",
modifier = Modifier
.padding(contentPadding)
.clip(CircleShape)
.size(48.dp),
contentScale = ContentScale.Crop,
fadeInEnabled = true,
fadeInDurationMs = 600,
)
}
is ImageLoadState.Error -> CoilImage(
data = "https://www.padtinc.com/blog/wp-content/uploads/2020/09/plc-errors.jpg",
contentDescription = "error",
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(contentPadding)
.clip(CircleShape)
.size(48.dp)
)
ImageLoadState.Loading -> CircularProgressIndicator()
ImageLoadState.Empty -> {}
}
}
}
CoilImage is now Deprecated,
use this
Image(
painter = rememberCoilPainter("https://picsum.photos/300/300"),
contentDescription = stringResource(R.string.image_content_desc),
previewPlaceholder = R.drawable.placeholder,
)
As you mentioned on the bug, this has already been fixed in v0.8.0 of Accompanist. You need to use the new rememberCoilPainter() though, and not the deprecated CoilImage().