Coil - 基于Kotlin协程的Android图片加载库 - 9

867 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

更多内容

如何对Coil处理一些通用的自定义用例?

Palette

Palette允许从图片中提取突出的颜色. 要创建Palette, 则需要访问图片的Bitmap. 有很多方式可以完成这些操作:

通过设置ImageRequest.Listener和入队ImageRequest, 可以访问图片的Bitmap:

imageView.load("https://www.example.com/image.jpg") {
    // Disable hardware bitmaps as Palette needs to read the image's pixels.
    allowHardware(false)
    listener(
        onSuccess = { _, result ->
            // Create the palette on a background thread.
            Palette.Builder(result.drawable.toBitmap()).generate { palette ->
                // Consume the palette.
            }
        }
    )
}

使用自定义OkHttpClient

Coil使用OkHttp来执行全部的网络操作. 在创建ImageLoader时, 可以指定自定义的OkHttpClient:

val imageLoader = ImageLoader.Builder(context)
    // Create the OkHttpClient inside a lambda so it will be initialized lazily on a background thread.
    .okHttpClient {
        OkHttpClient.Builder()
            .addInterceptor(CustomInterceptor())
            .build()
    }
    .build()

注意

如果已经有了OkHttpClient, 可以用newBuilder()来来构建新的客户端来和原来的共享资源.

Headers

Header可以通过2种方式中的一种来添加进图片请求. 可以给单个请求设置Header:

val request = ImageRequest.Builder(context)
    .data("https://www.example.com/image.jpg")
    .setHeader("Cache-Control", "no-cache")
    .target(imageView)
    .build()
imageLoader.execute(request)

或者创建OkHttp Interceptor来给ImageLoader执行的每一个请求添加Header:

class RequestHeaderInterceptor(
    private val name: String,
    private val value: String
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .header(name, value)
            .build()
        return chain.proceed(request)
    }
}

val imageLoader = ImageLoader.Builder(context)
    .okHttpClient {
        OkHttpClient.Builder()
            // This header will be added to every image request.
            .addNetworkInterceptor(RequestHeaderInterceptor("Cache-Control", "no-cache"))
            .build()
    }
    .build()

将内存缓存的Key用作Placeholder

将以前请求的MemoryCache.Key用作子请求的placeholder可能是有用处的, 如果有2张图片相同, 仅仅通过不同的尺寸加载. 举个例子, 如果第一个请求加载加载图片的尺寸是100x100, 第二个请求加载图片的尺寸是500x500, 则可以将第一个图片作为第二个图片的异步加载占位符.

下面是示例应用中该效果的代码:

列表中的图片有意加载成非常少的细节, 渐变被有意减速以高亮这种视觉效果.

要达到这种效果, 需要使用第一个请求的MemoryCache.Key作为第二个请求的ImageRequest.placeholderMemoryCacheKey:

// First request
listImageView.load("https://www.example.com/image.jpg")

// Second request (once the first request finishes)
detailImageView.load("https://www.example.com/image.jpg") {
    placeholderMemoryCacheKey(listImageView.result.memoryCacheKey)
}

注意

先前版本的Coil会automatically自动地尝试设置这种效果. 这要求图片管道部分在主线程上同步地执行, 由此在版本0.12.0中被彻底地移除.

共享元素转场

共享元素转场允许在ActivitiesFragments之间执行动画. 推荐在Coil中这样处理:

  • 共享元素转场和硬件Bitmap是不兼容的  应该设置allowHardware(false)以禁止硬件Bitmap, 无论是对动画起始的ImageView还是动画结束的View. 如果不这么设置, 转场动画会抛出java.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmaps异常.
  • 将首张图片的MemoryCache.Key作为最后图片的placeholderMemoryCacheKey. 这保证了首张图片用作最后图片的占位符, 这将产生顺滑的转场且没有白闪, 尤其是如图片在内存缓存中的话.
  • 联合使用ChangeImageTransformChangeBounds以达到最佳效果.

RemoteView

Coil原生并不提供RemoteViewTarget, 但可以像这样创建一个:

class RemoteViewsTarget(
    private val context: Context,
    private val componentName: ComponentName,
    private val remoteViews: RemoteViews,
    @IdRes private val imageViewResId: Int
) : Target {

    override fun onStart(placeholder: Drawable?) = setDrawable(placeholder)

    override fun onError(error: Drawable?) = setDrawable(error)

    override fun onSuccess(result: Drawable) = setDrawable(result)

    private fun setDrawable(drawable: Drawable?) {
        remoteViews.setImageViewBitmap(imageViewResId, drawable?.toBitmap())
        AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteViews)
    }
}

然后将请求正常地enqueue/execute:

val request = ImageRequest.Builder(context)
    .data("https://www.example.com/image.jpg")
    .target(RemoteViewsTarget(context, componentName, remoteViews, imageViewResId))
    .build()
imageLoader.enqueue(request)

Transforming Painters

AsyncImageAsyncImagePainter都有placeholder/error/fallback参数接收Painter. Painter并滑使用Composable灵活但却更快, 这是因为Coil不需要支持subcomposition. 即便如此, 将Painter插入, 伸展, 着色, 或者转换以得到想要的UI也是很必要的. 要完成这个目标, 可以这样做:

AsyncImage(
    model = "https://example.com/image.jpg",
    contentDescription = null,
    placeholder = forwardingPainter(
        painter = painterResource(R.drawable.placeholder),
        colorFilter = ColorFilter(Color.Red),
        alpha = 0.5f
    )
)

onDraw可以使用尾置lambda进行覆盖:

AsyncImage(
    model = "https://example.com/image.jpg",
    contentDescription = null,
    placeholder = forwardingPainter(painterResource(R.drawable.placeholder)) { info ->
        inset(50f, 50f) {
            with(info.painter) {
                draw(size, info.alpha, info.colorFilter)
            }
        }
    }
)