Android Coil3缩略图、默认占位图placeholder、error加载错误显示,Kotlin(1)

Android Coil3缩略图、默认占位图placeholder、error加载错误显示,Kotlin(1)

复制代码
    implementation("io.coil-kt.coil3:coil-core:3.1.0")
    implementation("io.coil-kt.coil3:coil-network-okhttp:3.1.0")
XML 复制代码
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
Kotlin 复制代码
import android.content.ContentUris
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.request.CachePolicy
import coil3.request.bitmapConfig
import android.os.Environment
import okio.Path.Companion.toPath
import java.io.File

class MainActivity : AppCompatActivity() {
    companion object {
        const val SPAN_COUNT = 8

        const val THUMB_WIDTH = 20
        const val THUMB_HEIGHT = 20
    }

    private var mImageLoader: ImageLoader? = null

    private val TAG = "fly/MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val rv = findViewById<RecyclerView>(R.id.rv)

        initCoil()

        val layoutManager = GridLayoutManager(this, SPAN_COUNT)
        layoutManager.orientation = LinearLayoutManager.VERTICAL
        val adapter = ImageAdapter(this, mImageLoader)
        rv.adapter = adapter
        rv.layoutManager = layoutManager

        rv.setItemViewCacheSize(SPAN_COUNT * 2)
        rv.recycledViewPool.setMaxRecycledViews(0, SPAN_COUNT * 2)

