如何为Compose Image提供网络图片加载支持

本文是源码分析类文章

如何为Compose Image提供网络图片加载支持?目前(Compose 1.0.5)最好的选择是使用图片框架Coil,Coil对Jetpack Compose相关的支持文档在这

Compose内的Image组件类似于ImageView,仅支持从本地加载图片资源,要想从网络中获取图片并加载,我们首先就得要使用能够处理网络请求的框架,将远程图片资源载入到本地才行。目前主流的图片加载框架Picasso、Glide、Coil等,它们更多面对的仍是传统的View系统下,将图片加载到ImageView中并显示这样的应用场景,而不是为Compose量身打造的,基于此,Accompanist库曾提供了一些图片加载框架的扩展库,为Compose的Image显示网络图片进行简便支持。时过境迁,后来Coil为Image加载图片提供了相关支持,故Accompanist以前关于图片加载框架扩展的依赖都被废弃并不推荐使用了。

接下来我们将分析Accompanist曾经是如何对图片框架做扩展适配,使之能够与Compose配合工作的。

Picasso(in version 0.6.2)

Accompanist在0.3.0版本就提供了Picasso的支持,不过,在版本0.7.0该集成被移除(相关的pull参见https://github.com/google/accompanist/pull/253

在0.6.2版本中,想要加载网络图片,你可能会使用如下代码:

PicassoImage(
    data = "http://..."
    modifier = Modifier.size(50.dp),
) {
    
     imageLoadState ->
    when(imageLoadState) {
    
    
        ...
    }
}
CoilImage(
    data = "https://i.imgur.com/StXm8nf.jpg",
    contentDescription = null,
    onRequestCompleted = {
    
    
        println("LoadingCoilImage onRequestCompleted $it")
    },
    contentScale = ContentScale.Crop,
    modifier = Modifier.fillMaxWidth(),
) {
    
    
    ...
}

在version 0.6.2中,加载远程图片的方法是使用专用的Image组件,使用Picasso框架的调用PicassoImage,使用Coil的则调用CoilImage,等等。它们都依赖于一个imageloader-core的核心库来进行图片加载,我们不难想象这个加载图片的方法,为了糅合各类框架,肯定要用不少泛型,事实上它长下面这样:

@Composable
fun <R : Any, TR : Any> ImageLoad(
    request: R,
    executeRequest: suspend (TR) -> ImageLoadState,
    modifier: Modifier = Modifier,
    requestKey: Any = request,
    transformRequestForSize: (R, IntSize) -> TR?,
    shouldRefetchOnSizeChange: (currentResult: ImageLoadState, size: IntSize) -> Boolean = DefaultRefetchOnSizeChangeLambda,
    onRequestCompleted: (ImageLoadState) -> Unit = EmptyRequestCompleteLambda,
    content: @Composable BoxScope.(imageLoadState: ImageLoadState) -> Unit
) {
    
    
    ...
}

泛型R代表请求的值,这个值之所以是泛型,是因为实际上各种框架都支持多类型的图片加载请求,这个请求可能是基于一个URL的String,也可能单纯是一个resource的id,或者就是一个Bitmap,等等。泛型TR代表了不同图片框架内收集本次图片请求信息的实体类(或者是Builder),在Picasso中这个类叫RequestCreator,在Glide中这个类叫RequestBuilder。

我们继续观察它的实现:

@Composable
fun <R : Any, TR : Any> ImageLoad(
    request: R,
    executeRequest: suspend (TR) -> ImageLoadState,
    modifier: Modifier = Modifier,
    requestKey: Any = request,
    transformRequestForSize: (R, IntSize) -> TR?,
    shouldRefetchOnSizeChange: (currentResult: ImageLoadState, size: IntSize) -> Boolean = DefaultRefetchOnSizeChangeLambda,
    onRequestCompleted: (ImageLoadState) -> Unit = EmptyRequestCompleteLambda,
    content: @Composable BoxScope.(imageLoadState: ImageLoadState) -> Unit
) {
    
    
    // 三个rememberUpdatedState,目的是为了避免更改后重组
    val updatedOnRequestCompleted by rememberUpdatedState(onRequestCompleted)
    val updatedTransformRequestForSize by rememberUpdatedState(transformRequestForSize)
    val updatedExecuteRequest by rememberUpdatedState(executeRequest)

    // 这个state拿来缓存控件大小,因为控件大小要等到Compose内容传入constraints才能确定
    var requestSize by remember(requestKey) {
    
     mutableStateOf<IntSize?>(null) }

    // 重点,这里使用produceState将executeRequest返回的非Compose状态转换为一个State
    // 之所以连加载图片的过程都抽象成一个叫executeRequest的lambda,还是因为要糅合多个框架
    val loadState by produceState<ImageLoadState>(
        initialValue = ImageLoadState.Loading,
        key1 = requestKey,
        key2 = requestSize,
    ) {
    
    
        // value一开始肯定被赋值为ImageLoadState.Loading,因为requestSize为空。
        // 当requestSize被赋值后,首先将开始执行transformRequestForSize这个lambda
        // 传入原来的request和新获得的size,要求返回一个类似RequestBuilder的结果
        value = requestSize?.let {
    
     updatedTransformRequestForSize(request, it) }
            ?.let {
    
     transformedRequest ->
                   // 这里传入刚才的RequestBuilder
                try {
    
    
                    // 发起图片加载请求,这里可能会挂起
                    updatedExecuteRequest(transformedRequest)
                } catch (e: CancellationException) {
    
    
                    // We specifically don't do anything for the request coroutine being
                    // cancelled: https://github.com/google/accompanist/issues/217
                    // 如果我们响应了协程的CancellationException,让ImageLoadState变成了Error
                    // 有可能会出问题,因为如果取消的协程在新协程完成后执行,
                    // 会导致新的图片状态(Success)被上次取消的结果(Error)覆盖
                    throw e
                } catch (e: Error) {
    
    
                    // Re-throw all Errors
                    throw e
                } catch (e: IllegalStateException) {
    
    
                    // Re-throw all IllegalStateExceptions
                    throw e
                } catch (t: Throwable) {
    
    
                    // Anything else, we wrap in a Error state instance
                    // 除了CancellationException、Error、IllegalStateException之外,
                    // 其余的错误将会令状态转变为Error
                    ImageLoadState.Error(painter = null, throwable = t)
                    // also内,加载完成,回调onRequestCompleted
                }.also(updatedOnRequestCompleted)
            } ?: ImageLoadState.Loading
    }

    BoxWithConstraints(
        modifier = modifier,
        propagateMinConstraints = true,
    ) {
    
    
        val size = IntSize(
            width = if (constraints.hasBoundedWidth) constraints.maxWidth else -1,
            height = if (constraints.hasBoundedHeight) constraints.maxHeight else -1
        )
        if (requestSize == null ||
            (requestSize != size && shouldRefetchOnSizeChange(loadState, size))
        ) {
    
    
            requestSize = size
        }

        content(loadState)
    }
}

ImageLoad的思路清晰明了:调用方告诉它如何build一个请求,并在使用图片框架的过程中产生ImageLoadState状态,它会把ImageLoadState转换为可以观察的State<ImageLoadState>

直接使用通用实现的缺点在于会产生很多模板代码,可以基于通用实现进行更简洁的封装,我们以特定的PicassoImage的实现为例进行分析:

// 这个API封装更彻底,不需要写when(state),直接在函数中传入error、loading的内容即可
@Composable
fun PicassoImage(
    data: Any,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    colorFilter: ColorFilter? = null,
    fadeIn: Boolean = false,
    picasso: Picasso = LocalPicasso.current,
    requestBuilder: (RequestCreator.(size: IntSize) -> RequestCreator)? = null,
    shouldRefetchOnSizeChange: (currentResult: ImageLoadState, size: IntSize) -> Boolean = DefaultRefetchOnSizeChangeLambda,
    onRequestCompleted: (ImageLoadState) -> Unit = EmptyRequestCompleteLambda,
    error: @Composable (BoxScope.(ImageLoadState.Error) -> Unit)? = null,
    loading: @Composable (BoxScope.() -> Unit)? = null,
) {
    
    
    PicassoImage(
        data = data,
        modifier = modifier,
        requestBuilder = requestBuilder,
        picasso = picasso,
        shouldRefetchOnSizeChange = shouldRefetchOnSizeChange,
        onRequestCompleted = onRequestCompleted,
    ) {
    
     imageState ->
        when (imageState) {
    
    
            is ImageLoadState.Success -> {
    
    
                // MaterialLoadingImage是0.6.2版本中存在的一个实现fadeIn效果的控件
                // 原理是使用Compose动画中的Transition托管三个动画
                // alpha(透明度),brightness(亮度),saturation(饱和度), 
                // 同时修改传入Image内的colorFliter的这三个值,从而实现渐入效果
                MaterialLoadingImage(
                    result = imageState,
                    contentDescription = contentDescription,
                    fadeInEnabled = fadeIn,
                    alignment = alignment,
                    contentScale = contentScale,
                    colorFilter = colorFilter
                )
            }
            is ImageLoadState.Error -> if (error != null) error(imageState)
            ImageLoadState.Loading -> if (loading != null) loading()
            ImageLoadState.Empty -> Unit
        }
    }
}


@Composable
fun PicassoImage(
    data: Any,
    modifier: Modifier = Modifier,
    picasso: Picasso = LocalPicasso.current,
    requestBuilder: (RequestCreator.(size: IntSize) -> RequestCreator)? = null,
    shouldRefetchOnSizeChange: (currentResult: ImageLoadState, size: IntSize) -> Boolean = DefaultRefetchOnSizeChangeLambda,
    onRequestCompleted: (ImageLoadState) -> Unit = EmptyRequestCompleteLambda,
    content: @Composable BoxScope.(imageLoadState: ImageLoadState) -> Unit
) {
    
    
    ImageLoad(
        request = data.toRequestCreator(picasso),
        requestKey = data, // Picasso RequestCreator doesn't support equality so we use the data
        executeRequest = {
    
     r ->
            @OptIn(ExperimentalCoroutinesApi::class)
            suspendCancellableCoroutine {
    
     cont ->
                // 初始化了一个Target,这个Target用来获取图片加载结果
                val target = object : com.squareup.picasso.Target {
    
    
                    override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
    
    
                        val state = ImageLoadState.Success(
                            painter = BitmapPainter(bitmap.asImageBitmap()),
                            source = from.toDataSource()
                        )
                        // 协程恢复
                        cont.resume(state) {
    
    
                            // Not much we can do here. Ignore this
                        }
                    }

                    override fun onBitmapFailed(exception: Exception, errorDrawable: Drawable?) {
    
    
                        val state = ImageLoadState.Error(
                            throwable = exception,
                            painter = errorDrawable?.toPainter(),
                        )
                        // 协程恢复
                        cont.resume(state) {
    
    
                            // Not much we can do here. Ignore this
                        }
                    }

                    override fun onPrepareLoad(placeholder: Drawable?) = Unit
                }

                cont.invokeOnCancellation {
    
    
                    // 取消图片加载
                    picasso.cancelRequest(target)
                }

                // Now kick off the image load into our target
                r.into(target)
            }
        },
        transformRequestForSize = {
    
     r, size ->
            val sizedRequest = when {
    
    
                // 如果尺寸包含未指定尺寸的尺寸,我们不会在Coil请求中指定尺寸
                size.width < 0 || size.height < 0 -> r
               
                size != IntSize.Zero -> {
    
    
                    r.resize(size.width, size.height)
                        .centerInside()
                        .onlyScaleDown()
                }
                // Otherwise we have a zero size, so no point executing a request
                // 未获得size,因此暂时无法生成请求
                else -> null
            }

            // 根据参数来build请求
            if (sizedRequest != null && requestBuilder != null) {
    
    
                // If we have a transformed request and builder, let it run
                requestBuilder(sizedRequest, size)
            } else {
    
    
                // Otherwise we just return the sizedRequest
                sizedRequest
            }
        },
        shouldRefetchOnSizeChange = shouldRefetchOnSizeChange,
        onRequestCompleted = onRequestCompleted,
        modifier = modifier,
        content = content
    )
}

