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.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
}
}
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)
}
}
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)"
}
}
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
}
}
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、现在分别使用缩略图内存缓存和正图内存缓存,感觉应该可以合并,只使用一套内存缓存。