        val ctx = this
        lifecycleScope.launch(Dispatchers.IO) {
            val imgList = readAllImage(ctx)
            val videoList = readAllVideo(ctx)

            Log.d(TAG, "readAllImage size=${imgList.size}")
            Log.d(TAG, "readAllVideo size=${videoList.size}")

            val lists = arrayListOf<MyData>()
            lists.addAll(imgList)
            lists.addAll(videoList)
            lists.shuffle()

            lifecycleScope.launch(Dispatchers.Main) {
                adapter.dataChanged(lists)
            }
        }
    }

    private fun initCoil() {
        val ctx = this

        //初始化加载器。
        mImageLoader = ImageLoader.Builder(this)
            .memoryCachePolicy(CachePolicy.ENABLED)
            .memoryCache(initMemoryCache())
            .diskCachePolicy(CachePolicy.ENABLED)
            .diskCache(initDiskCache())
            .networkCachePolicy(CachePolicy.ENABLED)
            .bitmapConfig(Bitmap.Config.ARGB_8888)
            .components {
                //add(ThumbInterceptor())
                //add(ThumbMapper())
                //add(ImageKeyer())
                add(ThumbFetcher.Factory(ctx))
                //add(ThumbDecoder.Factory())
            }.build()

        Log.d(TAG, "memoryCache.maxSize=${mImageLoader?.memoryCache?.maxSize}")
    }

    private fun initMemoryCache(): MemoryCache {
        //内存缓存。
        val memoryCache = MemoryCache.Builder()
            .maxSizeBytes(1024 * 1024 * 1024 * 1L) //1GB
            .build()

        return memoryCache
    }


    private fun initDiskCache(): DiskCache {
        //磁盘缓存。
        val diskCacheFolder = Environment.getExternalStorageDirectory()
        val diskCacheName = "coil_disk_cache"

        val cacheFolder = File(diskCacheFolder, diskCacheName)
        if (cacheFolder.exists()) {
            Log.d(TAG, "${cacheFolder.absolutePath} exists")
        } else {
            if (cacheFolder.mkdir()) {
                Log.d(TAG, "${cacheFolder.absolutePath} create OK")
            } else {
                Log.e(TAG, "${cacheFolder.absolutePath} create fail")
            }
        }

        val diskCache = DiskCache.Builder()
            .maxSizeBytes(1024 * 1024 * 1024 * 2L) //2GB
            .directory(cacheFolder.absolutePath.toPath())
            .build()

        Log.d(TAG, "cache folder = ${diskCache.directory.toFile().absolutePath}")


        return diskCache
    }

    class MyData(var path: String, var uri: Uri)

    private fun readAllImage(ctx: Context): ArrayList<MyData> {
        val photos = ArrayList<MyData>()

        //读取所有图
        val cursor = ctx.contentResolver.query(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null
        )

        while (cursor!!.moveToNext()) {
            //路径
            val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))

            val id = cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)
            val imageUri: Uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cursor.getLong(id))

            //名称
            //val name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))

            //大小
            //val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE))

            photos.add(MyData(path, imageUri))
        }
        cursor.close()

        return photos
    }

    private fun readAllVideo(context: Context): ArrayList<MyData> {
        val videos = ArrayList<MyData>()

        //读取视频Video
        val cursor = context.contentResolver.query(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            null,
            null,
            null,
            null
        )

        while (cursor!!.moveToNext()) {
            //路径
            val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA))

            val id = cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)
            val videoUri: Uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cursor.getLong(id))

            //名称
            //val name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME))

            //大小
            //val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE))

            videos.add(MyData(path, videoUri))
        }
        cursor.close()

        return videos
    }
}
Kotlin 复制代码
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import android.util.Size
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatImageView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import coil3.Image
import coil3.ImageLoader
import coil3.asImage
import coil3.memory.MemoryCache
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.request.target
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class ImageAdapter : RecyclerView.Adapter<ImageHolder> {
    private var mCtx: Context? = null
    private var mImageLoader: ImageLoader? = null
    private var mViewSize = 0
    private var mPlaceholderImage: Image? = null
    private var mErrorBmp: Bitmap? = null

    private val TAG = "fly/ImageAdapter"

    constructor(ctx: Context, il: ImageLoader?) : super() {
        mCtx = ctx
        mImageLoader = il

        mViewSize = mCtx!!.resources.displayMetrics.widthPixels / MainActivity.SPAN_COUNT

        mPlaceholderImage = BitmapFactory.decodeResource(mCtx!!.resources, android.R.drawable.ic_menu_gallery).asImage()
        mErrorBmp = BitmapFactory.decodeResource(mCtx!!.resources, android.R.drawable.stat_notify_error)
    }

    private var mItems = ArrayList<MainActivity.MyData>()

    fun dataChanged(items: ArrayList<MainActivity.MyData>) {
        this.mItems = items
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageHolder {
        val view = MyIV(mCtx!!, mViewSize)
        return ImageHolder(view)
    }

    override fun getItemCount(): Int {
        return mItems.size
    }

    override fun onBindViewHolder(holder: ImageHolder, position: Int) {
        val data = mItems[position]

        val thumbItem = ThumbItem(uri = data.uri, path = data.path)
        val thumbMemoryCacheKey = MemoryCache.Key(thumbItem.toString())
        val thumbMemoryCache = getMemoryCache(thumbMemoryCacheKey)

        val imageItem = ImageItem(uri = data.uri, path = data.path)
        val imageMemoryCacheKey = MemoryCache.Key(imageItem.toString())
        val imageMemoryCache = getMemoryCache(imageMemoryCacheKey)

        var isHighQuality = false
        if (thumbMemoryCache == null && imageMemoryCache == null) {
            (mCtx as AppCompatActivity).lifecycleScope.launch(Dispatchers.IO) {
                var bmp: Bitmap?
                try {
                    bmp = mCtx!!.contentResolver.loadThumbnail(
                        thumbItem.uri!!,
                        Size(MainActivity.THUMB_WIDTH, MainActivity.THUMB_HEIGHT),
                        null
                    )

                    mImageLoader?.memoryCache?.set(thumbMemoryCacheKey, MemoryCache.Value(bmp.asImage()))
                } catch (e: Exception) {
                    Log.e(TAG, "loadThumbnail e=$e $thumbItem")
                    bmp = mErrorBmp
                }

                withContext(Dispatchers.Main) {
                    if (!isHighQuality) {
                        holder.image.setImageBitmap(bmp)
                    }
                }
            }
        }

        var imgPlaceholder = mPlaceholderImage
        if (thumbMemoryCache != null) {
            imgPlaceholder = thumbMemoryCache.image
        }

        val imageReq = ImageRequest.Builder(mCtx!!)
            .data(mItems[position].uri)
            .memoryCacheKey(imageMemoryCacheKey)
            .size(mViewSize)
            .target(holder.image)
            .placeholder(imgPlaceholder)
            .listener(object : ImageRequest.Listener {
                override fun onSuccess(request: ImageRequest, result: SuccessResult) {
                    isHighQuality = true
                    Log.d(TAG, "image onSuccess ${result.dataSource} $imageItem ${calMemoryCache()}")
                }
            }).build()

        mImageLoader?.enqueue(imageReq)
    }

    private fun getMemoryCache(key: MemoryCache.Key): MemoryCache.Value? {
        return mImageLoader?.memoryCache?.get(key)
    }

    private fun calMemoryCache(): String {
        return "${mImageLoader?.memoryCache?.size} / ${mImageLoader?.memoryCache?.maxSize}"
    }
}

class ImageHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    var image = itemView as MyIV
}

