Android Coil 3 Fetcher大批量Bitmap拼接成1张扁平宽图,Kotlin

Android Coil 3 Fetcher大批量Bitmap拼接成1张扁平宽图,Kotlin

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")
    implementation("io.coil-kt.coil3:coil-video:3.1.0")
    implementation("io.coil-kt.coil3:coil-svg: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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch


class MainActivity : AppCompatActivity() {
    companion object {
        const val IMAGE = 1
        const val VIDEO = 2

        const val THUMB_SIZE = 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
        rv.layoutManager = layoutManager

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

        rv.adapter = adapter
        rv.layoutManager = layoutManager

        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(videoList)
            lists.addAll(imgList)

            val total = lists.size
            Log.d(TAG, "总数量=$total")
            lists.shuffle()

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

    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(imageUri, path, IMAGE))
        }
        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(videoUri, path, VIDEO))
        }
        cursor.close()

        return videos
    }
}
Kotlin 复制代码
import android.content.Context
import android.graphics.BitmapFactory
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil3.Bitmap
import coil3.ImageLoader

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

    private var mThumbError: Bitmap? = null

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

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

        mThumbError = BitmapFactory.decodeResource(mCtx!!.resources, android.R.drawable.stat_sys_warning)
    }

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

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageHolder {
        val view = MyGridImgView(mCtx!!, mImageLoader, mScreenWidth, mThumbError)
        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 MyGridImgView
    }
}
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.EventListener
import coil3.ImageLoader
import coil3.bitmapFactoryMaxParallelism
import coil3.decode.Decoder
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.gif.AnimatedImageDecoder
import coil3.imageDecoderEnabled
import coil3.memory.MemoryCache
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.Options
import coil3.svg.SvgDecoder
import coil3.video.VideoFrameDecoder
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

    // BitmapFactoryDecoder
    // internal const val DEFAULT_MAX_PARALLELISM = 4
    private val MAX_PARALLELISM = 6

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

        Log.d(TAG, "初始化ImageLoader")
        //初始化加载器。
        mImageLoader = ImageLoader.Builder(ctx)
            .imageDecoderEnabled(false) //false 对于一些特殊图,可以正常解码。
            .memoryCachePolicy(CachePolicy.ENABLED)
            .memoryCache(initMemoryCache())
            .diskCachePolicy(CachePolicy.ENABLED)
            .diskCache(initDiskCache())
            .bitmapFactoryMaxParallelism(MAX_PARALLELISM)
            .eventListener(object : EventListener() {
                override fun decodeStart(request: ImageRequest, decoder: Decoder, options: Options) {
                    //Log.d(TAG, "decodeStart ${request.data}")
                }
            })
            .components {
                add(MyThumbFetcher.Factory(ctx))

                add(AnimatedImageDecoder.Factory())
                add(VideoFrameDecoder.Factory())
                add(SvgDecoder.Factory())
            }.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 memoryCache(): MemoryCache? {
        return mImageLoader?.memoryCache
    }

    fun calMemoryCache(): String {
        val sz = mImageLoader?.memoryCache?.size
        return "${sz?.toFloat()!! / memoryCacheMaxSize.toFloat()},$sz/$memoryCacheMaxSize"
    }
}
Kotlin 复制代码
import android.net.Uri

open class MyData {
    var uri: Uri? = null
    var path: String? = null
    var lastModified = 0L
    var width = 0
    var height = 0

    var position = -1
    var type = -1  //-1未知。1,普通图。2,视频。

    constructor(uri: Uri?, path: String?, type: Int = -1) {
        this.uri = uri
        this.path = path
        this.type = type
    }

    override fun toString(): String {
        return "MyData(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.graphics.Canvas
import android.graphics.Color
import android.graphics.Picture
import android.graphics.RectF
import android.graphics.drawable.ColorDrawable
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.CachePolicy
import coil3.request.Disposable
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.toBitmap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

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

    private var mCtx: Context? = null
    private var mImageLoader: ImageLoader? = null
    private var mRowHeight: Int = 0 //正方形小格子,高度也就是宽度
    private var mRealSize: Int = 0
    private var mBmp = mutableListOf<DataBean>()
    private var mThumbDisposables = mutableListOf<Disposable?>()

    private var mThumbError: Bitmap? = null

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

        val h = screenWidth.toFloat() / MainActivity.ROW_SIZE
        Log.d(TAG, "小格子实际宽度=$h")
        mRowHeight = h.toInt()

        scaleType = ScaleType.FIT_START
    }