现在让我们来总结一下,在0.6.2版本,实现网络图片加载的集成库思路如下:

  1. 图片加载:使用Target回调获取加载的结果(各个框架都有类似的抽象的Target而不是限制目标必须是ImageView)。结果返回的过程是阻塞式,协程将在produceState内执行到updatedExecuteRequest(transformedRequest)后挂起,直到这个lambda返回结果,State的值将会在结果返回后产生变化。当然,如果协程被取消,Picasso也会取消加载到Target那个图片请求。
  2. 图片大小约束:依赖于BoxWithConstraints获得的约束大小。
  3. 渐入动画实现:使用动画API Transition对ColorFliter的alpha,brightness,saturation进行动态修改,从而实现渐入动画。
  4. loading占位图、error显示等:依赖于用户传入的@Composable内容.根据produceState生成的状态,PicassoImage内显示的@Composable内容会动态变化。

Glide(in version 0.13.0)

0.3.0版本诞生于2020年10月份,而当时间来到了2021年4月,Accompanist发布0.8.0版本,Coil 和 Glide 集成库进行了大规模的重构。上面提到的类似于CoilImage()GlideImage()API都已经被弃用了。

以下对Glide集成库的分析基于版本0.13.0的代码。

如果在0.13.0版本想要加载远程图片,或许你会写出以下的代码:

