Android Coil3 Fetcher preload批量Bitmap拼接扁平宽图,Kotlin

Android Coil3 Fetcher preload批量Bitmap拼接扁平宽图,Kotlin

在这一篇文章基础上改进:

Android Coil3阶梯preload批量Bitmap拼接扁平宽图,Kotlin-CSDN博客文章浏览阅读854次,点赞18次,收藏5次。遗留问题,配置的disk cache似乎没有work,指定的磁盘缓存文件路径生成是生成了,但是app跑起来运行后(图正常显示),里面是空的。Android拼接合并图片生成长图代码实现合并两张图片,以第一张图片的宽度为标准,如果被合并的第二张图片宽度和第一张不同,那么就以第一张图片的宽度为准线,对第二张图片进行缩放。Android拼接合并图片生成长图代码实现合并两张图片,以第一张图片的宽度为标准,如果被合并的第二张图片宽度和第一张不同,那么就以第一张图片的宽度为准线,对第二张图片进行缩放。https://blog.csdn.net/zhangphil/article/details/146353189

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.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil3.ImageLoader
import coil3.memory.MemoryCache
import coil3.request.ImageRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    companion object {
        const val THUMB_WIDTH = 150
        const val THUMB_HEIGHT = 150
        const val ROW_SIZE = 16

        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 = LinearLayoutManager(this)
        layoutManager.orientation = LinearLayoutManager.VERTICAL

        val imageLoader = MyCoilManager.INSTANCE.getImageLoader(applicationContext)
        val adapter = MyAdapter(this, imageLoader)

        rv.adapter = adapter
        rv.layoutManager = layoutManager

        rv.setItemViewCacheSize(ROW_SIZE)
        rv.recycledViewPool.setMaxRecycledViews(0, ROW_SIZE)

        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)
            val total = lists.size
            Log.d(TAG, "总数量=$total")
            lists.shuffle()

            val items = sliceDataList(lists)
            lifecycleScope.launch(Dispatchers.Main) {
                adapter.dataChanged(items)
            }

            val PRELOAD = true
            if (PRELOAD) {
                val probability = 0.85f
                val from = 500
                lists.forEachIndexed { idx, myData ->
                    if (idx > from && (Math.random() <= probability)) {
                        Log.d(TAG, "$idx/$total preload")
                        preload(imageLoader, myData)
                    }
                }
            }
        }
    }

    private fun preload(imageLoader: ImageLoader, myData: MyData) {
        val thumbItem = Item(uri = myData.uri, path = myData.path)
        thumbItem.type = Item.THUMB

        val thumbMemoryCacheKey = MemoryCache.Key(thumbItem.toString())
        val thumbMemoryCache = MyCoilManager.INSTANCE.getMemoryCache(thumbMemoryCacheKey)

        if (thumbMemoryCache == null) {
            val thumbReq = ImageRequest.Builder(this)
                .data(thumbItem)
                .size(THUMB_WIDTH, THUMB_HEIGHT)
                .memoryCacheKey(thumbMemoryCacheKey)
                .build()
            imageLoader.enqueue(thumbReq)
        }
    }

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

    private fun sliceDataList(data: ArrayList<MyData>): ArrayList<ArrayList<MyData>> {
        var k: Int
        val lists = ArrayList<ArrayList<MyData>>()
        for (i in data.indices step ROW_SIZE) {
            val temp = ArrayList<MyData>()

            k = 0
            for (j in 0 until ROW_SIZE) {
                k = i + j
                if (k >= data.size) {
                    break
                }
                temp.add(data[k])
            }

            lists.add(temp)
        }

        return lists
    }


    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.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.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil3.ImageLoader
import com.appdemo.MainActivity.MyData

class MyAdapter : RecyclerView.Adapter<MyAdapter.ImageHolder> {
    private var mCtx: Context? = null
    private var mImageLoader: ImageLoader? = null
    private var mItems = ArrayList<ArrayList<MyData>>()
    private var mScreenWidth = 0