class MyIV : AppCompatImageView {
    companion object {
        const val TAG = "fly/MyIV"
    }

    private var mSize = 0
    private var mCtx: Context? = null

    constructor(ctx: Context, size: Int) : super(ctx) {
        mCtx = ctx
        mSize = size
        scaleType = ScaleType.CENTER_CROP
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(mSize, mSize)
    }
}
Kotlin 复制代码
import android.net.Uri

open class Item {
    companion object {
        const val THUMB = 0
        const val IMG = 1
    }

    var uri: Uri? = null
    var path: String? = null
    var lastModified = 0L
    var width = 0
    var height = 0

    var position = -1
    var type = -1  //0,缩略图。 1,正图image。-1,未知。

    override fun toString(): String {
        return "Item(uri=$uri, path=$path, lastModified=$lastModified, width=$width, height=$height, position=$position, type=$type)"
    }
}
Kotlin 复制代码
import android.net.Uri

class ImageItem : Item {
    
    constructor(uri: Uri, path: String, time: Long = 0, width: Int = 0, height: Int = 0, position: Int = 0) {
        this.uri = uri
        this.path = path
        this.lastModified = time

        this.width = width
        this.height = height

        this.position = position

        this.type = IMG
    }
}
Kotlin 复制代码
import android.net.Uri

class ThumbItem : Item {

    constructor(uri: Uri, path: String, time: Long = 0, width: Int = 0, height: Int = 0, position: Int = 0) {
        this.uri = uri
        this.path = path
        this.lastModified = time

        this.width = width
        this.height = height

        this.position = position

        this.type = THUMB
    }
}

遗留问题:

1、在bind里面开启协程加载小缩略图不是很好,应该模块化改造。最好使用Coil的Fetcher加载缩略图。

2、现在分别使用缩略图内存缓存和正图内存缓存,感觉应该可以合并,只使用一套内存缓存。

Android Coil 3定制ImageRequest请求体data及内存复用,Kotlin-CSDN博客文章浏览阅读689次,点赞17次,收藏10次。Coil是专门针对Android平台上的Kotlin语言特性设计,这不像Glide,Glide的核心框架语言是Java。Coil实现看更细颗粒度的内存、磁盘缓存的客制化设置。扩大了内存,但跑起来发现设置后内存还是比较小(约300mb),这是不够的,需要通过其他配置方式扩大内存空间。3、app跑起来后,没有在当前app的硬盘缓存空间发现图片解码后的磁盘文件缓存痕迹。遗留问题,配置的disk cache似乎没有work,指定的磁盘缓存文件路径生成是生成了,但是app跑起来运行后(图正常显示),里面是空的。https://blog.csdn.net/zhangphil/article/details/145737643

相关推荐
雨白2 小时前
深入理解 Java/Kotlin 反射、注解与泛型:从原理到实战
android·架构
喜欢打篮球的普通人6 小时前
LLVM Backend Lowering 从入门到实战:把 IR 变成机器码的完整链路
android·java·数据库
萝卜er9 小时前
BroadcastReceiver 静态注册与动态注册-《Android深水区(七)》
android
萝卜er9 小时前
ContentProvider 与跨进程数据访问-《Android深水区(八)》
android
萝卜er9 小时前
Service、前台服务与后台限制-《Android深水区(六)》
android
萝卜er10 小时前
Android 存储体系与分区存储-《Android深水区(十二)》
android
会编程的吕洞宾10 小时前
DeepAgents In Action学习(Second)
android·java·学习
太子釢10 小时前
用 AI 完整实现 Github 客户端:基于 KMP + SwiftUI + Compose
android·人工智能·ios
汪海游龙11 小时前
本地源码还是 Maven 版本?Gradle composite build 双轨依赖的正确接法
android·ci/cd·kotlin
k3x1n11 小时前
三星安卓系统BUG排查记录:云闪付、铁路12306、个人所得税、中国移动等App,长期闪退的根本原因分析
android·逆向