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")
?
?
<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" />
?
?
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.Fileclass MainActivity : AppCompatActivity() {companion object {const val SPAN_COUNT = 8const val THUMB_WIDTH = 20const val THUMB_HEIGHT = 20}private var mImageLoader: ImageLoader? = nullprivate 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.VERTICALval adapter = ImageAdapter(this, mImageLoader)rv.adapter = adapterrv.layoutManager = layoutManagerrv.setItemViewCacheSize(SPAN_COUNT * 2)rv.recycledViewPool.setMaxRecycledViews(0, SPAN_COUNT * 2)val ctx = thislifecycleScope.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>()//讀取視頻Videoval 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}
}
?
?
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.withContextclass ImageAdapter : RecyclerView.Adapter<ImageHolder> {private var mCtx: Context? = nullprivate var mImageLoader: ImageLoader? = nullprivate var mViewSize = 0private var mPlaceholderImage: Image? = nullprivate var mErrorBmp: Bitmap? = nullprivate val TAG = "fly/ImageAdapter"constructor(ctx: Context, il: ImageLoader?) : super() {mCtx = ctxmImageLoader = ilmViewSize = mCtx!!.resources.displayMetrics.widthPixels / MainActivity.SPAN_COUNTmPlaceholderImage = 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 = itemsnotifyDataSetChanged()}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 = falseif (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 = mPlaceholderImageif (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 = trueLog.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 = 0private var mCtx: Context? = nullconstructor(ctx: Context, size: Int) : super(ctx) {mCtx = ctxmSize = sizescaleType = ScaleType.CENTER_CROP}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)setMeasuredDimension(mSize, mSize)}
}
?
?
?
import android.net.Uriopen class Item {companion object {const val THUMB = 0const val IMG = 1}var uri: Uri? = nullvar path: String? = nullvar lastModified = 0Lvar width = 0var height = 0var position = -1var 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)"}
}
?
?
import android.net.Uriclass ImageItem : Item {constructor(uri: Uri, path: String, time: Long = 0, width: Int = 0, height: Int = 0, position: Int = 0) {this.uri = urithis.path = paththis.lastModified = timethis.width = widththis.height = heightthis.position = positionthis.type = IMG}
}
?
?
import android.net.Uriclass ThumbItem : Item {constructor(uri: Uri, path: String, time: Long = 0, width: Int = 0, height: Int = 0, position: Int = 0) {this.uri = urithis.path = paththis.lastModified = timethis.width = widththis.height = heightthis.position = positionthis.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
?