Skip to content

Commit

Permalink
Merge pull request #334 from qdsfdhvh/feature/refactor_size_resolver
Browse files Browse the repository at this point in the history
add @composable AutoSizeBox & AutoSizeImage
  • Loading branch information
qdsfdhvh committed Nov 5, 2023
2 parents 364f73b + 7bf5fce commit 601c1b8
Show file tree
Hide file tree
Showing 28 changed files with 1,081 additions and 190 deletions.
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ fun Content() {
CompositionLocalProvider(
LocalImageLoader provides remember { generateImageLoader() },
) {
// Option 1 on 1.7.0+
AutoSizeImage(
"https://...",
contentDescription = "image",
)
// Option 2 on 1.7.0+
AutoSizeBox("https://...") { action ->
when (action) {
is ImageAction.Success -> {
Image(
rememberImageSuccessPainter(action),
contentDescription = "image",
)
}
is ImageAction.Loading -> {}
is ImageAction.Failure -> {}
}
}
// Option 3
val painter = rememberImagePainter("https://..")
Image(
painter = painter,
Expand All @@ -67,9 +86,13 @@ fun Content() {
}
```

Use priority: `AutoSizeImage` -> `AutoSizeBox` -> `rememberImagePainter`.

`AutoSizeBox` & `AutoSizeImage` are based on **Modifier.Node**, `AutoSizeImage``AutoSizeBox` + `Painter`.

#### in Android

```kotlin title="MainActivity.kt"
```kotlin
fun generateImageLoader(): ImageLoader {
return ImageLoader {
options {
Expand All @@ -96,7 +119,7 @@ fun generateImageLoader(): ImageLoader {

#### in Jvm

```kotlin title="Main.kt"
```kotlin
fun generateImageLoader(): ImageLoader {
return ImageLoader {
components {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
Expand All @@ -28,12 +27,11 @@ import androidx.compose.ui.layout.ContentScale
import com.seiko.imageloader.demo.model.Image
import com.seiko.imageloader.demo.util.NullDataInterceptor
import com.seiko.imageloader.demo.util.decodeJson
import com.seiko.imageloader.model.ImageEvent
import com.seiko.imageloader.model.ImageAction
import com.seiko.imageloader.model.ImageRequest
import com.seiko.imageloader.model.ImageRequestBuilder
import com.seiko.imageloader.model.ImageResult
import com.seiko.imageloader.rememberImageAction
import com.seiko.imageloader.rememberImageActionPainter
import com.seiko.imageloader.rememberImageSuccessPainter
import com.seiko.imageloader.ui.AutoSizeBox
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

Expand Down Expand Up @@ -77,7 +75,7 @@ fun ImageItem(
Box(modifier, Alignment.Center) {
val dataState by rememberUpdatedState(data)
val blockState by rememberUpdatedState(block)
val requestState = remember {
val request by remember {
derivedStateOf {
ImageRequest {
data(dataState)
Expand All @@ -92,27 +90,27 @@ fun ImageItem(
}
}
}
val action by rememberImageAction(requestState)
val painter = rememberImageActionPainter(action)
Image(
painter = painter,
contentDescription = null,
contentScale = contentScale,
modifier = Modifier.fillMaxSize(),
)
when (val current = action) {
is ImageEvent.StartWithDisk,
is ImageEvent.StartWithFetch,
-> {
CircularProgressIndicator()
}
is ImageResult.OfSource -> {
Text("image result is source")
}
is ImageResult.OfError -> {
Text(current.error.message ?: "Error")

AutoSizeBox(
request,
Modifier.matchParentSize(),
) { action ->
when (action) {
is ImageAction.Loading -> {
CircularProgressIndicator()
}
is ImageAction.Success -> {
Image(
rememberImageSuccessPainter(action),
contentDescription = "image",
contentScale = contentScale,
modifier = Modifier.matchParentSize(),
)
}
is ImageAction.Failure -> {
Text(action.error.message ?: "Error")
}
}
else -> Unit
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion app/web/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ kotlin {
implementation(libs.moko.resources)
}
}
getByName("jsMain") {
jsMain {
// https://github.com/icerockdev/moko-resources/issues/531
dependsOn(commonMain.get())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class KotlinMultiplatformConventionPlugin : Plugin<Project> {
nodejs()
}
@OptIn(ExperimentalKotlinGradlePluginApi::class)
targetHierarchy.custom {
applyHierarchyTemplate {
common {
group("jvm") {
withAndroidTarget()
Expand All @@ -49,6 +49,14 @@ class KotlinMultiplatformConventionPlugin : Plugin<Project> {
}
}
}
targets.configureEach {
compilations.configureEach {
compilerOptions.configure {
// https://youtrack.jetbrains.com/issue/KT-61573
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
}
}
configKotlin()
}
Expand Down
1 change: 1 addition & 0 deletions image-loader/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ kotlin {
}
commonMain {
dependencies {
api(compose.foundation)
api(compose.ui)
api(libs.kotlinx.coroutines.core)
api(libs.okio)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build.VERSION.SDK_INT
import androidx.compose.ui.geometry.isSpecified
import com.seiko.imageloader.option.Options
import com.seiko.imageloader.option.androidContext
import com.seiko.imageloader.util.DEFAULT_MAX_PARALLELISM
Expand Down Expand Up @@ -118,7 +119,15 @@ class BitmapFactoryDecoder private constructor(
// EXIF transformations (but before sampling).
val srcWidth = if (exifData.isSwapped) outHeight else outWidth
val srcHeight = if (exifData.isSwapped) outWidth else outHeight
val (dstWidth, dstHeight) = calculateDstSize(srcWidth, srcHeight, options.maxImageSize)

val maxImageSize = if (options.size.isSpecified && !options.size.isEmpty()) {
minOf(options.size.width, options.size.height).toInt()
.coerceAtMost(options.maxImageSize)
} else {
options.maxImageSize
}
val (dstWidth, dstHeight) = calculateDstSize(srcWidth, srcHeight, maxImageSize)

// Calculate the image's sample size.
inSampleSize = calculateInSampleSize(
srcWidth = srcWidth,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ class SvgDecoder private constructor(

override suspend fun decode(): DecodeResult {
val svg = SVG.getFromInputStream(source.source.inputStream())
val requestSize = options.sizeResolver.run {
density.size()
}
return DecodeResult.OfPainter(
painter = SVGPainter(svg, density, requestSize),
painter = SVGPainter(svg, density, options.size),
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
package com.seiko.imageloader

import androidx.compose.runtime.Immutable
import androidx.compose.ui.geometry.isSpecified
import com.seiko.imageloader.intercept.InterceptorChainImpl
import com.seiko.imageloader.model.ImageAction
import com.seiko.imageloader.model.ImageEvent
import com.seiko.imageloader.model.ImageRequest
import com.seiko.imageloader.model.ImageResult
import com.seiko.imageloader.option.Options
import com.seiko.imageloader.util.ioDispatcher
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.transformLatest
import kotlin.coroutines.CoroutineContext

@Immutable
interface ImageLoader {
val config: ImageLoaderConfig

fun async(requestFlow: Flow<ImageRequest>): Flow<ImageAction>

fun async(request: ImageRequest): Flow<ImageAction> = async(flowOf(request))
fun async(request: ImageRequest): Flow<ImageAction>

companion object
}
Expand All @@ -40,21 +38,26 @@ private class RealImageLoader(
private val requestCoroutineContext: CoroutineContext,
override val config: ImageLoaderConfig,
) : ImageLoader {
@OptIn(ExperimentalCoroutinesApi::class)
override fun async(requestFlow: Flow<ImageRequest>) = requestFlow
.transformLatest { request ->
if (!request.skipEvent) {
emit(ImageEvent.Start)
}
val chain = InterceptorChainImpl(
initialRequest = request,
config = config,
flowCollector = this,
)
emit(chain.proceed(request))
}.catch {
if (it !is CancellationException) {
emit(ImageResult.OfError(it))
override fun async(request: ImageRequest) = flow {
if (!request.skipEvent) {
emit(ImageEvent.Start)
}
val initialSize = request.sizeResolver.size()
val options = Options(config.defaultOptions) {
if (initialSize.isSpecified) {
size = initialSize
}
}.flowOn(requestCoroutineContext)
}
val chain = InterceptorChainImpl(
initialRequest = request,
initialOptions = options,
config = config,
flowCollector = this,
)
emit(chain.proceed(request))
}.catch {
if (it !is CancellationException) {
emit(ImageResult.OfError(it))
}
}.flowOn(requestCoroutineContext)
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ fun rememberImagePainter(
request: ImageRequest,
imageLoader: ImageLoader = LocalImageLoader.current,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
placeholderPainter: (@Composable () -> Painter)? = request.placeholderPainter,
errorPainter: (@Composable () -> Painter)? = request.errorPainter,
placeholderPainter: (@Composable () -> Painter)? = null,
errorPainter: (@Composable () -> Painter)? = null,
): Painter {
val action by rememberImageAction(request, imageLoader)
return rememberImageActionPainter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.withFrameMillis
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.FilterQuality
Expand All @@ -17,27 +16,6 @@ import com.seiko.imageloader.model.ImageEvent
import com.seiko.imageloader.model.ImageRequest
import com.seiko.imageloader.model.ImageResult
import com.seiko.imageloader.util.AnimationPainter
import kotlinx.coroutines.flow.Flow

@Composable
fun rememberImageAction(
request: State<ImageRequest>,
imageLoader: ImageLoader = LocalImageLoader.current,
): State<ImageAction> {
return remember(request, imageLoader) {
imageLoader.async(snapshotFlow { request.value })
}.collectAsState(ImageEvent.Start)
}

@Composable
fun rememberImageAction(
request: Flow<ImageRequest>,
imageLoader: ImageLoader = LocalImageLoader.current,
): State<ImageAction> {
return remember(request, imageLoader) {
imageLoader.async(request)
}.collectAsState(ImageEvent.Start)
}

@Composable
fun rememberImageAction(
Expand Down Expand Up @@ -91,9 +69,9 @@ fun rememberImageSuccessPainter(
action.image.toPainter()
}
}.also { painter ->
if (painter is AnimationPainter) {
if (painter is AnimationPainter && painter.isPlay()) {
LaunchedEffect(painter) {
while (painter.isPlay()) {
while (painter.nextPlay()) {
withFrameMillis { frameTimeMillis ->
painter.update(frameTimeMillis)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import kotlinx.coroutines.flow.FlowCollector

internal class InterceptorChainHelper(
initialImageRequest: ImageRequest,
private val initialOptions: Options,
private val config: ImageLoaderConfig,
private val flowCollector: FlowCollector<ImageAction>,
) {
val logger get() = config.logger

private val interceptors by lazy {
val interceptors by lazy {
initialImageRequest.interceptors?.plus(config.interceptors.list)
?: config.interceptors.list
}
Expand All @@ -25,15 +26,11 @@ internal class InterceptorChainHelper(
}

fun getOptions(request: ImageRequest): Options {
return Options(config.defaultOptions) {
return Options(initialOptions) {
takeFrom(request)
}
}

fun getInterceptor(index: Int): Interceptor {
return interceptors[index]
}

suspend fun emit(action: ImageAction) {
flowCollector.emit(action)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ internal class InterceptorChainImpl(

constructor(
initialRequest: ImageRequest,
initialOptions: Options,
config: ImageLoaderConfig,
flowCollector: FlowCollector<ImageAction>,
) : this(
helper = InterceptorChainHelper(
initialImageRequest = initialRequest,
initialOptions = initialOptions,
config = config,
flowCollector = flowCollector,
),
Expand All @@ -36,7 +38,7 @@ internal class InterceptorChainImpl(
)

override suspend fun proceed(request: ImageRequest): ImageResult {
val interceptor = helper.getInterceptor(index)
val interceptor = helper.interceptors[index]
val chain = copy(index = index + 1, request = request)
return interceptor.intercept(chain)
}
Expand Down

0 comments on commit 601c1b8

Please sign in to comment.