Image(
    painter = rememberGlidePainter(request = "http://..."),
    contentDescription = null
)

新的API不再需要专门的Image组件,而是使用Painter这种概念来表现加载的结果。新的API对性能的提升似乎有所提升:Compose内容重组后,需要重绘的不再是不同的Loading组件或Success组件,现在核心组件一定是一个Image,随加载状态变化的只不过是Image内绘制的内容而已,重绘范围有所缩小。这很符合我们对ImageView的想象:在加载的时候显示一张placeholder占位图,成功显示最终结果,否则显示error图片,而placeholder和error都可以发起图片加载请求的时候设置。

Painter是一个什么样的概念?我们可以先看一下类注释是怎么介绍它的:

/**
* 对可以画出来的东西的抽象。除了能够绘制到指定的有界区域外,Painter还提供了一些高级机制,消费者可以使用
* 这些机制来配置内容的绘制方式。其中包括alpha、ColorFilter和RTL
* 实现应该提供一个有意义的equals方法来比较不同Painter子类的值,而不仅仅依赖于引用相等
*/
abstract class Painter {
    
    
    ...
    protected abstract fun DrawScope.onDraw()
}

描述看起来有点像Drawable,但实际上Drawable比Painter更加复杂一些,除了上述的alpha、ColorFilter、LayoutDirection之外,Drawable还具有动画Callback、Level、Hotspot等属性。DrawScope.onDraw()方法类似于Drawable的draw(Canvas canvas)

继续观察rememberGlidePainter的具体实现:

@Composable
fun rememberGlidePainter(
    request: Any?,
    requestManager: RequestManager = GlidePainterDefaults.defaultRequestManager(),
    shouldRefetchOnSizeChange: ShouldRefetchOnSizeChange = ShouldRefetchOnSizeChange {
    
     _, _ -> false },
    // 注意这里的requestBuilder,加载的结果类型已经被固定为drawable
    requestBuilder: (RequestBuilder<Drawable>.(size: IntSize) -> RequestBuilder<Drawable>)? = null,
    // 新的API也能开启fadeIn效果
    fadeIn: Boolean = false,
    fadeInDurationMs: Int = LoadPainterDefaults.FadeInTransitionDuration,
    // 是不是很疑惑为什么这里有个占位图id的参数?Glide本身就支持占位图设置,
    // 在Build Request的时候设置不就行了吗?其实这个参数是给Compose预览模式用的
    @DrawableRes previewPlaceholder: Int = 0,
): LoadPainter<Any> {
    
    
    // GlideLoader是加载逻辑实现类,稍后展示
    val glideLoader = remember {
    
    
        GlideLoader(requestManager, requestBuilder)
    }.apply {
    
    
        // 这里的逻辑并不是多余的,要知道如果key没有变化,remember函数会直接返回上次计算的结果,
        // 这里想表达的是,对上次的结果调用apply,更新requestManager和requestBuilder
        this.requestManager = requestManager
        this.requestBuilder = requestBuilder
    }
    // rememberLoadPainter位于之前所说的imageloading-core的核心库
    // 在0.13.0版本Coil和Glide都用到这个库来获取LoadPainter
    return rememberLoadPainter(
        loader = glideLoader,
        request = checkData(request),
        shouldRefetchOnSizeChange = shouldRefetchOnSizeChange,
        fadeIn = fadeIn,
        fadeInDurationMs = fadeInDurationMs,
        previewPlaceholder = previewPlaceholder
    )
}
// checkData检查了request的类型
private fun checkData(data: Any?): Any? {
    
    
    when (data) {
    
    
        is Drawable -> {
    
    
            throw IllegalArgumentException(....)
        }
        is ImageBitmap -> {
    
    
            throw IllegalArgumentException(....)
        }
        is ImageVector -> {
    
    
            throw IllegalArgumentException(....)
        }
        is Painter -> {
    
    
            throw IllegalArgumentException(....)
        }
    }
    return data
}

imageloading-core这次如何抽象图片加载行为?我们先观察一下rememberLoadPainter的参数列表:

@Composable
fun <R> rememberLoadPainter(
    loader: Loader<R>,
    request: R?,
    shouldRefetchOnSizeChange: ShouldRefetchOnSizeChange,
    fadeIn: Boolean = false,
    fadeInDurationMs: Int = LoadPainterDefaults.FadeInTransitionDuration,
    @DrawableRes previewPlaceholder: Int = 0,
): LoadPainter<R> {
    
    ...}

@Stable
fun interface Loader<R> {
    
    
    fun load(request: R, size: IntSize): Flow<ImageLoadState>
}

与0.6.2版本不同,加载逻辑实现类需要返回一个状态流Flow<ImageLoadState>,而不再是单一的ImageLoadState,虽然请求类型仍然是泛型的,但是已经不需要表达类似于RequestBuilder这样的泛型类型,如何构建、发起请求由Loader自己决定。

ImageLoadState的实现如下

sealed class ImageLoadState {
    
    
    object Empty : ImageLoadState()
    data class Loading(
        val placeholder: Painter?,
        val request: Any,
    ) : ImageLoadState()
    data class Success(
        val result: Painter,
        val source: DataSource,
        val request: Any,
    ) : ImageLoadState()
    data class Error(
        val request: Any,
        val result: Painter? = null,
        val throwable: Throwable? = null
    ) : ImageLoadState()
}

不难发现所有的图片加载结果都要求封装成Painter进行返回,但尴尬的是,Drawable与Painter并不是天生互通的类型(Compose 1.0.5只有三种Painter,BitmapPainter、VectorPainter、ColorPainter),好在Accompanist提供了一个DrawablePainter。不过话又说回来,为什么非得要求生产者Loader返回Painter不可呢?那是因为加载请求是多类型的,消费者LoadPainter其实无法确定生产者返回的结果的类型,自然也不确定如何绘制它,因此LoadPainter采用了类似于装饰者模式的设计,图片结果绘制交由State内的Painter完成。

GlideLoader的实现如下:

