Android Coli 3 ImageView load two suit Bitmap thumb and formal,Kotlin(二)
在 Android Coli 3 ImageView load two suit Bitmap thumb and formal,Kotlin-CSDN博客
基础上继续完善。
<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" />
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")
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.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 = 15
const val THUMB_HEIGHT = 15
const val IMAGE_SIZE = 150
const val IMAGE = 1
const val VIDEO = 2
const val ROW_SIZE = 6
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, ROW_SIZE)
layoutManager.orientation = GridLayoutManager.VERTICAL
rv.layoutManager = layoutManager
val imageLoader = MyCoilManager.INSTANCE.getImageLoader(applicationContext)
val adapter = MyAdapter(this, imageLoader)
rv.adapter = adapter
rv.layoutManager = layoutManager
rv.setItemViewCacheSize(ROW_SIZE * 10)
rv.recycledViewPool.setMaxRecycledViews(0, ROW_SIZE * 10)
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(lists)
}
val PRELOAD = false
if (PRELOAD) {
val probability = 0.85f
val from = 30
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 data = MyData(uri = myData.uri, path = myData.path)
val thumbMemoryCacheKey = MemoryCache.Key(data.toString())
val thumbMemoryCache = MyCoilManager.INSTANCE.getMemoryCache(thumbMemoryCacheKey)
if (thumbMemoryCache == null) {
val thumbReq = ImageRequest.Builder(this)
.data(data)
.size(THUMB_WIDTH, THUMB_HEIGHT)
.memoryCacheKey(thumbMemoryCacheKey)
.build()
imageLoader.enqueue(thumbReq)
}
}
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
}
}
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<MyData>()
private var mScreenWidth = 0
private var mPlaceHolderBmp: Bitmap? = null
private var mThumbError: Bitmap? = null
private var mImageError: Bitmap? = null
companion object {
const val TAG = "fly/ImageAdapter"
}
constructor(ctx: Context, il: ImageLoader?) : super() {
mCtx = ctx
mScreenWidth = mCtx?.resources?.displayMetrics?.widthPixels!!
mImageLoader = il
mPlaceHolderBmp = BitmapFactory.decodeResource(mCtx!!.resources, R.mipmap.loading)
mThumbError = BitmapFactory.decodeResource(mCtx!!.resources, android.R.drawable.ic_menu_gallery)
mImageError = BitmapFactory.decodeResource(mCtx!!.resources, android.R.drawable.stat_sys_warning)
}
fun dataChanged(items: ArrayList<MyData>) {
this.mItems = items
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageHolder {
val view = MyImgView(mCtx!!, mImageLoader, mScreenWidth, mPlaceHolderBmp, mThumbError, mImageError)
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
}
}
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)
}
}
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 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
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))
add(VideoFrameDecoder.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"
}
}
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)"
}
}
import android.content.Context
import android.util.Log
import androidx.appcompat.widget.AppCompatImageView
import coil3.Bitmap
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
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 mThumbDisposable: Disposable? = null
private var mImageDisposable: Disposable? = null
private var mPlaceHolderBmp: Bitmap? = null
private var mThumbError: Bitmap? = null
private var mImageError: Bitmap? = null
constructor(
ctx: Context,
il: ImageLoader?,
screenWidth: Int,
placeHolderBmp: Bitmap?,
thumbError: Bitmap?,
imageError: Bitmap?
) : super(ctx) {
mCtx = ctx
mImageLoader = il
mScreenWidth = screenWidth
mHeight = mScreenWidth / MainActivity.ROW_SIZE + BALANCE_FACTOR
scaleType = ScaleType.CENTER_CROP
mPlaceHolderBmp = placeHolderBmp
mThumbError = thumbError
mImageError = imageError
}
fun setData(mData: MyData) {
clear()
val thumbItem = Item(mData)
thumbItem.memory = Item.MEM_THUMB
val thumbMemoryCacheKey = MemoryCache.Key(thumbItem.toString())
val thumbMemoryCache = MyCoilManager.INSTANCE.getMemoryCache(thumbMemoryCacheKey)
val imageItem = Item(mData)
imageItem.memory = Item.MEM_IMAGE
val imageMemoryCacheKey = MemoryCache.Key(imageItem.toString())
val imageMemoryCache = MyCoilManager.INSTANCE.getMemoryCache(imageMemoryCacheKey)
var highQuality = false
//第1次加载
if (thumbMemoryCache == null && imageMemoryCache == null) {
setImageBitmap(mPlaceHolderBmp)
val thumbReq = ImageRequest.Builder(mCtx!!)
.data(thumbItem)
.memoryCacheKey(thumbMemoryCacheKey)
.memoryCachePolicy(CachePolicy.WRITE_ONLY)
.size(MainActivity.THUMB_WIDTH, MainActivity.THUMB_HEIGHT)
.listener(object : ImageRequest.Listener {
override fun onSuccess(request: ImageRequest, result: SuccessResult) {
Log.d(TAG, "缩略图 onSuccess $thumbItem 缓存状态=${MyCoilManager.INSTANCE.calMemoryCache()}")
if (!highQuality) {
setImageBitmap(result.image.toBitmap())
}
}
override fun onError(request: ImageRequest, result: ErrorResult) {
Log.e(TAG, "缩略图 onError $thumbItem")
if (!highQuality) {
setImageBitmap(mThumbError)
}
}
}).build()
mThumbDisposable = mImageLoader?.enqueue(thumbReq)
} else if (thumbMemoryCache != null) {
Log.d(TAG, "命中缩略图缓存 $thumbItem 缓存状态=${MyCoilManager.INSTANCE.calMemoryCache()}")
setImageBitmap(thumbMemoryCache.image.toBitmap())
}
if (imageMemoryCache == null) {
val imageReq = ImageRequest.Builder(mCtx!!)
.data(
if (imageItem.data.type == MainActivity.VIDEO) {
imageItem.data.path
} else {
imageItem.data.uri
}
)
.memoryCacheKey(imageMemoryCacheKey)
.memoryCachePolicy(CachePolicy.WRITE_ONLY)
.size(MainActivity.IMAGE_SIZE).listener(object : ImageRequest.Listener {
override fun onSuccess(request: ImageRequest, result: SuccessResult) {
highQuality = true
mThumbDisposable?.dispose()
Log.d(
TAG, "正图 onSuccess $imageItem 缓存状态=${MyCoilManager.INSTANCE.calMemoryCache()}"
)
setImageBitmap(result.image.toBitmap())
}
override fun onError(request: ImageRequest, result: ErrorResult) {
Log.e(TAG, "正图 onError $imageItem")
setImageBitmap(mImageError)
}
}).build()
mImageDisposable = mImageLoader?.enqueue(imageReq)
} else {
Log.d(TAG, "命中正图缓存 $imageItem 缓存状态=${MyCoilManager.INSTANCE.calMemoryCache()}")
setImageBitmap(imageMemoryCache.image.toBitmap())
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
//强化clear
//clear()
}
private fun clear() {
Log.d(TAG, "clear")
mThumbDisposable?.dispose()
mImageDisposable?.dispose()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(mHeight, mHeight)
}
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)"
}
}
}
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 item: MyImgView.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(item.data.uri!!, Size(MainActivity.THUMB_WIDTH, MainActivity.THUMB_HEIGHT), 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<MyImgView.Item> {
override fun create(
item: MyImgView.Item,
options: Options,
imageLoader: ImageLoader,
): Fetcher {
return ThumbFetcher(ctx, item, options)
}
}
}