Skip to content

Commit

Permalink
Add crossfade to AsyncImage()
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes committed Jun 30, 2023
1 parent a886ff6 commit ff012bb
Showing 1 changed file with 89 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
// Copyright 2022, Google LLC, Christopher Banes and the Tivi project contributors
// SPDX-License-Identifier: Apache-2.0

@file:OptIn(ExperimentalCoroutinesApi::class)

package app.tivi.common.compose.ui

import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -17,21 +23,31 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import com.seiko.imageloader.ImageLoader
import com.seiko.imageloader.ImageRequestState
import com.seiko.imageloader.LocalImageLoader
import com.seiko.imageloader.asImageBitmap
import com.seiko.imageloader.model.ImageRequest
import com.seiko.imageloader.model.ImageRequestBuilder
import com.seiko.imageloader.model.ImageResult
import com.seiko.imageloader.option.Scale
import com.seiko.imageloader.option.SizeResolver
import com.seiko.imageloader.rememberAsyncImagePainter
import com.seiko.imageloader.toPainter
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.withContext

@Composable
fun AsyncImage(
Expand All @@ -40,43 +56,101 @@ fun AsyncImage(
modifier: Modifier = Modifier,
onState: ((ImageRequestState) -> Unit)? = null,
requestBuilder: (ImageRequestBuilder.() -> ImageRequestBuilder)? = null,
imageLoader: ImageLoader = LocalImageLoader.current,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
) {
val sizeResolver = ConstraintsSizeResolver()
var requestState: ImageRequestState by remember { mutableStateOf(ImageRequestState.Loading()) }

val request = ImageRequest {
data(model)
size(sizeResolver)
requestBuilder?.invoke(this)

options {
if (scale == Scale.AUTO) {
scale = contentScale.toScale()
}
}
eventListener {
requestState = ImageRequestState.Loading(it)
}
}

val painter = rememberAsyncImagePainter(
request = request,
contentScale = contentScale,
filterQuality = filterQuality,
)
var result by remember { mutableStateOf<ImageResult?>(null) }
LaunchedEffect(imageLoader) {
snapshotFlow { request }
.mapLatest {
withContext(imageLoader.config.imageScope.coroutineContext) {
imageLoader.execute(request)
}
}
.collect { result = it }
}

val lastOnState by rememberUpdatedState(onState)
LaunchedEffect(painter) {
snapshotFlow { painter.requestState }
LaunchedEffect(Unit) {
snapshotFlow { requestState }
.collect { lastOnState?.invoke(it) }
}

Crossfade(result) { r ->
ResultImage(
result = r,
alignment = alignment,
contentDescription = contentDescription,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
modifier = modifier.then(sizeResolver),
filterQuality = filterQuality,
)
}
}

@Composable
private fun ResultImage(
result: ImageResult?,
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
) {
Image(
painter = painter,
painter = when (result) {
is ImageResult.Bitmap -> {
BitmapPainter(
image = result.bitmap.asImageBitmap(),
filterQuality = filterQuality,
)
}
is ImageResult.Image -> result.image.toPainter(filterQuality)
is ImageResult.Painter -> result.painter
is ImageResult.Error -> TODO()
is ImageResult.Source -> TODO()
null -> EmptyPainter
},
alignment = alignment,
contentDescription = contentDescription,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
modifier = modifier.then(sizeResolver),
modifier = modifier,
)
}

private object EmptyPainter : Painter() {
override val intrinsicSize get() = Size.Unspecified
override fun DrawScope.onDraw() = Unit
}

/** A [SizeResolver] that computes the size from the constrains passed during the layout phase. */
internal class ConstraintsSizeResolver : SizeResolver, LayoutModifier {

Expand Down Expand Up @@ -113,3 +187,8 @@ private fun Constraints.toSizeOrNull() = when {
height = if (hasBoundedHeight) maxHeight.toFloat() else 0f,
)
}

private fun ContentScale.toScale() = when (this) {
ContentScale.Fit, ContentScale.Inside -> Scale.FIT
else -> Scale.FILL
}

0 comments on commit ff012bb

Please sign in to comment.