internal class GlideLoader(
    requestManager: RequestManager,
    requestBuilder: (RequestBuilder<Drawable>.(size: IntSize) -> RequestBuilder<Drawable>)?,
) : Loader<Any> {
    
    
    var requestManager by mutableStateOf(requestManager)
    var requestBuilder by mutableStateOf(requestBuilder)

    /**
     * 不要删除callbackFlow上的显式类型<ImageLoadState>。IR编译器不喜欢隐式类型。
     */
    @Suppress("RemoveExplicitTypeArguments")
    @OptIn(ExperimentalCoroutinesApi::class)
    override fun load(
        request: Any,
        size: IntSize
    ): Flow<ImageLoadState> = callbackFlow<ImageLoadState> {
    
    
        var failException: Throwable? = null
        // 这里同时使用Target与Listener两种机制来监听加载状态,并向flow发送对应状态
        // Target并不会去处理Success的状态,Listener已经抢先处理并拦截了Target的Success调用
        val target = object : EmptyCustomTarget(
            if (size.width > 0) size.width else Target.SIZE_ORIGINAL,
            if (size.height > 0) size.height else Target.SIZE_ORIGINAL
        ) {
    
    
            override fun onLoadStarted(placeholder: Drawable?) {
    
    
                trySendBlocking(
                    ImageLoadState.Loading(
                        placeholder = placeholder?.let(::DrawablePainter),
                        request = request
                    )
                )
            }

            override fun onLoadFailed(errorDrawable: Drawable?) {
    
    
                trySendBlocking(
                    ImageLoadState.Error(
                        result = errorDrawable?.let(::DrawablePainter),
                        request = request,
                        throwable = failException
                            ?: IllegalArgumentException("Error while loading $request")
                    )
                )
                // Close the channel[Flow]
                channel.close()
            }

            override fun onLoadCleared(resource: Drawable?) {
    
    
                // Glide想要释放资源,所以我们需要清除结果,否则我们可能会绘制已经被回收的视图
                trySendBlocking(ImageLoadState.Empty)
                // Close the channel[Flow]
                channel.close()
            }
        }

        val listener = object : RequestListener<Drawable> {
    
    
            override fun onResourceReady(
                drawable: Drawable,
                model: Any,
                target: Target<Drawable>,
                dataSource: com.bumptech.glide.load.DataSource,
                isFirstResource: Boolean
            ): Boolean {
    
    
                // 这里发送的Painter类型
                trySendBlocking(
                    ImageLoadState.Success(
                        result = DrawablePainter(drawable),
                        source = dataSource.toDataSource(),
                        request = request
                    )
                )
                // Close the channel[Flow]
                channel.close()
                // Return true so that the target doesn't receive the drawable
                // 这里返回true,Target就收不到结果了
                return true
            }

            override fun onLoadFailed(
                e: GlideException?,
                model: Any,
                target: Target<Drawable>,
                isFirstResource: Boolean
            ): Boolean {
    
    
                // Glide只为Listener派发错误的Exception,因此这里需要缓存一下
                failException = e
                // 返回false,允许Target被回调onLoadFailed
                return false
            }
        }

        // Start the image request into the target
        requestManager.load(request)
            .apply {
    
     requestBuilder?.invoke(this, size) }
            .addListener(listener)
            .into(target)

        // Await the channel being closed and request finishing...
        awaitClose {
    
    
            // 这里没有调用Glide.clear(),因为clear之后Painter进行绘制的位图可能会被回收,这会报错
            // See https://github.com/google/accompanist/issues/419
        }
    }
}

总体来说状态转换逻辑和以前类似,只不过使用callbackFlow生成数据流后,状态发送显得更加优雅了。

接下来关注rememberLoadPainter的具体实现:

/**
一个通用的 image loading painter,它为要实现的图像加载库提供Loader接口。应用程序通常不应该使用此功能,而更推荐使用在此基础上构建的扩展库,例如Coil和Glide库。
*/
@Composable
fun <R> rememberLoadPainter(
    loader: Loader<R>,
    request: R?,
    shouldRefetchOnSizeChange: ShouldRefetchOnSizeChange,
    fadeIn: Boolean = false,
    fadeInDurationMs: Int = LoadPainterDefaults.FadeInTransitionDuration,
    @DrawableRes previewPlaceholder: Int = 0,
): LoadPainter<R> {
    
    
    val coroutineScope = rememberCoroutineScope()

    // Our LoadPainter. This invokes the loader as appropriate to display the result.
    val painter = remember(loader, coroutineScope) {
    
    
        LoadPainter(loader, coroutineScope)
    }
    painter.request = request
    painter.shouldRefetchOnSizeChange = shouldRefetchOnSizeChange
    // 缓存父布局的大小,在计算图片请求的大小时会参考此值
    painter.rootViewSize = LocalView.current.let {
    
     IntSize(it.width, it.height) }

    // fadeIn动画的ColorFilter
    // 实现原理和0.6.2版本类似,也是修改了ColorFliter的alpha(透明度),
    // brightness(亮度),saturation(饱和度),不过这次的ColorFliter由LoadPainter直接进行处理
    animateFadeInColorFilter(
        painter = painter,
        enabled = {
    
     result ->
            // 从 disk/network 才去展示fadeIn动画
            // 这使我们可以近似地只在“首次加载”时运行动画
            fadeIn && result is ImageLoadState.Success && result.source != DataSource.MEMORY
        },
        durationMs = fadeInDurationMs,
    )

    // Our result painter, created from the ImageState with some composition lifecycle
    // callbacks
    // 我们的result painter,通过一些composition生命周期的回调从ImageState创建
    updatePainter(painter, previewPlaceholder)

    return painter
}

LoaderPainter的实现如下。这里要特别注意RememberObserver这个接口,RememberObserver是一个能够实现对remember行为的观察的接口,如果composition记住或者遗忘的是一个RememberObserver对象,RememberObserver能够收到这个事件,这些事件对LoaderPainter很有用。因为LoaderPainter毕竟并不是一个Compose组件,但是它必须了解它所在的父组件在什么时候离开了屏幕被销毁了(例如高速滑动列表时),这样它能够及时取消对状态流Flow<ImageLoadState>的收集,这是避免发生图片闪烁、错位等问题的关键。

