From c37b7b072243bb92858bcc9cf8576cb9c02075ed Mon Sep 17 00:00:00 2001 From: Sam Judd Date: Sun, 2 Jul 2023 09:18:12 -0700 Subject: [PATCH] Add an experimental rememberGlidePainter API --- .../glide/benchmark/BenchmarkFromCache.java | 9 +- .../com/bumptech/glide/MultiRequestTest.java | 16 +- integration/compose/api/compose.api | 57 +++ .../integration/compose/GlideImageTest.kt | 10 +- .../glide/integration/compose/GlideImage.kt | 33 +- .../glide/integration/compose/GlidePainter.kt | 373 ++++++++++++++++-- .../glide/integration/compose/Sizes.kt | 20 + .../integration/concurrent/GlideFutures.java | 4 +- integration/ktx/api/ktx.api | 11 +- .../bumptech/glide/integration/ktx/Flows.kt | 42 +- .../glide/integration/ktx/FlowsTest.kt | 16 +- .../glide/request/RequestFutureTarget.java | 4 +- .../glide/request/RequestListener.java | 20 +- .../java/com/bumptech/glide/GlideTest.java | 8 +- .../bumptech/glide/RequestBuilderTest.java | 17 +- .../glide/request/SingleRequestTest.java | 16 +- .../samples/giphy/FullscreenActivity.java | 9 +- .../samples/svg/SvgSoftwareLayerSetter.java | 9 +- 18 files changed, 540 insertions(+), 134 deletions(-) diff --git a/benchmark/src/androidTest/java/com/bumptech/glide/benchmark/BenchmarkFromCache.java b/benchmark/src/androidTest/java/com/bumptech/glide/benchmark/BenchmarkFromCache.java index 9b33ebd158..e6b5b94dfe 100644 --- a/benchmark/src/androidTest/java/com/bumptech/glide/benchmark/BenchmarkFromCache.java +++ b/benchmark/src/androidTest/java/com/bumptech/glide/benchmark/BenchmarkFromCache.java @@ -2,6 +2,7 @@ import android.app.Application; import android.graphics.Bitmap; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import androidx.benchmark.BenchmarkState; @@ -111,17 +112,17 @@ private void loadImageWithExpectedDataSource( public boolean onLoadFailed( @Nullable GlideException e, Object model, - Target target, + @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady( - Bitmap resource, - Object model, + @NonNull Bitmap resource, + @NonNull Object model, Target target, - DataSource dataSource, + @NonNull DataSource dataSource, boolean isFirstResource) { dataSourceRef.set(dataSource); return false; diff --git a/instrumentation/src/androidTest/java/com/bumptech/glide/MultiRequestTest.java b/instrumentation/src/androidTest/java/com/bumptech/glide/MultiRequestTest.java index b04cc220fe..cd5948b8ad 100644 --- a/instrumentation/src/androidTest/java/com/bumptech/glide/MultiRequestTest.java +++ b/instrumentation/src/androidTest/java/com/bumptech/glide/MultiRequestTest.java @@ -68,17 +68,17 @@ public void thumbnail_onResourceReady_forPrimary_isComplete_whenRequestListenerI public boolean onLoadFailed( @Nullable GlideException e, Object model, - Target target, + @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady( - Drawable resource, - Object model, + @NonNull Drawable resource, + @NonNull Object model, Target target, - DataSource dataSource, + @NonNull DataSource dataSource, boolean isFirstResource) { isPrimaryRequestComplete.set(target.getRequest().isComplete()); countDownLatch.countDown(); @@ -115,7 +115,7 @@ public void thumbnail_onLoadFailed_forPrimary_isNotRunningOrComplete_whenRequest public boolean onLoadFailed( @Nullable GlideException e, Object model, - Target target, + @NonNull Target target, boolean isFirstResource) { Request request = target.getRequest(); isNeitherRunningNorComplete.set(!request.isComplete() && !request.isRunning()); @@ -125,10 +125,10 @@ public boolean onLoadFailed( @Override public boolean onResourceReady( - Drawable resource, - Object model, + @NonNull Drawable resource, + @NonNull Object model, Target target, - DataSource dataSource, + @NonNull DataSource dataSource, boolean isFirstResource) { return false; } diff --git a/integration/compose/api/compose.api b/integration/compose/api/compose.api index 5185e1f008..c24da5f993 100644 --- a/integration/compose/api/compose.api +++ b/integration/compose/api/compose.api @@ -1,3 +1,12 @@ +public final class com/bumptech/glide/integration/compose/AnimationState : java/lang/Enum { + public static final field Animate Lcom/bumptech/glide/integration/compose/AnimationState; + public static final field Failed Lcom/bumptech/glide/integration/compose/AnimationState; + public static final field Loading Lcom/bumptech/glide/integration/compose/AnimationState; + public static final field Success Lcom/bumptech/glide/integration/compose/AnimationState; + public static fun valueOf (Ljava/lang/String;)Lcom/bumptech/glide/integration/compose/AnimationState; + public static fun values ()[Lcom/bumptech/glide/integration/compose/AnimationState; +} + public abstract interface annotation class com/bumptech/glide/integration/compose/ExperimentalGlideComposeApi : java/lang/annotation/Annotation { } @@ -8,6 +17,54 @@ public final class com/bumptech/glide/integration/compose/GlideImageKt { public static final fun placeholder (Lkotlin/jvm/functions/Function2;)Lcom/bumptech/glide/integration/compose/Placeholder; } +public final class com/bumptech/glide/integration/compose/GlidePainterKt { + public static final fun animate (Lcom/bumptech/glide/integration/compose/GlidePainterState;)Lcom/bumptech/glide/integration/compose/AnimationState; + public static final fun rememberGlidePainter (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Lcom/bumptech/glide/integration/compose/GlidePainterStateAndModifier; + public static final fun rememberGlidePainter (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Lkotlin/Pair; +} + +public abstract class com/bumptech/glide/integration/compose/GlidePainterState { + public static final field $stable I +} + +public final class com/bumptech/glide/integration/compose/GlidePainterState$Failure : com/bumptech/glide/integration/compose/GlidePainterState { + public static final field $stable I + public static final field INSTANCE Lcom/bumptech/glide/integration/compose/GlidePainterState$Failure; +} + +public final class com/bumptech/glide/integration/compose/GlidePainterState$Loading : com/bumptech/glide/integration/compose/GlidePainterState { + public static final field $stable I + public static final field INSTANCE Lcom/bumptech/glide/integration/compose/GlidePainterState$Loading; +} + +public final class com/bumptech/glide/integration/compose/GlidePainterState$Success : com/bumptech/glide/integration/compose/GlidePainterState { + public static final field $stable I + public fun (Lcom/bumptech/glide/load/DataSource;)V + public final fun component1 ()Lcom/bumptech/glide/load/DataSource; + public final fun copy (Lcom/bumptech/glide/load/DataSource;)Lcom/bumptech/glide/integration/compose/GlidePainterState$Success; + public static synthetic fun copy$default (Lcom/bumptech/glide/integration/compose/GlidePainterState$Success;Lcom/bumptech/glide/load/DataSource;ILjava/lang/Object;)Lcom/bumptech/glide/integration/compose/GlidePainterState$Success; + public fun equals (Ljava/lang/Object;)Z + public final fun getDataSource ()Lcom/bumptech/glide/load/DataSource; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/bumptech/glide/integration/compose/GlidePainterStateAndModifier { + public static final field $stable I + public fun (Landroidx/compose/ui/graphics/painter/Painter;Lcom/bumptech/glide/integration/compose/GlidePainterState;Landroidx/compose/ui/Modifier;)V + public final fun component1 ()Landroidx/compose/ui/graphics/painter/Painter; + public final fun component2 ()Lcom/bumptech/glide/integration/compose/GlidePainterState; + public final fun component3 ()Landroidx/compose/ui/Modifier; + public final fun copy (Landroidx/compose/ui/graphics/painter/Painter;Lcom/bumptech/glide/integration/compose/GlidePainterState;Landroidx/compose/ui/Modifier;)Lcom/bumptech/glide/integration/compose/GlidePainterStateAndModifier; + public static synthetic fun copy$default (Lcom/bumptech/glide/integration/compose/GlidePainterStateAndModifier;Landroidx/compose/ui/graphics/painter/Painter;Lcom/bumptech/glide/integration/compose/GlidePainterState;Landroidx/compose/ui/Modifier;ILjava/lang/Object;)Lcom/bumptech/glide/integration/compose/GlidePainterStateAndModifier; + public fun equals (Ljava/lang/Object;)Z + public final fun getModifier ()Landroidx/compose/ui/Modifier; + public final fun getPainter ()Landroidx/compose/ui/graphics/painter/Painter; + public final fun getState ()Lcom/bumptech/glide/integration/compose/GlidePainterState; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public abstract interface class com/bumptech/glide/integration/compose/GlidePreloadingData { public abstract fun get (ILandroidx/compose/runtime/Composer;I)Lkotlin/Pair; public abstract fun getSize ()I diff --git a/integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/GlideImageTest.kt b/integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/GlideImageTest.kt index 2080fa7cfa..3bb6341148 100644 --- a/integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/GlideImageTest.kt +++ b/integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/GlideImageTest.kt @@ -295,17 +295,17 @@ class GlideImageTest { override fun onLoadFailed( e: GlideException?, model: Any?, - target: Target?, + target: Target, isFirstResource: Boolean, ): Boolean { throw UnsupportedOperationException() } override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, + resource: Drawable, + model: Any, + target: Target, + dataSource: DataSource, isFirstResource: Boolean, ): Boolean { onResourceReadyCounter.incrementAndGet() diff --git a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlideImage.kt b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlideImage.kt index d6c9a6726c..06052094dc 100644 --- a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlideImage.kt +++ b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlideImage.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -22,12 +21,10 @@ import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestManager import com.bumptech.glide.integration.ktx.AsyncGlideSize -import com.bumptech.glide.integration.ktx.ExperimentGlideFlows import com.bumptech.glide.integration.ktx.ImmediateGlideSize import com.bumptech.glide.integration.ktx.InternalGlideApi import com.bumptech.glide.integration.ktx.ResolvableGlideSize import com.bumptech.glide.integration.ktx.Size -import com.bumptech.glide.integration.ktx.Status import com.google.accompanist.drawablepainter.rememberDrawablePainter /** Mutates and returns the given [RequestBuilder] to apply relevant options. */ @@ -129,6 +126,7 @@ public fun GlideImage( ) } + @OptIn(ExperimentalGlideComposeApi::class) @Composable private fun PreviewResourceOrDrawable( @@ -229,7 +227,7 @@ public sealed class Placeholder { @OptIn(InternalGlideApi::class) @Composable -private fun rememberResolvableSize( +internal fun rememberResolvableSize( overrideSize: Size?, ) = remember(overrideSize) { @@ -269,7 +267,7 @@ private fun RequestBuilder.contentScaleTransform( // TODO(judds): Think about how to handle the various fills } -@OptIn(InternalGlideApi::class, ExperimentGlideFlows::class) +@OptIn(InternalGlideApi::class, ExperimentalGlideComposeApi::class) @Composable private fun SizedGlideImage( requestBuilder: RequestBuilder, @@ -290,10 +288,11 @@ private fun SizedGlideImage( rememberGlidePainter( requestBuilder = requestBuilder, size = size, + resolveSize = true ) - if (placeholder != null && painter.status.showPlaceholder()) { + if (placeholder != null && (painter.state is GlidePainterState.Loading)) { placeholder.boxed() - } else if (failure != null && painter.status == Status.FAILED) { + } else if (failure != null && painter.state is GlidePainterState.Failure) { failure.boxed() } else { Image( @@ -308,26 +307,6 @@ private fun SizedGlideImage( } } -@OptIn(ExperimentGlideFlows::class) -private fun Status.showPlaceholder(): Boolean = - when (this) { - Status.RUNNING -> true - Status.CLEARED -> true - else -> false - } - -@OptIn(InternalGlideApi::class) -@Composable -private fun rememberGlidePainter( - requestBuilder: RequestBuilder, - size: ResolvableGlideSize, -): GlidePainter { - val scope = rememberCoroutineScope() - // TODO(judds): Calling onRemembered here manually might make a minor improvement in how quickly - // the image load is started, but it also triggers a recomposition. I can't figure out why it - // triggers a recomposition - return remember(requestBuilder, size) { GlidePainter(requestBuilder, size, scope) } -} internal val DisplayedDrawableKey = SemanticsPropertyKey>("DisplayedDrawable") diff --git a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlidePainter.kt b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlidePainter.kt index adb81d1568..dd5d40663b 100644 --- a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlidePainter.kt +++ b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlidePainter.kt @@ -3,12 +3,16 @@ package com.bumptech.glide.integration.compose import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.RememberObserver import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter @@ -18,7 +22,11 @@ import androidx.compose.ui.graphics.drawscope.DrawScope 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.layout.layout +import androidx.compose.ui.platform.LocalContext +import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.RequestManager import com.bumptech.glide.integration.ktx.AsyncGlideSize import com.bumptech.glide.integration.ktx.ExperimentGlideFlows import com.bumptech.glide.integration.ktx.ImmediateGlideSize @@ -28,6 +36,7 @@ import com.bumptech.glide.integration.ktx.ResolvableGlideSize import com.bumptech.glide.integration.ktx.Resource import com.bumptech.glide.integration.ktx.Status import com.bumptech.glide.integration.ktx.flowResolvable +import com.bumptech.glide.load.DataSource import com.google.accompanist.drawablepainter.DrawablePainter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -36,54 +45,345 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlinx.coroutines.plus +import java.lang.IllegalStateException + +/** + * Exposes a [Painter] and the painter's [GlidePainterState] so that they can be composed with other + * Composables like [androidx.compose.foundation.Image]. + * + * You can use this class to display custom placeholders and/or animations. For example to crossfade + * from a placeholder to a real image, you might do something like this: + * + * ``` + * val (painter, state) = rememberGlidePainter(model = item.uri) { + * it.override(targetImageSize) + * } + * when (state.animate()) { + * AnimationState.Loading, AnimationState.Animate -> { + * Crossfade(state == AnimationState.Loading) { + * if (it) { + * Image( + * painterResource(android.R.drawable.star_big_on), + * contentDescription, + * modifier, + * ) + * } else { + * Image(painter, contentDescription, modifier) + * } + * } + * } + * AnimationState.Success -> { + * Image(painter, contentDescription, modifier) + * } + * AnimationState.Failed -> { + * Image( + * painterResource(android.R.drawable.star_big_off), + * contentDescription, + * modifier, + * ) + * } + * } + * ``` + * + * [animate] is a helper that uses [AnimationState.Animate] to indicate that an + * animation should be performed because the [DataSource] is not [DataSource.MEMORY_CACHE], or + * [AnimationState.Success] to indicate that the animation should be skipped. It's derived from + * [GlidePainterState] so you can implement your own version if your requirements differ, or skip it + * entirely. + * + * Unlike [GlideImage], the painter cannot automatically determine its size. If no override size + * (by calling [RequestBuilder.override] in [requestBuilderTransform]) is provided, the painter will + * wait until it is drawn to start the image load. This can lead to the image load never starting. + * To ensure the painter is always able to determine a size and start the image load: + * + * 1. Use the other variant of [rememberGlidePainter] by providing a [Modifier] and using the + * [GlidePainterStateAndModifier.modifier] returned by that method. + * 2. Ensure that the painter is always drawn (unlike in the example above) in another Composable + * (e.g. [androidx.compose.foundation.Image] with a reasonable [Modifier]. + * 3. Using [requestBuilderTransform] to apply a specific [RequestBuilder.override] size to the + * Glide request directly. If you choose this option, try to pick an override size based on the area + * you want to display the image in or at least the screen size rather than using + * [com.bumptech.glide.request.target.Target.SIZE_ORIGINAL]. + */ +@ExperimentalGlideComposeApi +@OptIn(InternalGlideApi::class) +@Composable +public fun rememberGlidePainter( + model: Any?, + requestBuilderTransform: RequestBuilderTransform = { it }, +): Pair { + val requestManager: RequestManager = LocalContext.current.let { remember(it) { Glide.with(it) } } + + val requestBuilder = remember(model, requestManager, requestBuilderTransform) { + requestBuilderTransform(requestManager.load(model)) + } + + val overrideSize = requestBuilder.overrideSize() + val size = rememberResolvableSize(overrideSize) + val painter = rememberGlidePainter( + requestBuilder = requestBuilder, + size = size, + resolveSize = true, + ) + return Pair(painter, painter.state) +} + +@ExperimentalGlideComposeApi +public data class GlidePainterStateAndModifier( + val painter: Painter, + val state: GlidePainterState, + val modifier: Modifier, +) + +/** + * Similar to the other variant of [rememberGlidePainter] except that this method uses the given + * [modifier] to determine the size for the image load. See the documentation on the other variant + * for details. + * + * This method returns an updated [GlidePainterStateAndModifier.modifier] which must be applied to + * some other Composable whose layout size is the size of the image you want. For example, using + * this method and the returned `modifier` allows you avoid having to specify an `override()` size: + * + * ``` + * val (painter, state, updatedModifier) = rememberGlidePainter(model = item.uri, modifier) + * when (state.animate()) { + * AnimationState.Loading, AnimationState.Animate -> { + * Crossfade(state == AnimationState.Loading) { + * if (it) { + * Image( + * painterResource(android.R.drawable.star_big_on), + * contentDescription, + * updatedModifier, + * ) + * } else { + * Image(painter, contentDescription, updatedModifier) + * } + * } + * } + * AnimationState.Success -> { + * Image(painter, contentDescription, updatedModifier) + * } + * AnimationState.Failed -> { + * Image( + * painterResource(android.R.drawable.star_big_off), + * contentDescription, + * updatedModifier, + * ) + * } + * } + * ``` + * + * If you're using fixed size buckets across the app, or have some other reason to use + * [RequestBuilder.override], then this method is not helpful and you should use the other + * [rememberGlidePainter] variant instead. If an override size is set, this method will throw. + * + * Otherwise, using this method can improve efficiency by ensuring you load an image whose size + * matches the layout it's displayed in. In addition, this variant will always resolve a size and + * be able to start an image load if the the returned [GlidePainterStateAndModifier.modifier] is + * used and has a reasonable layout size. + * + * @throws IllegalArgumentException if [requestBuilderTransform] sets an [RequestBuilder.override] + * size. + */ +@ExperimentalGlideComposeApi +@OptIn(InternalGlideApi::class) +@Composable +public fun rememberGlidePainter( + model: Any?, + modifier: Modifier, + requestBuilderTransform: RequestBuilderTransform = { it }, +): GlidePainterStateAndModifier { + val requestManager: RequestManager = LocalContext.current.let { remember(it) { Glide.with(it) } } + + val requestBuilder = remember(model, requestManager, requestBuilderTransform) { + requestBuilderTransform(requestManager.load(model)) + } + if (requestBuilder.overrideSize() != null) { + throw IllegalArgumentException( + "Using rememberGlidePainterAndSize with a fixed override size " + + "(${requestBuilder.overrideSize()}) is redundant, use rememberGlidePainter instead") + } + + val (size, updatedModifier) = rememberSizeAndModifier(modifier) + val painter = rememberGlidePainter( + requestBuilder = requestBuilder, + size = size, + // Let the Modifier resolve the size + resolveSize = false, + ) + return GlidePainterStateAndModifier(painter, painter.state, updatedModifier) +} +@OptIn(InternalGlideApi::class) +@Composable +private fun rememberSizeAndModifier( + modifier: Modifier, +) = + remember(modifier) { + val size = AsyncGlideSize() + Pair( + size, + modifier.sizeObservingModifier(size) + ) + } + +@OptIn(InternalGlideApi::class) +private fun Modifier.sizeObservingModifier(size: AsyncGlideSize): Modifier = + this.layout { measurable, constraints -> + val inferredSize = constraints.inferredGlideSize() + if (inferredSize != null) { + size.setSize(inferredSize) + } + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { placeable.place(0, 0) } + } + +@OptIn(InternalGlideApi::class, ExperimentalGlideComposeApi::class) +@Composable +internal fun rememberGlidePainter( + requestBuilder: RequestBuilder, + size: ResolvableGlideSize, + resolveSize: Boolean, +): GlidePainter { + val scope = rememberCoroutineScope() + val result = remember(requestBuilder, size) { GlidePainter(requestBuilder, size, resolveSize, scope) } + result.onRemembered() + return result +} + +/** + * A helper enum that translates [GlidePainterState] into something easier to use with custom + * animations. See [animate]. + */ +@ExperimentalGlideComposeApi +public enum class AnimationState { + Loading, + /** + * The request has finished successfully and should animate. + */ + Animate, + /** + * The request has finished successfully but should not animate because it was loaded from + * [DataSource.MEMORY_CACHE]. + */ + Success, + Failed; +} + +/** + * An optional helper that simplifies writing animations with [GlidePainterState] by using + * [AnimationState.Animate] and [AnimationState.Success] to distinguish when an animation should + * occur. + * + * If you use thumbnails, this API will return [AnimationState.Animate] or [AnimationState.Success] + * based on the data source of the first image to be loaded. If the first image to be loaded is + * from the memory cache, this method will always return [AnimationState.Animate]. Otherwise it will + * always return [AnimationState.Success]. This behavior is based on [GlidePainterState.Success]. + */ +@ExperimentalGlideComposeApi +public fun GlidePainterState.animate(): AnimationState = + when (this) { + is GlidePainterState.Failure -> AnimationState.Failed + is GlidePainterState.Loading -> AnimationState.Loading + is GlidePainterState.Success -> + if (dataSource != DataSource.MEMORY_CACHE) { + AnimationState.Animate + } else { + AnimationState.Success + } + } + +/** + * The current state of a request associated with a Glide painter. + * + * This state is a bit of a simplification over Glide's real state. In particular [Success] is + * used in any case where we have an image, even if that image is the thumbnail of a full request + * where the full request has failed. From the point of view of the UI this is usually reasonable + * and a significant simplification of this API. + */ +@ExperimentalGlideComposeApi +public sealed class GlidePainterState { + + @ExperimentalGlideComposeApi + public object Loading : GlidePainterState() + + /** + * Indicates the load finished successfully (or at least one thumbnail was loaded, see the details + * on [GlidePainterState]). + * + * @param dataSource The data source the latest image was loaded from. If your request uses one + * or more thumbnails this value may change as each successive thumbnail is loaded. + */ + @ExperimentalGlideComposeApi + public data class Success( + val dataSource: DataSource, + ) : GlidePainterState() + @ExperimentalGlideComposeApi + public object Failure : GlidePainterState() +} // This class is inspired by a similar implementation in the excellent Coil library // (https://github.com/coil-kt/coil), specifically: // https://github.com/coil-kt/coil/blob/main/coil-compose-base/src/main/java/coil/compose/AsyncImagePainter.kt @Stable +@ExperimentalGlideComposeApi internal class GlidePainter @OptIn(InternalGlideApi::class) constructor( private val requestBuilder: RequestBuilder, private val resolvableSize: ResolvableGlideSize, + private val resolveSize: Boolean, scope: CoroutineScope, ) : Painter(), RememberObserver { - @OptIn(ExperimentGlideFlows::class) internal var status: Status by mutableStateOf(Status.CLEARED) + private var _state: GlidePainterState = GlidePainterState.Loading + set(value) { + field = value + state = value + } + private var _painter: Painter? = null + set(value) { + field = value + painter = value + } + internal var state: GlidePainterState by mutableStateOf(GlidePainterState.Loading) + private set + private var painter: Painter? by mutableStateOf(null) internal val currentDrawable: MutableState = mutableStateOf(null) + private var alpha: Float by mutableStateOf(DefaultAlpha) private var colorFilter: ColorFilter? by mutableStateOf(null) - private var delegate: Painter? by mutableStateOf(null) private val scope = scope + SupervisorJob(parent = scope.coroutineContext.job) + Dispatchers.Main.immediate private var currentJob: Job? = null override val intrinsicSize: Size - get() = delegate?.intrinsicSize ?: Size.Unspecified + get() = _painter?.intrinsicSize ?: Size.Unspecified @OptIn(InternalGlideApi::class) override fun DrawScope.onDraw() { - when (resolvableSize) { - is AsyncGlideSize -> { - size.toGlideSize()?.let { resolvableSize.setSize(it) } + if (resolveSize) { + when (resolvableSize) { + is AsyncGlideSize -> { + size.toGlideSize()?.let { resolvableSize.setSize(it) } + } + // Do nothing. + is ImmediateGlideSize -> {} } - // Do nothing. - is ImmediateGlideSize -> {} } - delegate?.apply { draw(size, alpha, colorFilter) } + painter?.apply { draw(size, alpha, colorFilter) } } override fun onAbandoned() { - (delegate as? RememberObserver)?.onAbandoned() + (_painter as? RememberObserver)?.onAbandoned() } override fun onForgotten() { - (delegate as? RememberObserver)?.onForgotten() + (_painter as? RememberObserver)?.onForgotten() currentJob?.cancel() currentJob = null } override fun onRemembered() { - (delegate as? RememberObserver)?.onRemembered() + (_painter as? RememberObserver)?.onRemembered() if (currentJob == null) { currentJob = launchRequest() } @@ -92,13 +392,23 @@ constructor( @OptIn(ExperimentGlideFlows::class, InternalGlideApi::class) private fun launchRequest() = this.scope.launch { requestBuilder.flowResolvable(resolvableSize).collect { - updateDelegate( - when (it) { - is Resource -> it.resource - is Placeholder -> it.placeholder + val (glidePainterState, painter) = when (it) { + is Resource -> { + currentDrawable.value = it.resource + Pair(GlidePainterState.Success(it.dataSource), it.resource.toPainter()) } - ) - status = it.status + is Placeholder -> { + currentDrawable.value = it.placeholder + val painter = it.placeholder?.toPainter() + when (it.status) { + Status.CLEARED -> Pair(GlidePainterState.Loading, painter) + Status.RUNNING -> Pair(GlidePainterState.Loading, painter) + Status.FAILED -> Pair(GlidePainterState.Failure, painter) + Status.SUCCEEDED -> throw IllegalStateException() + } + } + } + updateState(glidePainterState, painter) } } @@ -109,14 +419,23 @@ constructor( else -> DrawablePainter(mutate()) } - private fun updateDelegate(drawable: Drawable?) { - val newDelegate = drawable?.toPainter() - val oldDelegate = delegate - if (newDelegate !== oldDelegate) { - (oldDelegate as? RememberObserver)?.onForgotten() - (newDelegate as? RememberObserver)?.onRemembered() - currentDrawable.value = drawable - delegate = newDelegate + private fun updateState(glidePainterState: GlidePainterState, painter: Painter?) { + val previous = _painter + + if (previous !== painter) { + (previous as? RememberObserver)?.onForgotten() + (painter as? RememberObserver)?.onRemembered() + } + _painter = painter + + // Avoid updating the DataSource for multiple successful Glide loads (ie when using thumbnails). + // This makes the API a bit less flexible, but avoids recompositions when only the data source + // changes. + if (glidePainterState is GlidePainterState.Success && _state is GlidePainterState.Success) { + return + } + if (glidePainterState != _state) { + _state = glidePainterState } } @@ -129,4 +448,4 @@ constructor( this.colorFilter = colorFilter return true } -} +} \ No newline at end of file diff --git a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Sizes.kt b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Sizes.kt index c59550f865..0a37c1b3f1 100644 --- a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Sizes.kt +++ b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Sizes.kt @@ -2,6 +2,7 @@ package com.bumptech.glide.integration.compose +import androidx.compose.ui.unit.Constraints import com.bumptech.glide.RequestBuilder import com.bumptech.glide.integration.ktx.InternalGlideApi import com.bumptech.glide.integration.ktx.Size @@ -39,3 +40,22 @@ internal fun androidx.compose.ui.geometry.Size.toGlideSize(): Size? { } return Size(width, height); } + +internal fun Constraints.inferredGlideSize(): Size? { + val width = + if (hasBoundedWidth) { + maxWidth + } else { + com.bumptech.glide.request.target.Target.SIZE_ORIGINAL + } + val height = + if (hasBoundedHeight) { + maxHeight + } else { + com.bumptech.glide.request.target.Target.SIZE_ORIGINAL + } + if (!width.isValidGlideDimension() || !height.isValidGlideDimension()) { + return null + } + return Size(width, height) +} diff --git a/integration/concurrent/src/main/java/com/bumptech/glide/integration/concurrent/GlideFutures.java b/integration/concurrent/src/main/java/com/bumptech/glide/integration/concurrent/GlideFutures.java index 2557330b94..bec2bf37e7 100644 --- a/integration/concurrent/src/main/java/com/bumptech/glide/integration/concurrent/GlideFutures.java +++ b/integration/concurrent/src/main/java/com/bumptech/glide/integration/concurrent/GlideFutures.java @@ -181,14 +181,14 @@ private static final class GlideLoadingListener implements RequestListener @Override public boolean onLoadFailed( - @Nullable GlideException e, Object model, Target target, boolean isFirst) { + @Nullable GlideException e, Object model, @NonNull Target target, boolean isFirst) { completer.setException(e != null ? e : new RuntimeException("Unknown error")); return true; } @Override public boolean onResourceReady( - T resource, Object model, Target target, DataSource dataSource, boolean isFirst) { + @NonNull T resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirst) { try { completer.set(new TargetAndResult<>(target, resource)); } catch (Throwable t) { diff --git a/integration/ktx/api/ktx.api b/integration/ktx/api/ktx.api index 4e3d08762d..9471ef2a96 100644 --- a/integration/ktx/api/ktx.api +++ b/integration/ktx/api/ktx.api @@ -28,15 +28,20 @@ public final class com/bumptech/glide/integration/ktx/Placeholder : com/bumptech } public final class com/bumptech/glide/integration/ktx/Resource : com/bumptech/glide/integration/ktx/GlideFlowInstant { - public fun (Lcom/bumptech/glide/integration/ktx/Status;Ljava/lang/Object;)V + public fun (Lcom/bumptech/glide/integration/ktx/Status;Ljava/lang/Object;ZLcom/bumptech/glide/load/DataSource;)V + public final fun asFailure ()Lcom/bumptech/glide/integration/ktx/Resource; public final fun component1 ()Lcom/bumptech/glide/integration/ktx/Status; public final fun component2 ()Ljava/lang/Object; - public final fun copy (Lcom/bumptech/glide/integration/ktx/Status;Ljava/lang/Object;)Lcom/bumptech/glide/integration/ktx/Resource; - public static synthetic fun copy$default (Lcom/bumptech/glide/integration/ktx/Resource;Lcom/bumptech/glide/integration/ktx/Status;Ljava/lang/Object;ILjava/lang/Object;)Lcom/bumptech/glide/integration/ktx/Resource; + public final fun component3 ()Z + public final fun component4 ()Lcom/bumptech/glide/load/DataSource; + public final fun copy (Lcom/bumptech/glide/integration/ktx/Status;Ljava/lang/Object;ZLcom/bumptech/glide/load/DataSource;)Lcom/bumptech/glide/integration/ktx/Resource; + public static synthetic fun copy$default (Lcom/bumptech/glide/integration/ktx/Resource;Lcom/bumptech/glide/integration/ktx/Status;Ljava/lang/Object;ZLcom/bumptech/glide/load/DataSource;ILjava/lang/Object;)Lcom/bumptech/glide/integration/ktx/Resource; public fun equals (Ljava/lang/Object;)Z + public final fun getDataSource ()Lcom/bumptech/glide/load/DataSource; public final fun getResource ()Ljava/lang/Object; public fun getStatus ()Lcom/bumptech/glide/integration/ktx/Status; public fun hashCode ()I + public final fun isFirstResource ()Z public fun toString ()Ljava/lang/String; } diff --git a/integration/ktx/src/main/java/com/bumptech/glide/integration/ktx/Flows.kt b/integration/ktx/src/main/java/com/bumptech/glide/integration/ktx/Flows.kt index a0ce7afa76..e048188968 100644 --- a/integration/ktx/src/main/java/com/bumptech/glide/integration/ktx/Flows.kt +++ b/integration/ktx/src/main/java/com/bumptech/glide/integration/ktx/Flows.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.launch +import java.lang.UnsupportedOperationException @RequiresOptIn( level = RequiresOptIn.Level.ERROR, @@ -201,6 +202,8 @@ public data class Placeholder( public data class Resource( public override val status: Status, public val resource: ResourceT, + public val isFirstResource: Boolean, + public val dataSource: DataSource, ) : GlideFlowInstant() { init { require( @@ -215,6 +218,9 @@ public data class Resource( } ) } + + public fun asFailure():Resource = + Resource(Status.FAILED, resource, isFirstResource, dataSource) } @InternalGlideApi @@ -264,7 +270,7 @@ private class FlowTarget( ) : Target, RequestListener { @Volatile private var resolvedSize: Size? = null @Volatile private var currentRequest: Request? = null - @Volatile private var lastResource: ResourceT? = null + @Volatile private var lastResource: Resource? = null @GuardedBy("this") private val sizeReadyCallbacks = mutableListOf() @@ -306,15 +312,8 @@ private class FlowTarget( } override fun onResourceReady(resource: ResourceT, transition: Transition?) { - lastResource = resource - scope.trySend( - Resource( - // currentRequest is the entire request state, so we can use it to figure out if this - // resource is from a thumbnail request (isComplete is false) or the primary request. - if (currentRequest?.isComplete == true) Status.SUCCEEDED else Status.RUNNING, - resource - ) - ) + throw UnsupportedOperationException() + } override fun onLoadCleared(placeholder: Drawable?) { @@ -354,25 +353,36 @@ private class FlowTarget( override fun onLoadFailed( e: GlideException?, model: Any?, - target: Target?, + target: Target, isFirstResource: Boolean, ): Boolean { val localLastResource = lastResource val localRequest = currentRequest if (localLastResource != null && localRequest?.isComplete == false && !localRequest.isRunning) { - scope.channel.trySend(Resource(Status.FAILED, localLastResource)) + scope.channel.trySend(localLastResource.asFailure()) } return false } override fun onResourceReady( resource: ResourceT, - model: Any?, - target: Target?, - dataSource: DataSource?, + model: Any, + target: Target, + dataSource: DataSource, isFirstResource: Boolean, ): Boolean { - return false + val result = + Resource( + // currentRequest is the entire request state, so we can use it to figure out if this + // resource is from a thumbnail request (isComplete is false) or the primary request. + if (currentRequest?.isComplete == true) Status.SUCCEEDED else Status.RUNNING, + resource, + isFirstResource, + dataSource + ) + lastResource = result + scope.trySend(result) + return true } } diff --git a/integration/ktx/src/test/java/com/bumptech/glide/integration/ktx/FlowsTest.kt b/integration/ktx/src/test/java/com/bumptech/glide/integration/ktx/FlowsTest.kt index acee8028e4..d59d48ac77 100644 --- a/integration/ktx/src/test/java/com/bumptech/glide/integration/ktx/FlowsTest.kt +++ b/integration/ktx/src/test/java/com/bumptech/glide/integration/ktx/FlowsTest.kt @@ -646,22 +646,22 @@ private fun atMostOnce(function: () -> Unit): () -> Unit { } } -private fun onSuccess(onSuccess: () -> Unit) = +private fun onSuccess(onSuccess: () -> Unit) = simpleRequestListener(onSuccess) {} -private fun onFailure(onFailure: () -> Unit) = +private fun onFailure(onFailure: () -> Unit) = simpleRequestListener({}, onFailure) -private fun simpleRequestListener( +private fun simpleRequestListener( onSuccess: () -> Unit, onFailure: () -> Unit ): RequestListener = object : RequestListener { override fun onResourceReady( - resource: ResourceT?, - model: Any?, - target: Target?, - dataSource: DataSource?, + resource: ResourceT, + model: Any, + target: Target, + dataSource: DataSource, isFirstResource: Boolean, ): Boolean { onSuccess() @@ -671,7 +671,7 @@ private fun simpleRequestListener( override fun onLoadFailed( e: GlideException?, model: Any?, - target: Target?, + target: Target, isFirstResource: Boolean, ): Boolean { onFailure() diff --git a/library/src/main/java/com/bumptech/glide/request/RequestFutureTarget.java b/library/src/main/java/com/bumptech/glide/request/RequestFutureTarget.java index 8d3e825199..b6eba857c8 100644 --- a/library/src/main/java/com/bumptech/glide/request/RequestFutureTarget.java +++ b/library/src/main/java/com/bumptech/glide/request/RequestFutureTarget.java @@ -241,7 +241,7 @@ public void onDestroy() { @Override public synchronized boolean onLoadFailed( - @Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + @Nullable GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { loadFailed = true; exception = e; waiter.notifyAll(this); @@ -250,7 +250,7 @@ public synchronized boolean onLoadFailed( @Override public synchronized boolean onResourceReady( - R resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + @NonNull R resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { // We might get a null result. resultReceived = true; this.resource = resource; diff --git a/library/src/main/java/com/bumptech/glide/request/RequestListener.java b/library/src/main/java/com/bumptech/glide/request/RequestListener.java index f8da91dc25..bb97e843fa 100644 --- a/library/src/main/java/com/bumptech/glide/request/RequestListener.java +++ b/library/src/main/java/com/bumptech/glide/request/RequestListener.java @@ -2,6 +2,7 @@ import android.graphics.drawable.Drawable; import android.widget.ImageView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.load.DataSource; @@ -60,7 +61,10 @@ public interface RequestListener { * Target#onLoadFailed(Drawable)} to be called on {@code target}. */ boolean onLoadFailed( - @Nullable GlideException e, Object model, Target target, boolean isFirstResource); + @Nullable GlideException e, + @Nullable Object model, + @NonNull Target target, + boolean isFirstResource); /** * Called when a load completes successfully, immediately before {@link @@ -68,8 +72,12 @@ boolean onLoadFailed( * *

For threading guarantees, see the class comment. * - * @param resource The resource that was loaded for the target. - * @param model The specific model that was used to load the image. + * @param resource The resource that was loaded for the target. Non-null because a null resource + * will result in a call to {@link #onLoadFailed(GlideException, Object, Target, boolean)} + * instead of this method. + * @param model The specific model that was used to load the image. Non-null because a null model + * will result in a call to {@link #onLoadFailed(GlideException, Object, Target, boolean)} + * instead of this method. * @param target The target the model was loaded into. * @param dataSource The {@link DataSource} the resource was loaded from. * @param isFirstResource {@code true} if this is the first resource to in this load to be loaded @@ -81,5 +89,9 @@ boolean onLoadFailed( * Target#onResourceReady(Object, Transition)} to be called on {@code target}. */ boolean onResourceReady( - R resource, Object model, Target target, DataSource dataSource, boolean isFirstResource); + @NonNull R resource, + @NonNull Object model, + Target target, + @NonNull DataSource dataSource, + boolean isFirstResource); } diff --git a/library/test/src/test/java/com/bumptech/glide/GlideTest.java b/library/test/src/test/java/com/bumptech/glide/GlideTest.java index e174df7e7e..f7a8d1b720 100644 --- a/library/test/src/test/java/com/bumptech/glide/GlideTest.java +++ b/library/test/src/test/java/com/bumptech/glide/GlideTest.java @@ -424,17 +424,17 @@ private void runTestStringDefaultLoader(String string) { public boolean onLoadFailed( GlideException e, Object model, - Target target, + @NonNull Target target, boolean isFirstResource) { throw new RuntimeException("Load failed"); } @Override public boolean onResourceReady( - Drawable resource, - Object model, + @NonNull Drawable resource, + @NonNull Object model, Target target, - DataSource dataSource, + @NonNull DataSource dataSource, boolean isFirstResource) { return false; } diff --git a/library/test/src/test/java/com/bumptech/glide/RequestBuilderTest.java b/library/test/src/test/java/com/bumptech/glide/RequestBuilderTest.java index 7c2e0a5446..ebac2453ca 100644 --- a/library/test/src/test/java/com/bumptech/glide/RequestBuilderTest.java +++ b/library/test/src/test/java/com/bumptech/glide/RequestBuilderTest.java @@ -13,6 +13,7 @@ import android.app.Application; import android.net.Uri; import android.widget.ImageView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.DataSource; @@ -174,17 +175,17 @@ public void testEquals() { public boolean onLoadFailed( @Nullable GlideException e, Object model, - Target target, + @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady( - Object resource, - Object model, + @NonNull Object resource, + @NonNull Object model, Target target, - DataSource dataSource, + @NonNull DataSource dataSource, boolean isFirstResource) { return false; } @@ -195,17 +196,17 @@ public boolean onResourceReady( public boolean onLoadFailed( @Nullable GlideException e, Object model, - Target target, + @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady( - Object resource, - Object model, + @NonNull Object resource, + @NonNull Object model, Target target, - DataSource dataSource, + @NonNull DataSource dataSource, boolean isFirstResource) { return false; } diff --git a/library/test/src/test/java/com/bumptech/glide/request/SingleRequestTest.java b/library/test/src/test/java/com/bumptech/glide/request/SingleRequestTest.java index 2376cc13f1..6f6a4a6765 100644 --- a/library/test/src/test/java/com/bumptech/glide/request/SingleRequestTest.java +++ b/library/test/src/test/java/com/bumptech/glide/request/SingleRequestTest.java @@ -696,17 +696,17 @@ public void onResourceReady_notifiesRequestCoordinator_beforeCallingRequestListe public boolean onLoadFailed( @Nullable GlideException e, Object model, - Target target, + @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady( - List resource, - Object model, + @NonNull List resource, + @NonNull Object model, Target target, - DataSource dataSource, + @NonNull DataSource dataSource, boolean isFirstResource) { verify(builder.requestCoordinator).onRequestSuccess(target.getRequest()); isRequestCoordinatorVerified.set(true); @@ -733,7 +733,7 @@ public void onLoadFailed_notifiesRequestCoordinator_beforeCallingRequestListener public boolean onLoadFailed( @Nullable GlideException e, Object model, - Target target, + @NonNull Target target, boolean isFirstResource) { verify(builder.requestCoordinator).onRequestFailed(target.getRequest()); isRequestCoordinatorVerified.set(true); @@ -742,10 +742,10 @@ public boolean onLoadFailed( @Override public boolean onResourceReady( - List resource, - Object model, + @NonNull List resource, + @NonNull Object model, Target target, - DataSource dataSource, + @NonNull DataSource dataSource, boolean isFirstResource) { return false; } diff --git a/samples/giphy/src/main/java/com/bumptech/glide/samples/giphy/FullscreenActivity.java b/samples/giphy/src/main/java/com/bumptech/glide/samples/giphy/FullscreenActivity.java index 05996088b6..55920fd151 100644 --- a/samples/giphy/src/main/java/com/bumptech/glide/samples/giphy/FullscreenActivity.java +++ b/samples/giphy/src/main/java/com/bumptech/glide/samples/giphy/FullscreenActivity.java @@ -10,6 +10,7 @@ import android.os.Bundle; import android.view.View; import android.widget.ImageView; +import androidx.annotation.NonNull; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; @@ -70,17 +71,17 @@ public void onClick(View view) { public boolean onLoadFailed( GlideException e, Object model, - Target target, + @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady( - Drawable resource, - Object model, + @NonNull Drawable resource, + @NonNull Object model, Target target, - DataSource dataSource, + @NonNull DataSource dataSource, boolean isFirstResource) { if (resource instanceof GifDrawable) { gifDrawable = (GifDrawable) resource; diff --git a/samples/svg/src/main/java/com/bumptech/glide/samples/svg/SvgSoftwareLayerSetter.java b/samples/svg/src/main/java/com/bumptech/glide/samples/svg/SvgSoftwareLayerSetter.java index 411b50d34b..f2ff2c380d 100644 --- a/samples/svg/src/main/java/com/bumptech/glide/samples/svg/SvgSoftwareLayerSetter.java +++ b/samples/svg/src/main/java/com/bumptech/glide/samples/svg/SvgSoftwareLayerSetter.java @@ -2,6 +2,7 @@ import android.graphics.drawable.PictureDrawable; import android.widget.ImageView; +import androidx.annotation.NonNull; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; @@ -17,7 +18,7 @@ public class SvgSoftwareLayerSetter implements RequestListener @Override public boolean onLoadFailed( - GlideException e, Object model, Target target, boolean isFirstResource) { + GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { ImageView view = ((ImageViewTarget) target).getView(); view.setLayerType(ImageView.LAYER_TYPE_NONE, null); return false; @@ -25,10 +26,10 @@ public boolean onLoadFailed( @Override public boolean onResourceReady( - PictureDrawable resource, - Object model, + @NonNull PictureDrawable resource, + @NonNull Object model, Target target, - DataSource dataSource, + @NonNull DataSource dataSource, boolean isFirstResource) { ImageView view = ((ImageViewTarget) target).getView(); view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);