    fun setData(data: ArrayList<MyData>) {
        clear()
        setImageDrawable(ColorDrawable(Color.DKGRAY))

        mRealSize = data.size

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

            val t = System.currentTimeMillis()

            // this for-loop will cost some time
            // if no memory cache, for-loop more , time cost more
            data.forEachIndexed { _, myData ->
                val thumbItem = Item(myData)
                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) {
                    Log.d(TAG, "命中缓存 ${MyCoilManager.INSTANCE.calMemoryCache()}")

                    loadCount++
                    refresh(loadCount, thumbMemoryCache.image.toBitmap())
                } else {
                    val req = ImageRequest.Builder(mCtx!!)
                        .data(thumbItem)
                        .size(MainActivity.THUMB_SIZE, MainActivity.THUMB_SIZE)
                        .memoryCacheKey(thumbMemoryCacheKey)
                        .memoryCachePolicy(CachePolicy.WRITE_ONLY)
                        .listener(object : ImageRequest.Listener {
                            override fun onSuccess(request: ImageRequest, result: SuccessResult) {
                                Log.d(TAG, "onSuccess ${MyCoilManager.INSTANCE.calMemoryCache()}")

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

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

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

                    val d = mImageLoader?.enqueue(req)
                    mThumbDisposables.add(d)
                }
            }

            Log.d(TAG, "forEachIndexed 耗时=${System.currentTimeMillis() - t}")
        }
    }

    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) {
                setImageBitmap(jBmp)
            }
            mBmp.clear()
        }
    }

    data class DataBean(var bitmap: Bitmap?)

    private fun joinBitmap(): Bitmap {
        val bmp = Bitmap.createBitmap(mRowHeight * mRealSize, mRowHeight, 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 * mRowHeight.toFloat()
                val dstRct = RectF(dstRctLeft, 0f, dstRctLeft + mRowHeight, mRowHeight.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)
        setMeasuredDimension(mRowHeight * mRealSize, mRowHeight)
    }

    private fun clear() {
        mThumbDisposables.forEach {
            it?.dispose()
        }

        mThumbDisposables.clear()
    }


    class Item(val data: MyData) {
        companion object {
            //内存中的标记
            const val MEM_THUMB = 0
            const val MEM_IMAGE = 1
        }

        var memory = -1

        override fun toString(): String {
            return "Item(data=$data, memory=$memory)"
        }
    }
}
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 MyThumbFetcher(private val ctx: Context, private val item: MyGridImgView.Item, private val options: Options) : Fetcher {
    companion object {
        const val TAG = "fly/MyThumbFetcher"
    }

    override suspend fun fetch(): FetchResult {
        var bmp: Bitmap? = null
        val t = System.currentTimeMillis()
        try {
            bmp = ctx.contentResolver.loadThumbnail(item.data.uri!!, Size(MainActivity.THUMB_SIZE, MainActivity.THUMB_SIZE), null)
            Log.d(TAG, "loadThumbnail time cost=${System.currentTimeMillis() - t} $item ${MyCoilManager.INSTANCE.calMemoryCache()}")
        } catch (e: Exception) {
            Log.e(TAG, "e=$e Item=$item")
        }

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

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

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

Android Coli 3 ImageView load two suit Bitmap thumb and formal,Kotlin(四)-CSDN博客文章浏览阅读294次,点赞4次,收藏4次。遗留问题,配置的disk cache似乎没有work,指定的磁盘缓存文件路径生成是生成了,但是app跑起来运行后(图正常显示),里面是空的。遗留问题,配置的disk cache似乎没有work,指定的磁盘缓存文件路径生成是生成了,但是app跑起来运行后(图正常显示),里面是空的。遗留问题,配置的disk cache似乎没有work,指定的磁盘缓存文件路径生成是生成了,但是app跑起来运行后(图正常显示),里面是空的。2、现在分别使用缩略图内存缓存和正图内存缓存,感觉应该可以合并,只使用一套内存缓存。https://blog.csdn.net/zhangphil/article/details/147038485

相关推荐
薿夜3 小时前
SpringSecurity(三)
android
zh_xuan8 小时前
Android Hilt实现依赖注入
android·hilt
freshman_y9 小时前
Qtcreator怎么新建安卓项目?编写一个五子棋游戏APP?
android·qt
时寒的笔记10 小时前
js逆向7_案例惠nong网
android·开发语言·javascript
雨白10 小时前
深入理解 Kotlin 协程 (三):返璞归真,探寻协程基础设施的底层基石
kotlin
肯多洛夫斯基11 小时前
安卓工控屏静默连WiFi全攻略
android
极梦网络无忧11 小时前
Android无障碍服务实现抖音直播间界面监控(场控助手核心原理)
android
call me by ur name12 小时前
ERNIE 5.0 Technical Report论文解读
android·开发语言·人工智能·机器学习·ai·kotlin
kerli12 小时前
Compose 组件:Box 核心参数及其 Bias 算法
android·前端
BLUcoding12 小时前
Android 常用控件及核心属性
android