Skip to content

レシピ

このページでは、Coilを使用した一般的なユースケースの処理方法について説明します。要件に合わせてこのコードを修正する必要があるかもしれませんが、解決へのヒントとして役立つはずです。

ここに記載されていない一般的なユースケースをご存知ですか?新しいセクションを追加するPR(プルリクエスト)をぜひ送ってください。

Palette

Palette を使用すると、画像から主要な色を抽出できます。Palette を作成するには、画像の Bitmap にアクセスする必要があります。これにはいくつかの方法があります。

ImageRequest.Listener を設定し、ImageRequest を実行(enqueue)することで、画像のビットマップにアクセスできます。

kotlin
imageView.load("https://example.com/image.jpg") {
    // Paletteは画像のピクセルを読み取る必要があるため、ハードウェアビットマップを無効にします。
    allowHardware(false)
    listener(
        onSuccess = { _, result ->
            // バックグラウンドスレッドでパレットを作成します。
            Palette.Builder(result.image.toBitmap()).generate { palette ->
                // パレットを使用します。
            }
        }
    )
}

メモリキャッシュキーをプレースホルダーとして使用する

2つの画像が同じもので、異なるサイズで読み込まれる場合、前のリクエストの MemoryCache.Key を後続のリクエストのプレースホルダーとして使用すると便利です。例えば、最初のリクエストで画像を 100x100 で読み込み、2番目のリクエストで 500x500 で読み込む場合、最初の画像を2番目のリクエストの同期プレースホルダーとして使用できます。

サンプルアプリでのこのエフェクトの様子は以下の通りです。

リスト内の画像は、視覚的な効果を強調するために、意図的に非常に低い詳細度で読み込まれ、クロスフェードの速度を落としています。

このエフェクトを実現するには、最初のリクエストの MemoryCache.Key を2番目のリクエストの ImageRequest.placeholderMemoryCacheKey として使用します。例を以下に示します。

kotlin
// 1番目のリクエスト
listImageView.load("https://example.com/image.jpg")

// 2番目のリクエスト(1番目のリクエストが完了した後)
detailImageView.load("https://example.com/image.jpg") {
    placeholderMemoryCacheKey(listImageView.result.memoryCacheKey)
}

共通要素遷移(Shared Element Transitions)

共通要素遷移(Shared element transitions) を使用すると、ActivitiesFragments の間でアニメーションを行うことができます。Coilでこれらを動作させるための推奨事項は以下の通りです。

  • 共通要素遷移はハードウェアビットマップと互換性がありません。 アニメーション元の ImageView とアニメーション先のビューの両方で、allowHardware(false) を設定してハードウェアビットマップを無効にする必要があります。そうしないと、遷移時に java.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmaps 例外がスローされます。

  • 開始画像の MemoryCache.Key を終了画像の placeholderMemoryCacheKey として使用してください。これにより、開始画像が終了画像のプレースホルダーとして使用され、画像がメモリキャッシュにある場合に白いフラッシュ(点滅)が発生せず、スムーズな遷移が可能になります。

  • 最適な結果を得るために、ChangeImageTransformChangeBounds を組み合わせて使用してください。

Composeを使用していますか? AsyncImage で共通要素遷移を行う方法については、こちらの記事を確認してください。

Remote Views

Coilは標準で RemoteViews 用の Target を提供していませんが、以下のように作成することができます。

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

    override fun onStart(placeholder: Image?) = setImage(placeholder)

    override fun onError(error: Image?) = setImage(error)

    override fun onSuccess(result: Image) = setImage(result)

    private fun setImage(image: Image?) {
        remoteViews.setImageViewBitmap(imageViewResId, image?.toBitmap())
        AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteViews)
    }
}

その後、通常通りリクエストを enqueue/execute します。

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

Painterの変換

AsyncImageAsyncImagePainter はどちらも Painter を受け取る placeholder/error/fallback 引数を持っています。Painterはコンポーザブルを使用するよりも柔軟性は低いですが、Coilがサブコンポジションを使用する必要がないため高速です。とはいえ、目的のUIを実現するために、Painterをインセット(inset)、ストレッチ、ティント(tint)、または変形(transform)させる必要がある場合があります。これを実現するには、こちらのGistをプロジェクトにコピーし、以下のようにPainterをラップします。

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

後置ラムダ(trailing lambda)を使用して onDraw を上書きできます。

kotlin
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)
            }
        }
    },
)

リクエストの変換

画像をフェッチするために使用されるHTTPリクエストを変換する必要がある場合があります。この例では、Interceptor を使用して、リクエストURLに widthheight のクエリパラメータを追加します。

kotlin
class UrlSizeInterceptor : Interceptor {

    override suspend fun intercept(chain: Chain): ImageResult {
        val request = chain.request
        val uri = request.uri

        if (uri == null || uri.scheme !in setOf("https", "http")) {
            // HTTP以外のリクエストは無視します。
            return chain.proceed()
        }

        val (width, height) = chain.size
        return if (width is Pixels && height is Pixels) {
            val transformUri = uri.newBuilder()
                .query("width=${width.px}&height=${height.px}")
                .build()

            val transformedRequest = request.newBuilder()
                .data(transformUri)
                .build()
            return chain.withRequest(transformedRequest).proceed()
        } else {
            // 無限の制約(infinite constraints)などの理由で、幅と高さが利用できない場合。
            chain.proceed()
        }
    }

    private val ImageRequest.uri: Uri?
        get() = when (val data = data) {
            is Uri -> data
            is coil3.Uri -> data.toAndroidUri()
            is String -> data.toUri()
            else -> null
        }
}

ImageLoader にインターセプターを追加するのを忘れないでください!

kotlin
ImageLoader.Builder(context)
    .components {
        add(UrlSizeInterceptor())
    }
    .build()