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

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

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" />
XML 复制代码
    implementation("io.coil-kt.coil3:coil:3.1.0")
    implementation("io.coil-kt.coil3:coil-gif:3.1.0")
    implementation("io.coil-kt.coil3:coil-core:3.1.0")
Kotlin 复制代码
import android.content.ContentUris
import android.content.Context
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 coil3.imageLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch


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

        const val THUMB_WIDTH = 20
        const val THUMB_HEIGHT = 20

        const val IMAGE_SIZE = 400

        const val TAG = "fly/MainActivity"
    }

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

        val layoutManager = GridLayoutManager(this, SPAN_COUNT)
        layoutManager.orientation = LinearLayoutManager.VERTICAL

        val adapter = ImageAdapter(this, application.imageLoader)

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

    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.app.Application
import android.util.Log
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader


class MyApp : Application(), SingletonImageLoader.Factory {
    companion object {
        const val TAG = "fly/MyApp"
    }

    override fun newImageLoader(context: PlatformContext): ImageLoader {
        Log.d(TAG, "newImageLoader")
        return MyCoilManager.INSTANCE().getImageLoader(this)
    }
}
Kotlin 复制代码
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment
import android.util.Log
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.gif.AnimatedImageDecoder
import coil3.memory.MemoryCache
import coil3.request.CachePolicy
import coil3.request.bitmapConfig
import java.io.File

class MyCoilManager {
    companion object {
        const val TAG = "fly/MyCoilManager"

        private val single by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { MyCoilManager() }
        fun INSTANCE() = single
    }

    private var mImageLoader: ImageLoader? = null

    fun getImageLoader(ctx: Context): ImageLoader {
        if (mImageLoader != null) {
            Log.w(TAG, "ImageLoader已经初始化")
            return mImageLoader!!
        }

        Log.d(TAG, "初始化ImageLoader")
        //初始化加载器。
        mImageLoader = ImageLoader.Builder(ctx)
            .memoryCachePolicy(CachePolicy.ENABLED)
            .memoryCache(initMemoryCache())
            .diskCachePolicy(CachePolicy.ENABLED)
            .diskCache(initDiskCache())
            .bitmapConfig(Bitmap.Config.ARGB_8888)
            .components {
                add(AnimatedImageDecoder.Factory())
                add(ThumbFetcher.Factory(ctx))
            }.build()

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

        return mImageLoader!!
    }


    private fun initMemoryCache(): MemoryCache {
        //内存缓存。
        val memoryCache = MemoryCache.Builder()
            .maxSizeBytes(1024 * 1024 * 1024 * 2L) //2GB
            .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)
            .build()

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