class LoadPainter<R> internal constructor(
    private val loader: Loader<R>,
    private val coroutineScope: CoroutineScope,
) : Painter(), RememberObserver {
    
    
    private val paint by lazy(LazyThreadSafetyMode.NONE) {
    
     Paint() }

    internal var painter by mutableStateOf<Painter>(EmptyPainter)
    // 这个ColorFilter和渐入动画有关
    internal var transitionColorFilter by mutableStateOf<ColorFilter?>(null)
    // CoroutineScope for the current request
    private var requestCoroutineScope: CoroutineScope? = null
    /**
     * The current request object.
     */
    var request by mutableStateOf<R?>(null)
    /**
     * The root view size.
     */
    internal var rootViewSize by mutableStateOf(IntSize(0, 0))
    /**
     * Lambda which will be invoked when the size changes, allowing
     * optional re-fetching of the image.
     */
    var shouldRefetchOnSizeChange by mutableStateOf(ShouldRefetchOnSizeChange {
    
     _, _ -> false })

    /**
     * The current [ImageLoadState].
     * 被观察的ImageLoadState
     */
    var loadState: ImageLoadState by mutableStateOf(ImageLoadState.Empty)
        private set

    private var alpha: Float by mutableStateOf(1f)
    private var colorFilter: ColorFilter? by mutableStateOf(null)

    /**
     * 执行图像加载请求时要使用的大小
     */
    private var requestSize by mutableStateOf<IntSize?>(null)

    // Painter内的属性,指定边界大小
    override val intrinsicSize: Size
        get() = painter.intrinsicSize

    override fun applyAlpha(alpha: Float): Boolean {
    
    
        this.alpha = alpha
        return true
    }

    override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
    
    
        this.colorFilter = colorFilter
        return true
    }

    override fun DrawScope.onDraw() {
    
    
        // 根据Canvas的大小确定requestSize,是不是注意到requestSize的确定其实是存在延时的?
        updateRequestSize(canvasSize = size)
        
        // 下面是一些绘制逻辑
        val transitionColorFilter = transitionColorFilter
        if (colorFilter != null && transitionColorFilter != null) {
    
    
            // If we have a transition color filter, 
            // and a specified color filter we need to
            // draw the content in a layer for both to apply.
            // See https://github.com/google/accompanist/issues/262
           drawIntoCanvas {
    
     canvas ->
                paint.colorFilter = transitionColorFilter
                canvas.saveLayer(bounds = size.toRect(), paint = paint)
                with(painter) {
    
    
                    draw(size, alpha, colorFilter)
                }
                canvas.restore()
        } else {
    
    
            // Otherwise we just draw the content directly, using the filter
            with(painter) {
    
    
                draw(size, alpha, colorFilter ?: transitionColorFilter)
            }
        }
    }
    // RememberObserver的方法
    // remember运行了计算的lambda但是composition没记住这个对象时回调
    override fun onAbandoned() {
    
    
        // We've been abandoned from composition, so cancel our request scope
        requestCoroutineScope?.cancel()
        requestCoroutineScope = null
    }
    // RememberObserver的方法
    // composition忘记了这个对象时回调
    override fun onForgotten() {
    
    
        // We've been forgotten from composition, so cancel our request scope
        // onAbandoned和onForgotten时都会cancel运行中的协程
        requestCoroutineScope?.cancel()
        requestCoroutineScope = null
    }
    // RememberObserver的方法
    // 当composition成功记住此对象时调用。
    override fun onRemembered() {
    
    
        // Cancel any on-going scope (this shouldn't really happen anyway)
        // 先取消以前正running的协程
        requestCoroutineScope?.cancel()

        // 为当前请求创建新的scope,这允许我们取消作用域,而不影响父作用域的作业。
        val scope = coroutineScope.coroutineContext.let {
    
     context ->
            CoroutineScope(context + Job(context[Job]))
        }.also {
    
     requestCoroutineScope = it }

        // 我们已经被记住了,所以可以启动一个协程来观察当前的请求对象和请求大小。
        // 每当这些值中的任何一个发生变化时,collectLatest块将运行并执行图像加载(任何正在进行的请求都将被取消)。
        scope.launch {
    
    
            // combine方法如其名,能把两个流合并成一个流
            // 不过为什么这里要使用snapshotFlow把State转化成流呢?
            // 因为使用流来监听State变化的最大好处就是collectLatest能够
            // 取消掉上一次的execute调用并启动新一轮的加载
            combine(
                snapshotFlow {
    
     request },
                snapshotFlow {
    
     requestSize },
                transform = {
    
     request, size -> request to size }
            ).collectLatest {
    
     (request, size) ->
                execute(request, size)
            }
        }

        // 自动保险。如果没有从onDraw()获得合适的大小,
        // 我们会将请求大小更新为-1,-1,这将加载原始大小的图像。
        scope.launch {
    
    
            if (requestSize == null) {
    
    
                // 32ms should be enough time for measure/layout/draw to happen.
                // 微妙的32毫秒
                delay(32) 

                if (requestSize == null) {
    
    
                   // If we still don't have a request size, resolve the size without
                   // the canvas size
                    // 没获取到Canvas大小,使用原始尺寸
                    updateRequestSize(canvasSize = Size.Zero)
                }
            }
        }
    }

    /**
     * 执行图片加载请求并根据结果更新loadState的方法
     下面描述的是一些状态转换逻辑,比如如果请求为null,状态就转变为Empty
     */
    private suspend fun execute(request: R?, size: IntSize?) {
    
    
        if (request == null || size == null) {
    
    
            // If we don't have a request, set our state to Empty and return
            loadState = ImageLoadState.Empty
            return
        }
        // ...

        loader.load(request, size)
            .catch {
    
     throwable ->
                when (throwable) {
    
    
                    is Error -> throw throwable
                    is IllegalStateException -> throw throwable
                    is IllegalArgumentException -> throw throwable
                    else -> {
    
    
                        emit(
                            ImageLoadState.Error(
                                result = null,
                                throwable = throwable,
                                request = request
                            )
                        )
                    }
                }
            }
            .collect {
    
     loadState = it }
        // 上面collect收集了加载的状态,注意,代表图片结果的Painter没被设置到LoadPainter的字段内
    }

    private fun updateRequestSize(canvasSize: Size) {
    
    
        requestSize = IntSize(
            width = when {
    
    
                // If we have a canvas width, use it...
                canvasSize.width >= 0.5f -> canvasSize.width.roundToInt()
                // 还记得这个rootViewSize吗?它在rememberLoadPainter函数内被设置
                rootViewSize.width > 0 -> rootViewSize.width
                else -> -1
            },
            height = when {
    
    
                // If we have a canvas height, use it...
                canvasSize.height >= 0.5f -> canvasSize.height.roundToInt()
                // Otherwise we fall-back to the root view size as an upper bound
                rootViewSize.height > 0 -> rootViewSize.height
                else -> -1
            },
        )
    }
}

虽然说LoadPainter确实是实现了RememberObserver,但是,这个回调是怎么被注册的呢?答案藏在习以为常的remember函数中,传入remember的key,或者是calculation得出的值,它们如果是个RememberObserver,则会被插入到RememberManager的队列中,每当“记忆”和“遗忘”事件发生时都会得到通知。

