diff --git a/integration/compose/api/compose.api b/integration/compose/api/compose.api index ff27488b34..f4f4f6d59d 100644 --- a/integration/compose/api/compose.api +++ b/integration/compose/api/compose.api @@ -1,8 +1,21 @@ +public final class com/bumptech/glide/integration/compose/CrossFade : com/bumptech/glide/integration/compose/Transition$Factory { + public static final field $stable I + public static final field Companion Lcom/bumptech/glide/integration/compose/CrossFade$Companion; + public fun (Landroidx/compose/animation/core/AnimationSpec;)V + public fun build ()Lcom/bumptech/glide/integration/compose/Transition; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I +} + +public final class com/bumptech/glide/integration/compose/CrossFade$Companion : com/bumptech/glide/integration/compose/Transition$Factory { + public fun build ()Lcom/bumptech/glide/integration/compose/Transition; +} + public abstract interface annotation class com/bumptech/glide/integration/compose/ExperimentalGlideComposeApi : java/lang/annotation/Annotation { } public final class com/bumptech/glide/integration/compose/GlideImageKt { - public static final fun GlideImage (Ljava/lang/Object;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;FLandroidx/compose/ui/graphics/ColorFilter;Lcom/bumptech/glide/integration/compose/Placeholder;Lcom/bumptech/glide/integration/compose/Placeholder;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun GlideImage (Ljava/lang/Object;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;FLandroidx/compose/ui/graphics/ColorFilter;Lcom/bumptech/glide/integration/compose/Placeholder;Lcom/bumptech/glide/integration/compose/Placeholder;Lcom/bumptech/glide/integration/compose/Transition$Factory;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V public static final fun GlideSubcomposition (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V public static final fun placeholder (I)Lcom/bumptech/glide/integration/compose/Placeholder; public static final fun placeholder (Landroid/graphics/drawable/Drawable;)Lcom/bumptech/glide/integration/compose/Placeholder; @@ -54,3 +67,14 @@ public final class com/bumptech/glide/integration/compose/RequestState$Success : public fun toString ()Ljava/lang/String; } +public abstract interface class com/bumptech/glide/integration/compose/Transition { + public abstract fun getDrawCurrent ()Lkotlin/jvm/functions/Function5; + public abstract fun getDrawPlaceholder ()Lkotlin/jvm/functions/Function5; + public abstract fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun transition (Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class com/bumptech/glide/integration/compose/Transition$Factory { + public abstract fun build ()Lcom/bumptech/glide/integration/compose/Transition; +} + 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 1236ee4e66..3e79487bdf 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 @@ -1,7 +1,5 @@ package com.bumptech.glide.integration.compose -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import androidx.compose.foundation.Image @@ -15,8 +13,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.DefaultAlpha -import androidx.compose.ui.graphics.asImageBitmap -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.ContentScale @@ -26,10 +22,7 @@ import androidx.compose.ui.platform.LocalInspectionMode import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestManager -import com.bumptech.glide.integration.ktx.InternalGlideApi import com.bumptech.glide.load.DataSource -import com.google.accompanist.drawablepainter.DrawablePainter -import com.google.accompanist.drawablepainter.rememberDrawablePainter /** Mutates and returns the given [RequestBuilder] to apply relevant options. */ public typealias RequestBuilderTransform = (RequestBuilder) -> RequestBuilder @@ -77,6 +70,13 @@ public typealias RequestBuilderTransform = (RequestBuilder) -> RequestBuil * opposed to resource id or [Drawable]), this [Placeholder] will not be used unless the `error` * [RequestBuilder] also fails. This parameter does not override error [RequestBuilder]s, only error * resource ids and/or [Drawable]s. + * @param transition An optional [Transition.Factory] that can animate the transition from a + * placeholder to a loaded image. The transition will only be called once, when the load transitions + * from showing the placeholder to showing the first resource. The transition will persist across + * multiple resources if you're using thumbnail, but will not be called for each successive resource + * in the request chain. The transition factory will not be called across different requests if + * multiple are made. The transition will not be called if you use [placeholder] or [failure] with + * the deprecated [Composable] API. See [CrossFade] */ // TODO(judds): the API here is not particularly composeesque, we should consider alternatives // to RequestBuilder (though thumbnail() may make that a challenge). @@ -95,6 +95,7 @@ public fun GlideImage( // See http://shortn/_x79pjkMZIH for an internal discussion. loading: Placeholder? = null, failure: Placeholder? = null, + transition: Transition.Factory? = null, // TODO(judds): Consider defaulting to load the model here instead of always doing so below. requestBuilderTransform: RequestBuilderTransform = { it }, ) { @@ -144,6 +145,7 @@ public fun GlideImage( contentScale, alpha, colorFilter, + transition, ) ) } @@ -173,12 +175,6 @@ internal class GlideSubcompositionScopeImpl( override val painter: Painter get() = drawable?.toPainter() ?: ColorPainter(Color.Transparent) - private fun Drawable.toPainter(): Painter = - when (this) { - is BitmapDrawable -> BitmapPainter(bitmap.asImageBitmap()) - is ColorDrawable -> ColorPainter(Color(color)) - else -> DrawablePainter(mutate()) - } } /** @@ -253,7 +249,6 @@ public sealed class RequestState { * load never starting, or in an unreasonably large amount of memory being used. Loading overly * large images in memory can also impact scrolling performance. */ -@OptIn(InternalGlideApi::class) @ExperimentalGlideComposeApi @Composable public fun GlideSubcomposition( @@ -325,7 +320,7 @@ private fun PreviewResourceOrDrawable( throw IllegalArgumentException("Composables should go through the production codepath") } Image( - painter = rememberDrawablePainter(drawable), + painter = remember(drawable) { drawable.toPainter() }, modifier = modifier, contentDescription = contentDescription, ) diff --git a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlideModifier.kt b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlideModifier.kt index 7cac40f824..8c382fe8e2 100644 --- a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlideModifier.kt +++ b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlideModifier.kt @@ -2,18 +2,19 @@ package com.bumptech.glide.integration.compose import android.graphics.PointF import android.graphics.drawable.Drawable +import android.util.Log import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.DefaultAlpha -import androidx.compose.ui.graphics.asAndroidColorFilter import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.translate -import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.withSave import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasureResult @@ -35,6 +36,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.toSize import com.bumptech.glide.RequestBuilder import com.bumptech.glide.integration.ktx.AsyncGlideSize import com.bumptech.glide.integration.ktx.ExperimentGlideFlows @@ -46,7 +48,9 @@ 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.internalModel +import com.bumptech.glide.load.DataSource import com.bumptech.glide.util.Preconditions +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -66,6 +70,7 @@ internal fun Modifier.glideNode( contentScale: ContentScale? = null, alpha: Float? = null, colorFilter: ColorFilter? = null, + transitionFactory: Transition.Factory? = null, requestListener: RequestListener? = null, draw: Boolean? = true, ): Modifier { @@ -73,12 +78,13 @@ internal fun Modifier.glideNode( requestBuilder, contentScale ?: ContentScale.None, alignment ?: Alignment.Center, + alpha, colorFilter, requestListener, - draw + draw, + transitionFactory, ) then clipToBounds() then - alpha(alpha ?: DefaultAlpha) then if (contentDescription != null) { semantics { this@semantics.contentDescription = contentDescription @@ -95,9 +101,11 @@ internal data class GlideNodeElement constructor( private val requestBuilder: RequestBuilder, private val contentScale: ContentScale, private val alignment: Alignment, + private val alpha: Float?, private val colorFilter: ColorFilter?, private val requestListener: RequestListener?, private val draw: Boolean?, + private val transitionFactory: Transition.Factory?, ) : ModifierNodeElement() { override fun create(): GlideNode { val result = GlideNode() @@ -110,9 +118,11 @@ internal data class GlideNodeElement constructor( requestBuilder, contentScale, alignment, + alpha, colorFilter, requestListener, draw, + transitionFactory, ) } @@ -124,6 +134,13 @@ internal data class GlideNodeElement constructor( properties["contentScale"] = contentScale properties["colorFilter"] = colorFilter properties["draw"] = draw + properties["transition"] = when (transitionFactory) { + is DoNotTransition.Factory -> "None" + is CrossFade -> "CrossFade" + else -> { + "Custom: $transitionFactory" + } + } } } @@ -134,14 +151,27 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi private lateinit var requestBuilder: RequestBuilder private lateinit var contentScale: ContentScale private lateinit var alignment: Alignment - private var draw: Boolean = true + private var alpha: Float = DefaultAlpha private var colorFilter: ColorFilter? = null + private var transitionFactory: Transition.Factory = DoNotTransition.Factory + private var draw: Boolean = true private var requestListener: RequestListener? = null private var currentJob: Job? = null + private var painter: Painter? = null + + // Only used for debugging private var drawable: Drawable? = null private var state: RequestState = RequestState.Loading private var resolvableGlideSize: ResolvableGlideSize? = null + private var placeholder: Painter? = null + private var isFirstResource = true + + // Avoid allocating Point/PointFs during draw + private var placeholderPositionAndSize: CachedPositionAndSize? = null + private var drawablePositionAndSize: CachedPositionAndSize? = null + + private var transition: Transition = DoNotTransition private fun RequestBuilder<*>.maybeImmediateSize() = this.overrideSize()?.let { ImmediateGlideSize(it) } @@ -150,9 +180,11 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi requestBuilder: RequestBuilder, contentScale: ContentScale, alignment: Alignment, + alpha: Float?, colorFilter: ColorFilter?, requestListener: RequestListener?, draw: Boolean?, + transitionFactory: Transition.Factory?, ) { // Other attributes can be refreshed by re-drawing rather than restarting a request val restartLoad = @@ -162,9 +194,11 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi this.requestBuilder = requestBuilder this.contentScale = contentScale this.alignment = alignment + this.alpha = alpha ?: DefaultAlpha this.colorFilter = colorFilter this.requestListener = requestListener this.draw = draw ?: true + this.transitionFactory = transitionFactory ?: DoNotTransition.Factory if (restartLoad) { clear() @@ -187,14 +221,10 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi launchRequest(requestBuilder) } } else { - drawable?.colorFilter = colorFilter?.asAndroidColorFilter() invalidateDraw() } } - private val Int.isValidDimension - get() = this > 0 - private val Float.isValidDimension get() = this > 0f @@ -205,16 +235,26 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi private fun IntOffset.toPointF() = PointF(x.toFloat(), y.toFloat()) - override fun ContentDrawScope.draw() { - if (draw) { - val drawable = drawable ?: return - val srcWidth = if (drawable.intrinsicWidth.isValidDimension) { - drawable.intrinsicWidth.toFloat() + internal data class CachedPositionAndSize(val position: PointF, val size: Size) + + private fun ContentDrawScope.drawOne( + painter: Painter?, + cache: CachedPositionAndSize?, + drawOne: DrawScope.(Size) -> Unit + ): CachedPositionAndSize? { + if (painter == null) { + return null + } + val currentPositionAndSize = if (cache != null) { + cache + } else { + val srcWidth = if (painter.intrinsicSize.width.isValidDimension) { + painter.intrinsicSize.width } else { size.width } - val srcHeight = if (drawable.intrinsicHeight.isValidDimension) { - drawable.intrinsicHeight.toFloat() + val srcHeight = if (painter.intrinsicSize.height.isValidDimension) { + painter.intrinsicSize.height } else { size.height } @@ -226,15 +266,38 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi Size.Zero.roundToInt() } - drawable.setBounds(0, 0, scaledSize.width, scaledSize.height) - val alignedPosition: PointF = alignment.align( - IntSize(scaledSize.width, scaledSize.height), - IntSize(size.width.roundToInt(), size.height.roundToInt()), - layoutDirection - ).toPointF() + CachedPositionAndSize( + alignment.align( + IntSize(scaledSize.width, scaledSize.height), + IntSize(size.width.roundToInt(), size.height.roundToInt()), + layoutDirection + ).toPointF(), scaledSize.toSize() + ) + } + + translate(currentPositionAndSize.position.x, currentPositionAndSize.position.y) { + drawOne.invoke(this, currentPositionAndSize.size) + } + return currentPositionAndSize + } - translate(alignedPosition.x, alignedPosition.y) { - drawable.draw(drawContext.canvas.nativeCanvas) + override fun ContentDrawScope.draw() { + if (draw) { + val drawPlaceholder = transition.drawPlaceholder ?: DoNotTransition.drawPlaceholder + placeholder?.let { painter -> + drawContext.canvas.withSave { + placeholderPositionAndSize = drawOne(painter, placeholderPositionAndSize) { size -> + drawPlaceholder.invoke(this, painter, size, alpha, colorFilter) + } + } + } + + painter?.let { painter -> + drawContext.canvas.withSave { + drawablePositionAndSize = drawOne(painter, drawablePositionAndSize) { size -> + transition.drawCurrent.invoke(this, painter, size, alpha, colorFilter) + } + } } } @@ -258,6 +321,25 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi updateDrawable(null) } + @OptIn(ExperimentGlideFlows::class) + private fun CoroutineScope.maybeAnimate(instant: Resource) { + if (instant.dataSource == DataSource.MEMORY_CACHE + || !isFirstResource + || transitionFactory == DoNotTransition.Factory + ) { + isFirstResource = false + transition = DoNotTransition + return + } + isFirstResource = false + transition = transitionFactory.build() + launch { + transition.transition { + invalidateDraw() + } + } + } + @OptIn(ExperimentGlideFlows::class, InternalGlideApi::class, ExperimentalComposeUiApi::class) private fun launchRequest(requestBuilder: RequestBuilder) = // Launch via sideEffect because onAttach is called before onNewRequest and onNewRequest is not @@ -274,11 +356,17 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi Preconditions.checkArgument(currentJob == null) currentJob = (coroutineScope + Dispatchers.Main.immediate).launch { this@GlideNode.resolvableGlideSize = requestBuilder.maybeImmediateSize() ?: AsyncGlideSize() + placeholder = null + placeholderPositionAndSize = null requestBuilder.flowResolvable(resolvableGlideSize!!).collect { val (state, drawable) = when (it) { - is Resource -> Pair(RequestState.Success(it.dataSource), it.resource) + is Resource -> { + maybeAnimate(it) + Pair(RequestState.Success(it.dataSource), it.resource) + } + is Placeholder -> { val drawable = it.placeholder val state = when (it.status) { @@ -286,6 +374,8 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi Status.FAILED -> RequestState.Failure Status.SUCCEEDED -> throw IllegalStateException() } + placeholder = drawable?.toPainter() + placeholderPositionAndSize = null Pair(state, drawable) } } @@ -298,25 +388,23 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi } private fun updateDrawable(drawable: Drawable?) { - this.drawable?.callback = null this.drawable = drawable - drawable?.colorFilter = colorFilter?.asAndroidColorFilter() - drawable?.callback = object : Drawable.Callback { - override fun invalidateDrawable(who: Drawable) { - invalidateDraw() - } - - override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {} - override fun unscheduleDrawable(who: Drawable, what: Runnable) {} - } + painter = drawable?.toPainter() + drawablePositionAndSize = null } override fun onDetach() { super.onDetach() clear() + if (transition != DoNotTransition) { + coroutineScope.launch { + transition.stop() + } + } } private fun clear() { + isFirstResource = true resolvableGlideSize = null currentJob?.cancel() currentJob = null @@ -329,6 +417,8 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi measurable: Measurable, constraints: Constraints ): MeasureResult { + placeholderPositionAndSize = null + drawablePositionAndSize = null when (val currentSize = resolvableGlideSize) { is AsyncGlideSize -> { val inferredSize = constraints.inferredGlideSize() @@ -341,7 +431,9 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi null -> {} } val placeable = measurable.measure(constraints) - return layout(constraints.maxWidth, constraints.maxHeight) { placeable.placeRelative(0, 0) } + return layout(constraints.maxWidth, constraints.maxHeight) { + placeable.placeRelative(0, 0) + } } override fun SemanticsPropertyReceiver.applySemantics() { @@ -356,6 +448,8 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi && colorFilter == other.colorFilter && requestListener == other.requestListener && draw == other.draw + && transitionFactory == other.transitionFactory + && alpha == other.alpha } return false } @@ -367,7 +461,8 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi result = 31 * result + colorFilter.hashCode() result = 31 * result + draw.hashCode() result = 31 * result + requestListener.hashCode() - + result = 31 * result + transitionFactory.hashCode() + result = 31 * result + alpha.hashCode() return result } } diff --git a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Painter.kt b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Painter.kt new file mode 100644 index 0000000000..5b1c2cea8a --- /dev/null +++ b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Painter.kt @@ -0,0 +1,19 @@ +package com.bumptech.glide.integration.compose + +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.graphics.painter.Painter +import com.google.accompanist.drawablepainter.DrawablePainter + +internal fun Drawable?.toPainter(): Painter = + when (this) { + is BitmapDrawable -> BitmapPainter(bitmap.asImageBitmap()) + is ColorDrawable -> ColorPainter(Color(color)) + null -> ColorPainter(Color.Transparent) + else -> DrawablePainter(mutate()) + } diff --git a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Transition.kt b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Transition.kt new file mode 100644 index 0000000000..8b30e0bfa9 --- /dev/null +++ b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Transition.kt @@ -0,0 +1,164 @@ +package com.bumptech.glide.integration.compose + +import android.util.Log +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.tween +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.painter.Painter + +/** + * Transition between a given request's optional placeholder and the resource. + */ +public interface Transition { + /** + * Start the transition, calling [invalidate] each time the transition requires that the image + * be re-drawn. + */ + public suspend fun transition(invalidate: () -> Unit) + + /** + * Stop the transition + */ + public suspend fun stop() + + /** + * Optionally draw the current placeholder. If you want the placeholder to be shown during the + * transition, you must implement this method. If you only want to show the placeholder until + * the first resource finishes loading, then you can simply return null + * + * The canvas is prepared so that the image will be drawn in the appropriate location based on + * the parameters you provided (e.g. `ContentScale`). The canvas is saved before this method is + * called and restored afterward so you do not need to do so manually unless it's required for + * your transition. + * + * This method will only be called if a placeholder is set. If this method is called, it will + * always be called before [drawCurrent]. + */ + public val drawPlaceholder: DrawPainter? + + /** + * Draw the current image. If you do not draw the current image, it will not be shown. + * + * This method is used before and after the transition finishes, so you need to ensure that you + * draw something even if your transition is not currently running, if you want the image to + * be displayed. + * + * The canvas is prepared so that the image will be drawn in the appropriate location based on + * the parameters you provided (e.g. `ContentScale`). The canvas is saved before this method is + * called and restored afterward so you do not need to do so manually unless it's required for + * your transition. + * + * This method is always called if there is something to draw. If [drawPlaceholder] is also + * called, [drawPlaceholder] will be called before this method. + */ + public val drawCurrent: DrawPainter + + /** + * Creates a new instance of this [Transition]. Must implement equals/hashcode. The simplest way + * of ensuring equals and hashcode are implemented correctly for custom transitions is to use an + * Object. + */ + public interface Factory { + /** + * Create a new stateful [Transition] instance. + * + * May be called multiple times for a single Composable. + */ + public fun build(): Transition + } +} + +public typealias DrawPainter = DrawScope.(Painter, Size, Float, ColorFilter?) -> Unit + +internal object DoNotTransition : Transition { + object Factory : Transition.Factory { + override fun build() = DoNotTransition + } + + override suspend fun transition(invalidate: () -> Unit) {} + override suspend fun stop() {} + override val drawPlaceholder: DrawPainter = { _, _, _, _ -> } + override val drawCurrent: DrawPainter = { painter, size, alpha, colorFilter -> + with(painter) { + draw(size, alpha, colorFilter) + } + } +} + +/** + * A factory for [CrossFade]. If you do not want to modify the [animationSpec], use the companion + * object (ie `transition = CrossFade`) + */ +public class CrossFade( + private val animationSpec: AnimationSpec +) : Transition.Factory { + + override fun build(): Transition = CrossFadeImpl(animationSpec) + + /** + * A default [CrossFade] with a 250ms duration + */ + public companion object : Transition.Factory { + override fun build(): Transition = + CrossFadeImpl(animationSpec = tween(250)) + } + + override fun equals(other: Any?): Boolean { + if (other is CrossFade) { + return animationSpec == other.animationSpec + } + return false + } + + override fun hashCode(): Int { + return animationSpec.hashCode() + } +} + +/** + * Fades out the placeholder while fading in the resource(s). + */ +internal class CrossFadeImpl( + private val animationSpec: AnimationSpec +) : Transition { + + private companion object { + const val OPAQUE_ALPHA = 1f + } + + private val animatable: Animatable = + Animatable(0f, Float.VectorConverter, OPAQUE_ALPHA) + + override suspend fun transition(invalidate: () -> Unit) { + try { + animatable.animateTo(OPAQUE_ALPHA, animationSpec) + invalidate() + } finally { + animatable.snapTo(OPAQUE_ALPHA) + invalidate() + } + } + + override suspend fun stop() { + animatable.stop() + } + + override val drawPlaceholder: DrawPainter = { painter, size, alpha, colorFilter -> + with(painter) { + draw(size, (OPAQUE_ALPHA - animatable.value) * alpha, colorFilter) + } + } + + override val drawCurrent: DrawPainter = { painter, size, alpha, colorFilter -> + with(painter) { + draw(size, animatable.value * alpha, colorFilter) + } + } +} \ No newline at end of file diff --git a/samples/gallery/src/main/java/com/bumptech/glide/samples/gallery/HorizontalGalleryFragment.kt b/samples/gallery/src/main/java/com/bumptech/glide/samples/gallery/HorizontalGalleryFragment.kt index 80e0d4f497..7b9a823f6f 100644 --- a/samples/gallery/src/main/java/com/bumptech/glide/samples/gallery/HorizontalGalleryFragment.kt +++ b/samples/gallery/src/main/java/com/bumptech/glide/samples/gallery/HorizontalGalleryFragment.kt @@ -72,7 +72,7 @@ class HorizontalGalleryFragment : Fragment() { preloadRequestBuilder: RequestBuilder, modifier: Modifier, ) = - GlideImage(model = item.uri, contentDescription = null, modifier = modifier) { + GlideImage(model = item.uri, contentDescription = item.displayName, modifier = modifier) { it.thumbnail(preloadRequestBuilder).signature(item.signature()) }