Skip to content

Commit

Permalink
Add ContentScale argument to AsyncImagePainter. (#1144)
Browse files Browse the repository at this point in the history
* Add ContentScale argument to AsyncImagePainter.

* Docs.
  • Loading branch information
colinrtwhite authored Feb 15, 2022
1 parent c500014 commit f2be390
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 88 deletions.
4 changes: 2 additions & 2 deletions coil-compose-base/api/coil-compose-base.api
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ public final class coil/compose/AsyncImagePainter$State$Success : coil/compose/A
}

public final class coil/compose/AsyncImagePainterKt {
public static final fun rememberAsyncImagePainter-19ie5dc (Ljava/lang/Object;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter;
public static final fun rememberAsyncImagePainter-MqR-F_0 (Ljava/lang/Object;Lcoil/ImageLoader;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter;
public static final fun rememberAsyncImagePainter-3HmZ8SU (Ljava/lang/Object;Lcoil/ImageLoader;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/layout/ContentScale;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter;
public static final fun rememberAsyncImagePainter-5jETZwI (Ljava/lang/Object;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/layout/ContentScale;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter;
}

public final class coil/compose/ComposableSingletons$SubcomposeAsyncImageKt {
Expand Down
30 changes: 15 additions & 15 deletions coil-compose-base/src/main/java/coil/compose/AsyncImage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ import coil.size.Size as CoilSize
* onscreen.
* @param colorFilter Optional [ColorFilter] to apply for the [AsyncImagePainter] when it is
* rendered onscreen.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn
* into the destination.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn into the
* destination.
*/
@Composable
fun AsyncImage(
Expand Down Expand Up @@ -112,8 +112,8 @@ fun AsyncImage(
* onscreen.
* @param colorFilter Optional [ColorFilter] to apply for the [AsyncImagePainter] when it is
* rendered onscreen.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn
* into the destination.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn into the
* destination.
*/
@Composable
fun AsyncImage(
Expand All @@ -131,7 +131,9 @@ fun AsyncImage(
) {
// Create and execute the image request.
val request = updateRequest(requestOf(model), contentScale)
val painter = rememberAsyncImagePainter(request, imageLoader, transform, onState, filterQuality)
val painter = rememberAsyncImagePainter(
request, imageLoader, transform, onState, contentScale, filterQuality
)

// Draw the content without a parent composable or subcomposition.
val constraintsResolver = request.constraintsResolver
Expand Down Expand Up @@ -184,10 +186,14 @@ internal fun updateRequest(
contentScale: ContentScale,
) = request.newBuilder()
.apply {
val resolver = remember { ConstraintsResolver() }
resolver.scale = contentScale.toScale()
if (request.defined.sizeResolver == null) size(resolver)
if (request.defined.scaleResolver == null) scale(resolver)
if (request.defined.sizeResolver == null) {
val resolver = remember { ConstraintsResolver() }
size(resolver)
if (request.defined.scaleResolver == null) {
resolver.scale = contentScale.toScale()
scale(resolver)
}
}
}
.build()

Expand Down Expand Up @@ -255,9 +261,3 @@ private fun computeScale(constraints: Constraints, original: Scale): Scale {
return Scale.FIT
}
}

@Stable
private fun ContentScale.toScale() = when (this) {
ContentScale.Fit, ContentScale.Inside -> Scale.FIT
else -> Scale.FILL
}
58 changes: 36 additions & 22 deletions coil-compose-base/src/main/java/coil/compose/AsyncImagePainter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package coil.compose
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.Stable
Expand All @@ -26,7 +28,9 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.unit.Constraints
import coil.ImageLoader
import coil.compose.AsyncImagePainter.Companion.DefaultTransform
import coil.compose.AsyncImagePainter.State
Expand Down Expand Up @@ -58,10 +62,12 @@ import coil.size.Size as CoilSize
* **This is a lower-level API than [AsyncImage] and may not work as expected in all situations.**
* **It's highly recommended to use [AsyncImage] unless you need a reference to a [Painter].**
*
* Notably, [AsyncImagePainter] will not finish loading if [AsyncImagePainter.onDraw] is not called,
* which can occur for composables that don't have a fixed size (e.g. [LazyColumn]). Also
* [AsyncImagePainter.state] will not transition to [State.Success] synchronously during the
* composition phase.
* - [AsyncImagePainter] will not finish loading if [AsyncImagePainter.onDraw] is not called.
* This can occur if a composable has an unbounded (i.e. [Constraints.Infinity]) width/height
* constraint. For example, to use [AsyncImagePainter] with [LazyRow] or [LazyColumn], you must
* set a bounded width or height respectively.
* - [AsyncImagePainter.state] does not transition to [State.Success] synchronously during the
* composition phase. Use [SubcomposeAsyncImage] if you need this.
*
* @param model Either an [ImageRequest] or the [ImageRequest.data] value.
* @param imageLoader The [ImageLoader] that will be used to execute the request.
Expand All @@ -71,8 +77,11 @@ import coil.size.Size as CoilSize
* @param onLoading Called when the image request begins loading.
* @param onSuccess Called when the image request completes successfully.
* @param onError Called when the image request completes unsuccessfully.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn
* into the destination.
* @param contentScale Used to determine the aspect ratio scaling to be used if the canvas bounds
* are a different size from the intrinsic size of the image loaded by [model]. This should be set
* to the same value that's passed to [Image].
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn into the
* destination.
*/
@Composable
fun rememberAsyncImagePainter(
Expand All @@ -84,12 +93,14 @@ fun rememberAsyncImagePainter(
onLoading: ((State.Loading) -> Unit)? = null,
onSuccess: ((State.Success) -> Unit)? = null,
onError: ((State.Error) -> Unit)? = null,
contentScale: ContentScale = ContentScale.Fit,
filterQuality: FilterQuality = DefaultFilterQuality,
) = rememberAsyncImagePainter(
model = model,
imageLoader = imageLoader,
transform = transformOf(placeholder, error, fallback),
onState = onStateOf(onLoading, onSuccess, onError),
contentScale = contentScale,
filterQuality = filterQuality,
)

Expand All @@ -99,25 +110,31 @@ fun rememberAsyncImagePainter(
* **This is a lower-level API than [AsyncImage] and may not work as expected in all situations.**
* **It's highly recommended to use [AsyncImage] unless you need a reference to a [Painter].**
*
* Notably, [AsyncImagePainter] will not finish loading if [AsyncImagePainter.onDraw] is not called,
* which can occur for composables that don't have a fixed size (e.g. [LazyColumn]). Also
* [AsyncImagePainter.state] will not transition to [State.Success] synchronously during the
* composition phase.
* - [AsyncImagePainter] will not finish loading if [AsyncImagePainter.onDraw] is not called.
* This can occur if a composable has an unbounded (i.e. [Constraints.Infinity]) width/height
* constraint. For example, to use [AsyncImagePainter] with [LazyRow] or [LazyColumn], you must
* set a bounded width or height respectively.
* - [AsyncImagePainter.state] does not transition to [State.Success] synchronously during the
* composition phase. Use [SubcomposeAsyncImage] if you need this.
*
* @param model Either an [ImageRequest] or the [ImageRequest.data] value.
* @param imageLoader The [ImageLoader] that will be used to execute the request.
* @param transform A callback to transform a new [State] before it's applied to the
* [AsyncImagePainter]. Typically this is used to overwrite the state's [Painter].
* @param onState Called when the state of this painter changes.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn
* into the destination.
* @param contentScale Used to determine the aspect ratio scaling to be used if the canvas bounds
* are a different size from the intrinsic size of the image loaded by [model]. This should be set
* to the same value that's passed to [Image].
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn into the
* destination.
*/
@Composable
fun rememberAsyncImagePainter(
model: Any?,
imageLoader: ImageLoader,
transform: (State) -> State = DefaultTransform,
onState: ((State) -> Unit)? = null,
contentScale: ContentScale = ContentScale.Fit,
filterQuality: FilterQuality = DefaultFilterQuality,
): AsyncImagePainter {
val request = requestOf(model)
Expand All @@ -126,6 +143,7 @@ fun rememberAsyncImagePainter(
val painter = remember { AsyncImagePainter(request, imageLoader) }
painter.transform = transform
painter.onState = onState
painter.contentScale = contentScale
painter.filterQuality = filterQuality
painter.isPreview = LocalInspectionMode.current
painter.imageLoader = imageLoader
Expand Down Expand Up @@ -165,6 +183,7 @@ class AsyncImagePainter internal constructor(

internal var transform = DefaultTransform
internal var onState: ((State) -> Unit)? = null
internal var contentScale = ContentScale.Fit
internal var filterQuality = DefaultFilterQuality
internal var isPreview = false

Expand Down Expand Up @@ -254,6 +273,10 @@ class AsyncImagePainter internal constructor(
// If no other size resolver is set, suspend until the canvas size is positive.
size { drawSize.mapNotNull { it.toSizeOrNull() }.first() }
}
if (request.defined.scaleResolver == null) {
// If no other scale resolver is set, use the content scale.
scale(contentScale.toScale())
}
if (request.defined.precision != Precision.EXACT) {
// AsyncImagePainter scales the image to fit the canvas size at draw time.
precision(Precision.INEXACT)
Expand Down Expand Up @@ -291,19 +314,10 @@ class AsyncImagePainter internal constructor(
// a `CrossfadeTransformation`.
val transition = result.request.transitionFactory.create(FakeTransitionTarget, result)
if (transition is CrossfadeTransition) {
// Use the original scale inside of the scale that's used for determining the
// decode dimensions.
val scaleResolver = result.request.scaleResolver
val scale = if (scaleResolver is ConstraintsResolver) {
scaleResolver.scale
} else {
scaleResolver.scale()
}

return CrossfadePainter(
start = previous.painter.takeIf { previous is State.Loading },
end = current.painter,
scale = scale,
contentScale = contentScale,
durationMillis = transition.durationMillis,
fadeStart = result !is SuccessResult || !result.isPlaceholderCached,
preferExactIntrinsicSize = transition.preferExactIntrinsicSize
Expand Down
26 changes: 6 additions & 20 deletions coil-compose-base/src/main/java/coil/compose/CrossfadePainter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.inset
import androidx.compose.ui.graphics.painter.Painter
import coil.decode.DecodeUtils
import coil.size.Scale
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.times
import kotlin.math.max

/**
* A [Painter] that crossfades from [start] to [end].
*
* NOTE: The animation can only be executed once as the [start] drawable is
* dereferenced at the end of the transition.
* NOTE: The animation can only be executed once as the [start] drawable is dereferenced at
* the end of the transition.
*/
@Stable
internal class CrossfadePainter(
private var start: Painter?,
private val end: Painter?,
private val scale: Scale,
private val contentScale: ContentScale,
private val durationMillis: Int,
private val fadeStart: Boolean,
private val preferExactIntrinsicSize: Boolean,
Expand Down Expand Up @@ -119,23 +119,9 @@ internal class CrossfadePainter(
}
}

/** Scale the src size into the dst size preserving aspect ratio. */
private fun computeDrawSize(srcSize: Size, dstSize: Size): Size {
if (srcSize.isUnspecified || srcSize.isEmpty()) return dstSize
if (dstSize.isUnspecified || dstSize.isEmpty()) return dstSize

val srcWidth = srcSize.width
val srcHeight = srcSize.height
val multiplier = DecodeUtils.computeSizeMultiplier(
srcWidth = srcWidth,
srcHeight = srcHeight,
dstWidth = dstSize.width,
dstHeight = dstSize.height,
scale = scale
)
return Size(
width = multiplier * srcWidth,
height = multiplier * srcHeight
)
return srcSize * contentScale.computeScaleFactor(srcSize, dstSize)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ import coil.request.ImageRequest
* onscreen.
* @param colorFilter Optional [ColorFilter] to apply for the [AsyncImagePainter] when it is
* rendered onscreen.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn
* into the destination.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn into the
* destination.
*/
@Composable
fun SubcomposeAsyncImage(
Expand Down Expand Up @@ -97,8 +97,8 @@ fun SubcomposeAsyncImage(
* onscreen.
* @param colorFilter Optional [ColorFilter] to apply for the [AsyncImagePainter] when it is
* rendered onscreen.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn
* into the destination.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn into the
* destination.
* @param content A callback to draw the content inside an [SubcomposeAsyncImageScope].
*/
@Composable
Expand All @@ -118,7 +118,9 @@ fun SubcomposeAsyncImage(
) {
// Create and execute the image request.
val request = updateRequest(requestOf(model), contentScale)
val painter = rememberAsyncImagePainter(request, imageLoader, transform, onState, filterQuality)
val painter = rememberAsyncImagePainter(
request, imageLoader, transform, onState, contentScale, filterQuality
)

val constraintsResolver = request.constraintsResolver
if (constraintsResolver == null) {
Expand Down
8 changes: 8 additions & 0 deletions coil-compose-base/src/main/java/coil/compose/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import coil.compose.AsyncImagePainter.Companion.DefaultTransform
import coil.compose.AsyncImagePainter.State
import coil.request.ImageRequest
import coil.request.NullRequestDataException
import coil.size.Scale
import kotlin.math.roundToInt

/** Create an [ImageRequest] from the [model]. */
Expand Down Expand Up @@ -71,6 +73,12 @@ internal fun onStateOf(
}
}

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

internal val ImageRequest.constraintsResolver: ConstraintsResolver?
@SuppressLint("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
get() = sizeResolver as? ConstraintsResolver ?: scaleResolver as? ConstraintsResolver
Expand Down
4 changes: 2 additions & 2 deletions coil-compose-singleton/api/coil-compose-singleton.api
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ public final class coil/compose/SingletonAsyncImageKt {
}

public final class coil/compose/SingletonAsyncImagePainterKt {
public static final fun rememberAsyncImagePainter-5h-nEew (Ljava/lang/Object;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter;
public static final fun rememberAsyncImagePainter-DJqXB-Q (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter;
public static final fun rememberAsyncImagePainter-19ie5dc (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/layout/ContentScale;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter;
public static final fun rememberAsyncImagePainter-MqR-F_0 (Ljava/lang/Object;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/layout/ContentScale;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter;
}

public final class coil/compose/SingletonImagePainterKt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ import coil.request.ImageRequest
* onscreen.
* @param colorFilter Optional [ColorFilter] to apply for the [AsyncImagePainter] when it is
* rendered onscreen.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn
* into the destination.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn into the
* destination.
*/
@Composable
fun AsyncImage(
Expand Down Expand Up @@ -93,8 +93,8 @@ fun AsyncImage(
* onscreen.
* @param colorFilter Optional [ColorFilter] to apply for the [AsyncImagePainter] when it is
* rendered onscreen.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn
* into the destination.
* @param filterQuality Sampling algorithm applied to a bitmap when it is scaled and drawn into the
* destination.
*/
@Composable
fun AsyncImage(
Expand Down
Loading

0 comments on commit f2be390

Please sign in to comment.