@Composable
inline fun <T> remember(
    key1: Any?,
    calculation: @DisallowComposableCalls () -> T
): T {
    
    
    return currentComposer.cache(currentComposer.changed(key1), calculation)
}
// 注意检查key是否有变化的changed函数
@ComposeCompilerApi
override fun changed(value: Any?): Boolean {
    
    
    return if (nextSlot() != value) {
    
    
        updateValue(value)
        true
    } else {
    
    
        false
    }
}

@PublishedApi
@OptIn(InternalComposeApi::class)
internal fun updateValue(value: Any?) {
    
    
    // 两个if分支我们都可以看到 rememberManager.remembering()
    // rememberManager.forgetting()这些调用
    if (inserting) {
    
    
        writer.update(value)
        if (value is RememberObserver) {
    
    
            // 注意,判断value是不是RememberObserver
            record {
    
     _, _, rememberManager -> rememberManager.remembering(value) }
        }
    } else {
    
    
        val groupSlotIndex = reader.groupSlotIndex - 1
        recordSlotTableOperation(forParent = true) {
    
     _, slots, rememberManager ->
            if (value is RememberObserver) {
    
    
                abandonSet.add(value)
                rememberManager.remembering(value)
            }
            when (val previous = slots.set(groupSlotIndex, value)) {
    
    
                is RememberObserver ->
                    rememberManager.forgetting(previous)
                is RecomposeScopeImpl -> {
    
    
                    val composition = previous.composition
                    if (composition != null) {
    
    
                        previous.composition = null
                        composition.pendingInvalidScopes = true
                    }
                }
            }
        }
    }
}
// RememberManager是个接口
internal interface RememberManager {
    
    
    /**
     * The [RememberObserver] is being remembered by a slot in the slot table.
     */
    fun remembering(instance: RememberObserver)

    /**
     * The [RememberObserver] is being forgotten by a slot in the slot table.
     */
    fun forgetting(instance: RememberObserver)
    
    ...
}
// RememberManager的实现类
private class RememberEventDispatcher(
    private val abandoning: MutableSet<RememberObserver>
) : RememberManager {
    
    
    private val remembering = mutableListOf<RememberObserver>()
    private val forgetting = mutableListOf<RememberObserver>()
    private val sideEffects = mutableListOf<() -> Unit>()

    override fun remembering(instance: RememberObserver) {
    
    
        forgetting.lastIndexOf(instance).let {
    
     index ->
            if (index >= 0) {
    
    
                forgetting.removeAt(index)
                abandoning.remove(instance)
            } else {
    
    
                remembering.add(instance)
            }
        }
    }

    override fun forgetting(instance: RememberObserver) {
    
    
        remembering.lastIndexOf(instance).let {
    
     index ->
            if (index >= 0) {
    
    
                remembering.removeAt(index)
                abandoning.remove(instance)
            } else {
    
    
                forgetting.add(instance)
            }
        }
    }
    fun dispatchRememberObservers() {
    
    
        // 派发forgetting和remembering事件的逻辑
        if (forgetting.isNotEmpty()) {
    
    
            for (i in forgetting.size - 1 downTo 0) {
    
    
                val instance = forgetting[i]
                if (instance !in abandoning) {
    
    
                    instance.onForgotten()
                }
            }
        }
        if (remembering.isNotEmpty()) {
    
    
            remembering.fastForEach {
    
     instance ->
                abandoning.remove(instance)
                instance.onRemembered()
            }
        }
    }
    // ....
}

我们已经明白LoadPainter到底是怎么管理Loader返回的流结果了,最后一个需要注意的地方在函数updatePainter里,这个调用位于rememberLoadPainter最后,函数实现会根据图片加载State的变化来为LoadPainter设置Painter。不过这不是兜了个圈子吗?似乎也可以在collect更新State的同时把Painter更新一下?

/**
* 允许我们以状态观察当前结果。这个函数允许我们最小化重组范围,这样当loadState改变时,只有这个函数需要重新
* 启动。
*/
@Composable
private fun <R> updatePainter(
    loadPainter: LoadPainter<R>,
    @DrawableRes previewPlaceholder: Int = 0,
) {
    
    
    loadPainter.painter = if (LocalInspectionMode.current && previewPlaceholder != 0) {
    
    
        // 如果我们处于检查模式(预览),并且有一个预览占位符,只需使用图像绘制它并返回
        // 还记得rememberGlidePainter的参数吗?这里就是传入的参数previewPlaceholder的用途
        // 这个函数令LoadPainter完全忽略了State的变化,只展示静态图片
        painterResource(previewPlaceholder)
    } else {
    
    
        // remember在这里看上去像是毫无必要的调用,
        // 但这允许任何Painter实例接收记忆事件(如果它实现了RememberObserver)。不要移除。
        remember(loadPainter.loadState) {
    
     loadPainter.loadState.painter } ?: EmptyPainter
    }
}

现在来总结一下0.13.0版本的Glide远程图片扩展的实现思路:

  1. 图片加载:依然是用Target回调获取加载的结果。但是加载状态的返回现在使用流(Flow)来封装,不管是发起加载,异常处理,加载取消都更加优雅直观了。Loader是彻彻底底的生产者,LoadPainter则是消费者。

    LoadPainter并不具有@Composable上下文,作为替代,它实现了RememberObserver来监听控件是否已经离屏销毁。

  2. 图片大小约束:依赖于LoadPainter获取的Canvas的大小。

  3. 渐入动画实现:跟0.6.2版本的思路相似,不过消费ColorFilter的类变成了LoadPainter。

  4. loading占位图、error图等:这些功能直接依赖于具体的图片加载框架的实现,有则有,无则无。0.13.0版本稍微舍去了一些灵活性,不能够像PicassoImage一样直接传入error、loading的Compose内容(控件),不过仍然留有监听图片加载状态的方式,注意,LoadPainter的loadState字段是公开的:

    /**
     * The current [ImageLoadState].
     */
    var loadState: ImageLoadState by mutableStateOf(ImageLoadState.Empty)
        private set
    

Coil

Accompanist内的Coil集成库最终集成到了Coil内部,成为其扩展,Glide的集成支持则在2021年8月的0.16.0版本被删除。

现在我们简要分析Coil的图片加载逻辑(版本2.0.0-alpha06)。Coil扩展库提供了两种方式来加载网络图片,两种方式正巧就是上面提到的在0.6.2版本与在0.13.0版本的两种实现形式:

// 实现形式1
@Composable
fun AsyncImage(
    model: Any?,
    contentDescription: String?,
    imageLoader: ImageLoader,
    modifier: Modifier = Modifier,
    loading: @Composable (AsyncImageScope.(State.Loading) -> Unit)? = null,
    success: @Composable (AsyncImageScope.(State.Success) -> Unit)? = null,
    error: @Composable (AsyncImageScope.(State.Error) -> Unit)? = null,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null,
    filterQuality: FilterQuality = DefaultFilterQuality,
) {
    
    ...}