        return diskCache
    }
}
Kotlin 复制代码
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatImageView
import androidx.recyclerview.widget.RecyclerView
import coil3.Image
import coil3.ImageLoader
import coil3.asImage
import coil3.memory.MemoryCache
import coil3.request.Disposable
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.request.target
import coil3.toBitmap


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

    companion object {
        const 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]
        loadItemWithThumbForImage(data, holder.image)
    }

    private fun loadItemWithThumbForImage(data: MainActivity.MyData, myIv: MyIV) {
        val thumbItem = Item(uri = data.uri, path = data.path)
        thumbItem.type = Item.THUMB
        val thumbMemoryCacheKey = MemoryCache.Key(thumbItem.toString())
        val thumbMemoryCache = getMemoryCache(thumbMemoryCacheKey)

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

        var isHighQualityLoaded = false
        var thumbDisposable: Disposable? = null

        //首次加载,没有正图缓存,也没有缩略图缓存。
        if (thumbMemoryCache == null && imageMemoryCache == null) {
            val thumbReq = ImageRequest.Builder(mCtx!!)
                .data(thumbItem)
                .memoryCacheKey(thumbMemoryCacheKey)
                .listener(object : ImageRequest.Listener {
                    override fun onSuccess(request: ImageRequest, result: SuccessResult) {
                        if (!isHighQualityLoaded) {
                            myIv.setImageBitmap(result.image.toBitmap())
                        }
                    }
                }).build()

            thumbDisposable = mImageLoader?.enqueue(thumbReq)
        }

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

        //无论如何,都要加载正图。
        val imageReq = ImageRequest.Builder(mCtx!!)
            .data(data.uri)
            .memoryCacheKey(imageMemoryCacheKey)
            .size(MainActivity.IMAGE_SIZE)
            .target(myIv)
            .placeholder(imgPlaceholder)
            .error(mErrorBmp!!.asImage())
            .listener(object : ImageRequest.Listener {
                override fun onSuccess(request: ImageRequest, result: SuccessResult) {
                    isHighQualityLoaded = true
                    thumbDisposable?.dispose()

                    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

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,未知。

    constructor(uri: Uri, path: String) {
        this.uri = uri
        this.path = path
    }

    override fun toString(): String {
        return "Item(uri=$uri, path=$path, lastModified=$lastModified, width=$width, height=$height, position=$position, type=$type)"
    }
}
Kotlin 复制代码
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import android.util.Size
import coil3.ImageLoader
import coil3.asImage
import coil3.decode.DataSource
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.ImageFetchResult
import coil3.request.Options

/**
 * 例如 FileUriFetcher
 */
class ThumbFetcher(private val ctx: Context, private val thumbItem: Item, private val options: Options) : Fetcher {
    companion object {
        const val TAG = "fly/ThumbFetcher"
    }

    override suspend fun fetch(): FetchResult {
        var bmp: Bitmap? = null
        val t = System.currentTimeMillis()
        try {
            bmp = ctx.contentResolver.loadThumbnail(thumbItem.uri!!, Size(MainActivity.THUMB_WIDTH, MainActivity.THUMB_HEIGHT), null)
            Log.d(TAG, "loadThumbnail time cost=${System.currentTimeMillis() - t} $thumbItem")
        } catch (e: Exception) {
            Log.e(TAG, "e=$e ThumbItem=$thumbItem")
        }

        return ImageFetchResult(
            bmp?.asImage()!!,
            true,
            dataSource = DataSource.DISK
        )
    }

    class Factory(private val ctx: Context) : Fetcher.Factory<Item> {
        override fun create(
            data: Item,
            options: Options,
            imageLoader: ImageLoader,
        ): Fetcher {
            return ThumbFetcher(ctx, data, options)
        }
    }
}

Android Coil3缩略图、默认占位图placeholder、error加载错误显示,Kotlin(4)_android coil 图片加载框架,设置占位图后,图片的比例不正确-CSDN博客文章浏览阅读786次,点赞19次,收藏10次。遗留问题,配置的disk cache似乎没有work,指定的磁盘缓存文件路径生成是生成了,但是app跑起来运行后(图正常显示),里面是空的。遗留问题,配置的disk cache似乎没有work,指定的磁盘缓存文件路径生成是生成了,但是app跑起来运行后(图正常显示),里面是空的。遗留问题,配置的disk cache似乎没有work,指定的磁盘缓存文件路径生成是生成了,但是app跑起来运行后(图正常显示),里面是空的。2、现在分别使用缩略图内存缓存和正图内存缓存,感觉应该可以合并,只使用一套内存缓存。_android coil 图片加载框架,设置占位图后,图片的比例不正确https://blog.csdn.net/zhangphil/article/details/145832225

相关推荐
墨天梦8 分钟前
B19_SunnyWeather到阅读客户端
android·kotlin
美狐美颜sdk1 小时前
直播APP源码可以直接接入视频美颜sdk吗?技术方案详解
android·人工智能·音视频·美颜sdk·直播美颜sdk
用户69371750013844 小时前
2026,程序员的时代拐点到了
android·前端·后端
浪潮IT馆6 小时前
Android Studio 非最新版本下载安装教程
android·ide·android studio
xiaozongt19897 小时前
MasterGo + Claude Code 生成 Android 布局:完整实践指南
android
Anhty8 小时前
2026最新免费手机音频处理工具 !!
android·功能测试·ios·智能手机·音视频
2501_9159090610 小时前
iOS应用从开发到上架App Store的完整发布流程与步骤指南
android·ios·小程序·https·uni-app·webview
狗凯之家源码网10 小时前
AI 步数修改提交系统源码应用场景与落地指南
android·数据库·人工智能·运动步数
ZHOUPUYU11 小时前
PHP 9.0 前瞻:下一代 PHP 会带来哪些新功能?
android·开发语言·php
JMchen11 小时前
性能优化——让Native代码飞起来
android·c++·性能优化