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

相关推荐
好的佩奇8 小时前
Dart 之扩展类型
android·flutter·dart
alexhilton10 小时前
2027 Android 开发者路线图:学习、面试与 AI
android·kotlin·android jetpack
灿飞屋12 小时前
Kotlin与Java虚拟机三:控制流
android·java·开发语言·后端·python·kotlin·安卓
疯华整猫13 小时前
我用 Trae 把 Android 内存账重算了一遍
android·性能优化·源码
唐青枫14 小时前
Kotlin 对象声明与对象表达式详解
kotlin
TimeFine14 小时前
AI 编程工作流的设计:从人工判断到 AI裁判
android·ai编程
阿杰m15 小时前
Android源码第四章:Android 系统启动——App 的第一行代码从哪来?
android
灿飞屋16 小时前
Kotlin与Java虚拟机四:函数
java·python·kotlin·安卓·android jetpack
疯华整猫19 小时前
混淆起初根本不是为了防破解:我用 Trae 把 Android 混淆这二十来年重跑了一遍
android·人工智能·计算机视觉
Carson带你学Android19 小时前
AI时代,你的 App 会被 Agent 这三种方式调用
android·ai编程