// 实现形式2
@Composable
fun rememberAsyncImagePainter(
    model: Any?,
    imageLoader: ImageLoader,
    filterQuality: FilterQuality = DefaultFilterQuality,
): AsyncImagePainter {
    
    ...}

我们重点分析第二种形式,即rememberAsyncImagePainter函数,其实该函数的实现逻辑与Glide扩展库比较类似,只在某些细节有所区别:

// 这里不再详细分析源码,挑重要的讲
@Composable
fun rememberAsyncImagePainter(
    model: Any?,
    imageLoader: ImageLoader,
    filterQuality: FilterQuality = DefaultFilterQuality,
): AsyncImagePainter {
    
    
    val request = requestOf(model)
    requireSupportedData(request.data)
    // 注意这里,这里要求request的target为null
    require(request.target == null) {
    
     "request.target must be null." }

    // Dispatchers.Main.immediate是一个有趣的协程调度器,具体效果见类注释
    val scope = rememberCoroutineScope {
    
     Dispatchers.Main.immediate }
    // AsyncImagePainter
    val painter = remember(scope) {
    
     AsyncImagePainter(scope, request, imageLoader) }
    painter.request = request
    painter.imageLoader = imageLoader
    painter.filterQuality = filterQuality
    // 是否处于预览模式
    painter.isPreview = LocalInspectionMode.current
    // 这里手动调用了一次onRemembered,onRemembered里有向ImageLoader提交request的逻辑
    painter.onRemembered() // Invoke this manually so `painter.state` is up to date immediately.
    // 这里的updatePainter更加复杂,里面有处理fadeIn动画的逻辑
    updatePainter(painter, request, imageLoader)
    return painter
}

Dispatchers.Main.immediate比单纯的Dispatchers.Main更加智能,它会减少不必要的调度,当它已经在正确的上下文中,它会立刻执行相应逻辑而无需额外的重新调度。效果类似于下面这样:

suspend fun updateUiElement(val text: String) {
    
    
  /*
   * 假设updateUiElement既会被Main线程调用也会被其他线程调用。
   * 那么,当updateUiElement是在Main线程被调用的,更新uiElement.text 这段代码会直接运行,而换成Dispatchers.Main的话,它会再进行一次到Main的调度(明显这是赘余的调度)。
   */
  withContext(Dispatchers.Main.immediate) {
    
    
    uiElement.text = text
  }
  // Do context-independent logic such as logging
}

接下来我们关注AsyncImagePainter的具体实现:

/**
 * 异步执行ImageRequest并呈现结果的Painter。
 */
class AsyncImagePainter internal constructor(
    private val parentScope: CoroutineScope,
    request: ImageRequest,
    imageLoader: ImageLoader
) : Painter(), RememberObserver {
    
    

    private var rememberScope: CoroutineScope? = null
    // 图片请求的协程的Job
    private var requestJob: Job? = null
    private var drawSize = MutableStateFlow(Size.Zero)

    private var alpha: Float by mutableStateOf(1f)
    private var colorFilter: ColorFilter? by mutableStateOf(null)

    internal var painter: Painter? by mutableStateOf(null)
    internal var filterQuality = DefaultFilterQuality
    internal var isPreview = false

    /** The current [AsyncImagePainter.State]. */
    var state: State by mutableStateOf(State.Empty)
        private set

    var request: ImageRequest by mutableStateOf(request)
        internal set

    var imageLoader: ImageLoader by mutableStateOf(imageLoader)
        internal set

    override val intrinsicSize: Size
        get() = painter?.intrinsicSize ?: Size.Unspecified

    override fun DrawScope.onDraw() {
    
    
        // 绘制逻辑非常清爽
        drawSize.value = size
        // Draw the current painter.
        painter?.apply {
    
     draw(size, alpha, colorFilter) }
    }
    ...

    override fun onRemembered() {
    
    
        // 如果我们处于检查模式(预览),请跳过执行图像请求,并将状态设置为加载。
        // 对于预览模式的支持
        if (isPreview) {
    
    
            val request = request.newBuilder().defaults(imageLoader.defaults).build()
            state = State.Loading(request.placeholder?.toPainter())
            return
        }
        // 与Glide扩展类似,创建了一个子作用域
        if (rememberScope != null) return
        val scope = parentScope + SupervisorJob(parentScope.coroutineContext.job)
        rememberScope = scope

        // 观察当前请求+请求大小,并根据需要启动新请求。
        // Coil天然支持Kotlin协程,无需为生产者额外编写代码
        scope.launch {
    
    
            snapshotFlow {
    
     request }.collect {
    
     request ->
                requestJob?.cancel()
                requestJob = launch {
    
    
                    // execute是挂起函数,返回ImageResult
                    state = imageLoader.execute(updateRequest(request)).toState()
                }
            }
        }
    }

    override fun onForgotten() {
    
    
        rememberScope?.cancel()
        rememberScope = null
        requestJob?.cancel()
        requestJob = null
    }

    override fun onAbandoned() = onForgotten()

    /** Update the [request] to work with [AsyncImagePainter]. */
    private fun updateRequest(request: ImageRequest): ImageRequest {
    
    
        return request.newBuilder()
            .target(
                onStart = {
    
     placeholder ->
                     // 这里获取到placeholder的Painter并更新State为Loading
                    state = State.Loading(placeholder?.toPainter())
                }
            )
            .apply {
    
    
                if (request.defined.sizeResolver == null) {
    
    
                    // Coil内关于设置图片大小的代码
                    // size接受一个SizeResolver,一个含suspend函数的接口
                    // 获取尺寸的函数是挂起函数,非常合理,因为很多时候需要等待控件测量完毕才知道大小
                    size(DrawSizeResolver())
                }
                if (request.defined.precision != Precision.EXACT) {
    
    
                    precision(Precision.INEXACT)
                }
            }
            .build()
    }

    private fun ImageResult.toState() = when (this) {
    
    ....}
    private fun Drawable.toPainter() = when (this) {
    
    ...}

    /** Suspends until the draw size for this [AsyncImagePainter] is unspecified or positive. */
    private inner class DrawSizeResolver : SizeResolver {
    
    

        override suspend fun size() = drawSize
            .mapNotNull {
    
     size ->
                when {
    
    
                    // mapNotNull会将drawSize转化为Flow,同时过滤null值,然后挂起函数first()
                    // 将会返回Flow中传送的第一个值
                    size.isUnspecified -> CoilSize.ORIGINAL
                    size.isPositive -> CoilSize(size.width.roundToInt(), size.height.roundToInt())
                    else -> null
                }
            }
            .first()
    }

    /**
     * The current state of the [AsyncImagePainter].
     * 状态定义
     */
    sealed class State {
    
    
        abstract val painter: Painter?
        object Empty : State() {
    
    
            override val painter: Painter? get() = null
        }
        data class Loading(
            override val painter: Painter?,
        ) : State()
        data class Success(
            override val painter: Painter,
            val result: SuccessResult,
        ) : State()
        data class Error(
            override val painter: Painter?,
            val result: ErrorResult,
        ) : State()
    }
}

