images: shimmer placeholder while thumbnails and art load (#28)
Thumbnails, channel banners and avatars popped in from a flat grey rectangle. A new ShimmerAsyncImage shows the same faint shimmer as the card placeholders while the image loads, and the sweep disposes once the image (or an error) arrives. Used for video thumbnails and the channel banners/avatars.
This commit is contained in:
parent
aea649eab9
commit
b2af6fc1a5
2 changed files with 48 additions and 6 deletions
|
|
@ -64,9 +64,9 @@ import net.jeena.jtubetv.data.api.ChannelStats
|
|||
import net.jeena.jtubetv.data.api.UserDto
|
||||
import net.jeena.jtubetv.data.api.VideoChannelDto
|
||||
import net.jeena.jtubetv.data.api.VideoDto
|
||||
import net.jeena.jtubetv.ui.list.Snapshots
|
||||
import net.jeena.jtubetv.ui.list.ImagePrefetcher
|
||||
import net.jeena.jtubetv.ui.screens.list.ContainerShape
|
||||
import net.jeena.jtubetv.ui.screens.list.ShimmerAsyncImage
|
||||
import net.jeena.jtubetv.ui.screens.list.UnfocusedCardBg
|
||||
import net.jeena.jtubetv.ui.screens.list.dpadCenterActivatable
|
||||
|
||||
|
|
@ -531,7 +531,7 @@ fun ChannelCard(
|
|||
val coverUrl = banner?.let { absoluteBanner(it) }
|
||||
?: stats?.latestThumbUrl
|
||||
if (coverUrl != null) {
|
||||
AsyncImage(
|
||||
ShimmerAsyncImage(
|
||||
model = coil.request.ImageRequest.Builder(
|
||||
androidx.compose.ui.platform.LocalContext.current,
|
||||
)
|
||||
|
|
@ -545,7 +545,7 @@ fun ChannelCard(
|
|||
}
|
||||
val avatar = channel.avatars.maxByOrNull { it.width }
|
||||
if (avatar != null) {
|
||||
AsyncImage(
|
||||
ShimmerAsyncImage(
|
||||
model = coil.request.ImageRequest.Builder(
|
||||
androidx.compose.ui.platform.LocalContext.current,
|
||||
)
|
||||
|
|
@ -554,12 +554,12 @@ fun ChannelCard(
|
|||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
base = Color(0xFF333333),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(8.dp)
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF333333)),
|
||||
.clip(CircleShape),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ fun VideoCard(
|
|||
.data(video.thumbnailUrl ?: AppGraph.repository.absoluteUrl(path))
|
||||
.size(480, 270) // ~2x the drawn thumb; avoids decoding 1080p+ jpegs
|
||||
.build()
|
||||
AsyncImage(
|
||||
ShimmerAsyncImage(
|
||||
model = req,
|
||||
contentDescription = video.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
|
|
@ -320,6 +320,48 @@ fun ShimmerCard(modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coil image that shows the same faint shimmer as [ShimmerCard] while it loads
|
||||
* (issue #28) — thumbnails, channel banners and avatars no longer pop in from a
|
||||
* flat grey rectangle. The sweep runs only while loading: once the image (or an
|
||||
* error) arrives, the overlay leaves composition and its animation disposes.
|
||||
*/
|
||||
@Composable
|
||||
fun ShimmerAsyncImage(
|
||||
model: Any?,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
contentScale: ContentScale = ContentScale.Crop,
|
||||
base: Color = Color(0xFF222222),
|
||||
) {
|
||||
var loaded by remember(model) { androidx.compose.runtime.mutableStateOf(false) }
|
||||
Box(modifier.background(base)) {
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = contentScale,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
onState = { state ->
|
||||
loaded = state is coil.compose.AsyncImagePainter.State.Success ||
|
||||
state is coil.compose.AsyncImagePainter.State.Error
|
||||
},
|
||||
)
|
||||
if (!loaded) ShimmerFill(Modifier.matchParentSize(), base)
|
||||
}
|
||||
}
|
||||
|
||||
/** Solid base + sweeping sheen; owns its own transition so it disposes on removal. */
|
||||
@Composable
|
||||
private fun ShimmerFill(modifier: Modifier, base: Color) {
|
||||
val sweep = rememberInfiniteTransition()
|
||||
val x by sweep.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(tween(1600, easing = LinearEasing)),
|
||||
)
|
||||
Box(modifier.background(base).shimmer(x))
|
||||
}
|
||||
|
||||
/** Very faint metallic sheen sweeping per [progress] (0..1); barely stronger than the base. */
|
||||
fun Modifier.shimmer(progress: Float): Modifier = drawBehind {
|
||||
drawRect(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue