diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt index e00093c303..44d3dbaece 100644 --- a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt @@ -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 @@ -17,6 +23,8 @@ 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 @@ -24,14 +32,22 @@ 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( @@ -40,6 +56,7 @@ 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, @@ -47,36 +64,93 @@ fun AsyncImage( 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(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 { @@ -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 +}