Remote image crossfade
Sunday 5th July 2026
Unused composable from a recent project that crossfades remote images. The use-case was a ui that periodically fetched a random image Url from a server.
package yorkshire.systems.demo
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.request.ImageRequest
@Composable
fun RemoteImageBackground(
interfaceVisible: Boolean,
imageUrl: String?
) {
if (imageUrl == null) return
var currentUrl by remember { mutableStateOf(imageUrl) }
var incomingUrl by remember { mutableStateOf<String?>(null) }
LaunchedEffect(imageUrl) {
incomingUrl = if (imageUrl != currentUrl) imageUrl else null
}
var incomingLoaded by remember(incomingUrl) { mutableStateOf(false) }
val incomingAlpha by animateFloatAsState(
targetValue = if (incomingLoaded) 1f else 0f,
animationSpec = tween(durationMillis = 2000),
label = "ImageFade",
finishedListener = { value ->
val incoming = incomingUrl
if (value == 1f && incoming != null) {
currentUrl = incoming
incomingUrl = null
}
}
)
Box(Modifier.fillMaxSize()) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(currentUrl)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
incomingUrl?.let { url ->
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
onState = { state ->
if (state is AsyncImagePainter.State.Success) incomingLoaded = true
},
modifier = Modifier
.fillMaxSize()
.alpha(incomingAlpha)
)
}
if(interfaceVisible) {
Box(
Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background.copy(alpha = if(isSystemInDarkTheme()){
0.55f
}else{
0.65f
}))
)
}
}
}