与Glide扩展库的思路类似,updatePainter函数会监听AsyncImagePainter的加载状态变化,同时更新AsyncImagePainter内的Painter字段。

@Composable
private fun updatePainter(
    imagePainter: AsyncImagePainter,
    request: ImageRequest,
    imageLoader: ImageLoader
) {
    
    
    // This may look like a useless remember, but this allows any painter instances
    // to receive remember events (if it implements RememberObserver). Do not remove.
    // 与Glide扩展库一样,允许结果Painter实例接收remember事件(如果它实现了RememberObserver)
    val state = imagePainter.state
    val painter = remember(state) {
    
     state.painter }

    // 如果没有CrossfadeTransition(实现渐入变换)的话,直接设置imagePainter.painter并返回
    val transition = request.defined.transitionFactory ?: imageLoader.defaults.transitionFactory
    if (transition !is CrossfadeTransition.Factory) {
    
    
        imagePainter.painter = painter
        return
    }

    // ValueHolder是一个包含static field的数据类,目的是储存state.painter的值,
    // 避免在state.painter值更新后函数rememberCrossfadePainter重组,
    // 与rememberUpdatedState有异曲同工之妙,估计是因为rememberUpdatedState没有
    // 传入key的API(这里要监听request变化),所以这里提供了简易的避免重组的实现
    val loading = remember(request) {
    
     ValueHolder<Painter?>(null) }
    if (state is State.Loading) loading.value = state.painter

    // 必须位于Success状态且图片是从网络或磁盘加载的,才允许启动Crossfade,否则返回即可
    if (state !is State.Success || state.result.dataSource == DataSource.MEMORY_CACHE) {
    
    
        imagePainter.painter = painter
        return
    }

    // Set the crossfade painter.
    // 千呼万唤始出来的CrossfadePainter
    imagePainter.painter = rememberCrossfadePainter(
        key = state,
        start = loading.value,
        end = painter,
        scale = request.scale,
        durationMillis = transition.durationMillis,
        fadeStart = !state.result.isPlaceholderCached,
        preferExactIntrinsicSize = transition.preferExactIntrinsicSize
    )
}
/** A simple mutable value holder that avoids recomposition. */
// 使用静态字段(static)避免重组
private class ValueHolder<T>(@JvmField var value: T)

CrossfadePainter的实现如下:

@Stable
private class CrossfadePainter(
    private var start: Painter?,
    private val end: Painter?,
    private val scale: Scale,
    private val durationMillis: Int,
    private val fadeStart: Boolean,
    private val preferExactIntrinsicSize: Boolean,
) : Painter() {
    
    

    private var invalidateTick by mutableStateOf(0)
    private var startTimeMillis = -1L
    private var isDone = false

    private var maxAlpha: Float by mutableStateOf(1f)
    private var colorFilter: ColorFilter? by mutableStateOf(null)

    override val intrinsicSize get() = computeIntrinsicSize()

    override fun DrawScope.onDraw() {
    
    
        // 如果Alpha变化完毕,直接使用end绘制
        if (isDone) {
    
    
            drawPainter(end, maxAlpha)
            return
        }

        // Initialize startTimeMillis the first time we're drawn.
        val uptimeMillis = SystemClock.uptimeMillis()
        if (startTimeMillis == -1L) {
    
    
            startTimeMillis = uptimeMillis
        }

        // Alpha的百分比 = (当前时间 - 开始时间) / 持续时间
        val percent = (uptimeMillis - startTimeMillis) / durationMillis.toFloat()
        val endAlpha = percent.coerceIn(0f, 1f) * maxAlpha
        val startAlpha = if (fadeStart) maxAlpha - endAlpha else maxAlpha
        isDone = percent >= 1.0

        // Loading占位图渐出,Success图片结果渐入
        drawPainter(start, startAlpha)
        drawPainter(end, endAlpha)

        if (isDone) {
    
    
            start = null
        } else {
    
    
            // Increment this value to force the painter to be redrawn.
            invalidateTick++
        }
    }
    ...
}

现在来总结一下Coil远程图片扩展的实现思路:

  1. 图片加载:Coil对协程提供直接的支持,size函数、execute加载函数本身就是挂起函数,因此无需额外的转换逻辑。而AsyncImagePainter则使用Job来控制图片加载协程。

    AsyncImagePainter并不具有@Composable上下文,作为替代,它实现了RememberObserver来监听控件是否已经离屏销毁。

  2. 图片大小约束:依赖于DrawContext的Size。

  3. 渐入动画实现:依赖于DrawScope.onDraw()内的重绘行为,通过对透明度Alpha的百分比计算来实现,令Loading状态的占位图渐出,Success状态的最终结果渐入。

  4. loading占位图、error图等:由Coil提供具体的实现。

根据上述分析我们可以发现,相比于Glide或是Picasso,基于Kotlin协程实现的图片加载库Coil,的确能够很轻松与Jetpack Compose配合工作。

至此对扩展库的分析已经完毕。横向对比来说,无论是对Picasso还是Glide进行扩展,我们都得额外做一些处理,才能够令本身不支持协程的它们在Compose下正常工作。要注意的是,单纯使用自定义的Target把结果返回到某个State,这种简单的做法在列表中可能会遇到严重的性能问题,因为Glide也好,Picasso也好,它们内部实现中取消图片加载以避免图片错位、闪烁的重要参照物就是ImageView,随着列表滑动不断创建的自定义的Target无法被它们识别并进行相应处理。相比之下基于协程的Coil的加载能够变得简单得多,我们只需要利用Job本身就可以控制加载的协程。

最后

我和另外两位小伙伴最近合作构建了一款仿网易云的Android客户端,项目采用MVVM架构,部分界面使用Compose编写,除此之外,项目中还集成了多线程断点续传组件(by Giagor)与基于原生MediaPlayer进行再封装的音乐Service框架(by lanlin-code)

项目地址:https://github.com/giagor/PureJoy

如果项目对你有所帮助,欢迎点赞、Star、收藏~

猜你喜欢

转载自blog.csdn.net/chong_lai/article/details/122893308