    companion object {
        const val TAG = "fly/ImageAdapter"
    }

    constructor(ctx: Context, il: ImageLoader?) : super() {
        mCtx = ctx
        mScreenWidth = mCtx?.resources?.displayMetrics?.widthPixels!!
        mImageLoader = il
    }

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

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

    override fun onBindViewHolder(holder: ImageHolder, position: Int) {
        holder.image.setData(mItems[position])
    }

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

    class ImageHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var image = itemView as MyImgView
    }
}
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.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 java.io.File

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

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

    private var mImageLoader: ImageLoader? = null
    private var memoryCacheMaxSize = 0L

    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())
            .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()

        memoryCacheMaxSize = memoryCache.maxSize

        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
    }

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

    fun calMemoryCache(): String {
        val sz = mImageLoader?.memoryCache?.size
        return "${sz?.toFloat()!! / memoryCacheMaxSize.toFloat()} , $sz / $memoryCacheMaxSize"
    }
}
Kotlin 复制代码
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Picture
import android.graphics.RectF
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.graphics.toRect
import androidx.lifecycle.lifecycleScope
import coil3.ImageLoader
import coil3.memory.MemoryCache
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.toBitmap
import com.appdemo.MainActivity.MyData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch


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

        //整数相除,精度损失的平衡因子
        const val BALANCE_FACTOR = 1
    }

    private var mCtx: Context? = null
    private var mImageLoader: ImageLoader? = null
    private var mScreenWidth: Int = 0
    private var mHeight: Int = 0
    private var mRealSize: Int = 0
    private var mBmp = mutableListOf<DataBean>()

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

        mScreenWidth = screenWidth
        mHeight = mScreenWidth / MainActivity.ROW_SIZE + BALANCE_FACTOR

        scaleType = ScaleType.CENTER_CROP
    }

    fun setData(data: ArrayList<MyData>) {
        mRealSize = data.size

        (mCtx as AppCompatActivity).lifecycleScope.launch(Dispatchers.IO) {
            var loadCount = 0

            // this for-loop will cost some time
            // if no memory cache, for-loop more , time cost more
            data.forEachIndexed { _, myData ->
                val thumbItem = Item(uri = myData.uri, path = myData.path)
                thumbItem.type = Item.THUMB

                val thumbMemoryCacheKey = MemoryCache.Key(thumbItem.toString())
                val thumbMemoryCache = MyCoilManager.INSTANCE.getMemoryCache(thumbMemoryCacheKey)

                // be careful, this block will cost time
                // time cost point
                if (thumbMemoryCache == null) {
                    val t = System.currentTimeMillis()

                    val thumbReq = ImageRequest.Builder(mCtx!!)
                        .data(thumbItem)
                        .size(MainActivity.THUMB_WIDTH, MainActivity.THUMB_HEIGHT)
                        .memoryCacheKey(thumbMemoryCacheKey)
                        .listener(object : ImageRequest.Listener {
                            override fun onSuccess(request: ImageRequest, result: SuccessResult) {
                                loadCount++

                                refresh(loadCount, result.image.toBitmap())
                            }

                            override fun onCancel(request: ImageRequest) {
                                Log.w(TAG, "onCancel")

                                loadCount++
                                refresh(loadCount, null)
                            }

                            override fun onError(request: ImageRequest, result: ErrorResult) {
                                Log.e(TAG, "onError")

                                loadCount++
                                refresh(loadCount, null)
                            }
                        }).build()

                    mImageLoader?.enqueue(thumbReq)
                    Log.d(TAG, "加载... 耗时=${System.currentTimeMillis() - t}")
                } else {
                    Log.d(TAG, "命中缓存 ${MyCoilManager.INSTANCE.calMemoryCache()}")

                    loadCount++
                    refresh(loadCount, thumbMemoryCache.image.toBitmap())
                }
            }
        }
    }

    private fun refresh(loadCount: Int, bmp: Bitmap?) {
        val bean = DataBean(bmp)
        mBmp.add(bean)

        if (loadCount == mRealSize) {
            val jBmp = joinBitmap()
            (mCtx as AppCompatActivity).lifecycleScope.launch(Dispatchers.Main) {
                this@MyImgView.setImageBitmap(jBmp)
            }
            mBmp.clear()
        }
    }

    data class DataBean(var bitmap: Bitmap?)

    private fun joinBitmap(): Bitmap {
        val bmp = Bitmap.createBitmap(mHeight * mRealSize, mHeight, Bitmap.Config.RGB_565)
        val canvas = Canvas(bmp)
        canvas.drawColor(Color.LTGRAY)

        val bitmaps = mBmp.toMutableList()
        bitmaps.forEachIndexed { idx, dataBean ->
            if (dataBean.bitmap != null) {
                if (Bitmap.Config.HARDWARE == dataBean.bitmap!!.config) {
                    Log.d(TAG, "Bitmap.Config.HARDWARE")
                    dataBean.bitmap = convert(dataBean.bitmap)
                }

                val w = dataBean.bitmap!!.width
                val h = dataBean.bitmap!!.height

                val mini = Math.min(w, h)
                val left = (w - mini) / 2f
                val top = (h - mini) / 2f
                val right = (w + mini) / 2f
                val bottom = (h + mini) / 2f

                val srcRct = RectF(left, top, right, bottom)

                val dstRctLeft = idx * mHeight.toFloat()
                val dstRct = RectF(dstRctLeft, 0f, dstRctLeft + mHeight, mHeight.toFloat())

                canvas.drawBitmap(dataBean.bitmap!!, srcRct.toRect(), dstRct.toRect(), null)
            }
        }

        bitmaps.clear()

        return bmp
    }

    private fun convert(src: Bitmap?): Bitmap {
        val w = src?.width
        val h = src?.height
        val picture = Picture()
        val canvas = picture.beginRecording(w!!, h!!)
        canvas.drawBitmap(src, 0f, 0f, null)
        picture.endRecording()
        return Bitmap.createBitmap(picture, w, h, Bitmap.Config.RGB_565)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (mHeight == 0) {
            setMeasuredDimension(mScreenWidth, MainActivity.THUMB_HEIGHT)
        } else {
            setMeasuredDimension(mHeight * mRealSize, mHeight)
        }
    }
}
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 ${MyCoilManager.INSTANCE.calMemoryCache()}")
        } 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)
        }
    }
}
相关推荐
拥小抱14 小时前
Compose 知识点
android
文章永久免费只为良心14 小时前
Web应用安全与防护_第三章_日志文件包含_从一道 CTFshow 题目讲透「日志投毒型 LFI → RCE」:原理、时序陷阱与全变种攻防手册
android·网络安全·渗透测试·漏洞危害·漏洞描述·日志投毒·红队指南
AIsoft_868915 小时前
学术讲座录音如何整理?录音APP可行吗
android·iphone
Goway_Hui17 小时前
【KMP for OpenHarmony】-- 在鸿蒙工程中使用 Apollo-Kotlin
华为·kotlin·harmonyos
Patrick在香港17 小时前
一个斜杠,把毫秒变成十几秒
android·数据库·python·性能优化·正则表达式·回溯·redos
海雅达手持终端PDA18 小时前
工业手持终端的“去手机化”演进:看海雅达如何以“同轴扫摄”终结工程妥协,重塑工业PDA新范式
android·物联网·智能手机·硬件工程·能源·制造·iot
释厄62318 小时前
AI 军备竞赛悖论——为 AI 卖游戏但 AI 需要游戏母场景
人工智能·游戏·kotlin
booboosui19 小时前
删掉一个住在本机的 agent:8 处要清,顺序不能反
android
hcgeng20 小时前
Android获取崩溃日志
android