diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt index e26a824af..3bd3471ad 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt @@ -16,11 +16,28 @@ package com.google.samples.apps.nowinandroid.core.designsystem.component +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter.State.Error +import coil.compose.AsyncImagePainter.State.Loading +import coil.compose.rememberAsyncImagePainter +import com.google.samples.apps.nowinandroid.core.designsystem.R import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme /** @@ -31,14 +48,37 @@ fun DynamicAsyncImage( imageUrl: String, contentDescription: String?, modifier: Modifier = Modifier, - placeholder: Painter? = null, + placeholder: Painter = painterResource(R.drawable.ic_placeholder_default), ) { val iconTint = LocalTintTheme.current.iconTint - AsyncImage( - placeholder = placeholder, + var isLoading by remember { mutableStateOf(true) } + var isError by remember { mutableStateOf(false) } + val imageLoader = rememberAsyncImagePainter( model = imageUrl, - contentDescription = contentDescription, - colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, - modifier = modifier, + onState = { state -> + isLoading = state is Loading + isError = state is Error + }, ) + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + if (isLoading) { + // Display a progress bar while loading + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .size(80.dp), + color = MaterialTheme.colorScheme.tertiary, + ) + } + Image( + contentScale = ContentScale.Crop, + painter = if (isError.not()) imageLoader else placeholder, + contentDescription = contentDescription, + colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, + modifier = modifier, + ) + } } diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index c9a327881..4ed583b66 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -17,6 +17,7 @@ package com.google.samples.apps.nowinandroid.core.ui import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -31,6 +32,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -57,7 +59,9 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter +import coil.compose.rememberAsyncImagePainter +import com.google.samples.apps.nowinandroid.core.designsystem.R.drawable import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons @@ -72,7 +76,6 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.Locale -import com.google.samples.apps.nowinandroid.core.designsystem.R as DesignsystemR /** * [NewsResource] card used on the following screens: For You, Saved @@ -147,21 +150,41 @@ fun NewsResourceCardExpanded( fun NewsResourceHeaderImage( headerImageUrl: String?, ) { - AsyncImage( - placeholder = if (LocalInspectionMode.current) { - painterResource(DesignsystemR.drawable.ic_placeholder_default) - } else { - // TODO b/228077205, show specific loading image visual - null + var isLoading by remember { mutableStateOf(true) } + var isError by remember { mutableStateOf(false) } + val imageLoader = rememberAsyncImagePainter( + model = headerImageUrl, + onState = { state -> + isLoading = state is AsyncImagePainter.State.Loading + isError = state is AsyncImagePainter.State.Error }, + ) + Box( modifier = Modifier .fillMaxWidth() .height(180.dp), - contentScale = ContentScale.Crop, - model = headerImageUrl, - // TODO b/226661685: Investigate using alt text of image to populate content description - contentDescription = null, // decorative image - ) + contentAlignment = Alignment.Center, + ) { + if (isLoading) { + // Display a progress bar while loading + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .size(80.dp), + color = MaterialTheme.colorScheme.tertiary, + ) + } + + Image( + modifier = Modifier + .fillMaxWidth() + .height(180.dp), + contentScale = ContentScale.Crop, + painter = if (isError.not()) imageLoader else painterResource(drawable.ic_placeholder_default), + // TODO b/226661685: Investigate using alt text of image to populate content description + contentDescription = null, // decorative image, + ) + } } @Composable diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index c8c74f78f..a24a91f1a 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -434,7 +434,6 @@ fun TopicIcon( modifier: Modifier = Modifier, ) { DynamicAsyncImage( - // TODO b/228077205, show loading image visual instead of static placeholder placeholder = painterResource(R.drawable.ic_icon_placeholder), imageUrl = imageUrl, contentDescription = null, // decorative