Android Glide DiskCacheStrategy.NONE DataFetcher fast loadThumbnail Video, Kotlin(三)
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat
import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.signature.MediaStoreSignature
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "glide-fly"
const val SIZE = 450
const val VIEW_TYPE = 0
const val DATE_TYPE = 1
const val SPAN_COUNT = 6
const val EXTRA_HEIGHT = 1000
const val PAD_SIZE = 1
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rv: RecyclerView = findViewById(R.id.rv)
rv.setHasFixedSize(true)
rv.setItemViewCacheSize(
SPAN_COUNT * 10
)
rv.setRecyclerListener(object : RecyclerView.RecyclerListener {
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if ((holder as MyVH).itemView is MyIV) {
GlideApp.with(holder.itemView.context).clear(holder.itemView)
}
}
})
val layoutManager = MyGridLayoutManager(this, SPAN_COUNT)
layoutManager.orientation = LinearLayoutManager.VERTICAL
rv.layoutManager = layoutManager
val adapter = MyAdapter(this)
rv.adapter = adapter
lifecycleScope.launch(Dispatchers.IO) {
val items = readAllMedia(this@MainActivity)
items.sortByDescending {
it.dateModified
}
val lists = items.distinctBy {
it.dateString
}
lists.forEach { it_lists ->
val idx = items.indexOfFirst {
it_lists.dateString == it.dateString
}
val data = MyData()
data.type = DATE_TYPE
data.dateString = it_lists.dateString
items.add(idx, data) //不要直接加 it_Lists,这里面涉及到List的深拷贝/浅拷贝问题。
}
withContext(Dispatchers.Main) {
adapter.dataChanged(items)
}
}
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (adapter.getItemViewType(position) == DATE_TYPE) {
//group,标题
SPAN_COUNT
} else {
//单个小格子
1
}
}
}
}
class MyGridLayoutManager : GridLayoutManager {
constructor(ctx: Context, cnt: Int) : super(ctx, cnt) {
}
override fun getExtraLayoutSpace(state: RecyclerView.State?): Int {
return EXTRA_HEIGHT
}
}
class MyAdapter : RecyclerView.Adapter<MyVH> {
private var mItems = arrayListOf<MyData>()
private var mContext: Context? = null
private var mPlaceholder: Drawable? = null
private var mError: Drawable? = null
constructor(ctx: Context) {
mContext = ctx
mPlaceholder = ContextCompat.getDrawable(mContext!!, android.R.drawable.ic_menu_gallery)
mError = ContextCompat.getDrawable(mContext!!, android.R.drawable.stat_notify_error)
}
fun dataChanged(items: ArrayList<MyData>) {
this.mItems = items
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyVH {
var v: View?
if (viewType == VIEW_TYPE) {
v = MyIV(mContext!!)
} else {
v = LayoutInflater.from(mContext!!).inflate(android.R.layout.simple_list_item_1, null)
v.setBackgroundColor(Color.LTGRAY)
}
return MyVH(v!!)
}
override fun getItemCount(): Int {
return mItems.size
}
override fun getItemViewType(position: Int): Int {
return mItems[position].type
}
override fun onBindViewHolder(holder: MyVH, @SuppressLint("RecyclerView") position: Int) {
val type = getItemViewType(holder.adapterPosition)
if (type == VIEW_TYPE) {
val miv = holder.itemView as MyIV
val signature = MediaStoreSignature("", 0, 0)
val mt = MyThumb(mItems[holder.adapterPosition], SIZE, SIZE, signature)
GlideApp.with(mContext!!)
.asBitmap()
.load(mt)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.fitCenter()
.signature(signature)
.override(SIZE)
.placeholder(mPlaceholder)
.error(mError)
.into(miv)
} else if (type == DATE_TYPE) {
holder.itemView.findViewById<TextView>(android.R.id.text1).text = "${mItems[holder.adapterPosition].dateString}"
}
}
}
class MyVH : RecyclerView.ViewHolder {
constructor(itemView: View) : super(itemView) {
}
}
class MyIV : AppCompatImageView {
constructor(ctx: Context) : super(ctx) {
setPadding(PAD_SIZE)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val w = MeasureSpec.getSize(widthMeasureSpec)
val h = MeasureSpec.getSize(heightMeasureSpec)
val size = Math.max(w, h) //取w,h的最大值。
setMeasuredDimension(size, size) //使得ImageView为正方形。
}
}
class MyData {
var type = VIEW_TYPE
var dateModified: Long? = 0L
var dateString: String? = null
var filePath: String? = null
var uri: Uri? = null
override fun toString(): String {
return "MyData(dateString=$dateString, filePath=$filePath, uri=$uri)"
}
}
private fun readAllMedia(context: Context): ArrayList<MyData> {
val items = ArrayList<MyData>()
//读所有资源
val cursor = context.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,//MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null,
null,
null,
null
)
val sdf = SimpleDateFormat("yyyy-MM-dd")
val baseUri = Uri.parse("content://media/external/video/media")
while (cursor!!.moveToNext()) {
//文件路径
val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA))
if (TextUtils.isEmpty(path)) {
continue
}
val id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID))
val uri = Uri.withAppendedPath(baseUri, "" + id)
val dateModified = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_MODIFIED))
//名称
//val name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))
//大小
//val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE))
val dateStr = sdf.format(dateModified?.toLong()!! * 1000)
val data = MyData()
data.type = VIEW_TYPE
data.filePath = path
data.uri = uri
data.dateModified = dateModified.toLong()
data.dateString = dateStr
items.add(data)
}
cursor.close()
Log.d(TAG, "size=${items.size}")
return items
}
}
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.engine.cache.MemorySizeCalculator
import com.bumptech.glide.load.engine.executor.GlideExecutor
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
@GlideModule
class MyGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
super.applyOptions(context, builder)
builder.setLogLevel(Log.VERBOSE)
val memoryCacheScreens = 200F
val maxSizeMultiplier = 0.8F
val calculator = MemorySizeCalculator.Builder(context)
.setMemoryCacheScreens(memoryCacheScreens)
.setBitmapPoolScreens(memoryCacheScreens)
.setMaxSizeMultiplier(maxSizeMultiplier)
.setLowMemoryMaxSizeMultiplier(maxSizeMultiplier * 0.8F)
.setArrayPoolSize(((1024 * 1024 * memoryCacheScreens / 2).toInt()))
.build()
builder.setMemorySizeCalculator(calculator)
val memoryCache = MyLruResourceCache.Instance().init(context, calculator.memoryCacheSize.toLong())
builder.setMemoryCache(memoryCache)
val diskCacheSize = 1024 * 1024 * 2000L
builder.setDiskCache(InternalCacheDiskCacheFactory(context, diskCacheSize))
val mSourceBuilder = GlideExecutor.newSourceBuilder()
.setUncaughtThrowableStrategy(GlideExecutor.UncaughtThrowableStrategy.LOG)
.setThreadCount(4)
//.setThreadTimeoutMillis(1000) //线程读写超时时间。
.setName("src")
.build()
val mDiskCacheBuilder = GlideExecutor.newDiskCacheBuilder()
.setThreadCount(1)
//.setThreadTimeoutMillis(1000) //线程读写超时时间。
.setName("diskcache")
.build()
val mAnimationBuilder = GlideExecutor.newAnimationBuilder()
.setThreadCount(1)
//.setThreadTimeoutMillis(1000) //线程读写超时时间。
.setName("anim")
.build()
builder.setSourceExecutor(mSourceBuilder)
builder.setDiskCacheExecutor(mDiskCacheBuilder)
builder.setAnimationExecutor(mAnimationBuilder)
val requestOptions = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.format(DecodeFormat.PREFER_ARGB_8888)
.encodeQuality(100)
.sizeMultiplier(1f)
builder.setDefaultRequestOptions(requestOptions)
}
override fun isManifestParsingEnabled(): Boolean {
return false
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
super.registerComponents(context, glide, registry)
registry.append(MyThumb::class.java, Bitmap::class.java, MyThumbModelLoaderFactory(context))
}
}
import android.content.Context
import android.util.Log
import com.bumptech.glide.load.Key
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.engine.cache.LruResourceCache
class MyLruResourceCache {
companion object {
private val inst by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { MyLruResourceCache() }
fun Instance() = inst
}
private var ctx: Context? = null
private var cache: InnerLruResourceCache? = null
private constructor() {
}
fun init(c: Context, size: Long): InnerLruResourceCache? {
if (cache == null) {
ctx = c
cache = InnerLruResourceCache(size)
}
return cache
}
class InnerLruResourceCache : LruResourceCache {
constructor(size: Long) : super(size) {
}
override fun put(key: Key, item: Resource<*>?): Resource<*>? {
Log.d(MainActivity.TAG, "put key=$key")
return super.put(key, item)
}
override fun remove(key: Key): Resource<*>? {
Log.d(MainActivity.TAG, "remove key=$key")
return super.remove(key)
}
}
}
import com.bumptech.glide.signature.MediaStoreSignature
class MyThumb {
var mData: MainActivity.MyData? = null
var mWith: Int = 0
var mHeight: Int = 0
var mSignature: MediaStoreSignature? = null
private var mHashCode = 0
constructor(d: MainActivity.MyData, w: Int, h: Int, signature: MediaStoreSignature) {
this.mData = d
this.mWith = w
this.mHeight = h
this.mSignature = signature
}
override fun equals(other: Any?): Boolean {
if (other is MyThumb) {
val b = (mData?.uri == other.mData?.uri)
&& (mData?.filePath == other.mData?.filePath)
&& (mData?.dateModified == other.mData?.dateModified)
return b && (mWith == other.mWith)
&& (mHeight == other.mHeight)
&& (mSignature == other.mSignature)
}
return false
}
override fun hashCode(): Int {
if (mHashCode == 0) {
mHashCode = mData?.dateModified.hashCode()
mHashCode = 31 * mHashCode + mData?.uri.hashCode()
mHashCode = 31 * mHashCode + mData?.filePath.hashCode()
mHashCode = 31 * mHashCode + mWith
mHashCode = 31 * mHashCode + mHeight
mHashCode = 31 * mHashCode + mSignature.hashCode()
}
return mHashCode
}
override fun toString(): String {
return "MyThumb(mData=${mData.toString()}, mWith=$mWith, mHeight=$mHeight, mSignature=$mSignature)"
}
}
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.RectF
import android.os.CancellationSignal
import android.util.Log
import android.util.Size
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
import java.lang.Exception
import kotlin.math.min
class MyThumbDataFetcher : DataFetcher<Bitmap> {
private var mModel: MyThumb? = null
private var mContext: Context? = null
private var mSignal: CancellationSignal? = null
constructor(c: Context, m: MyThumb) {
mContext = c
mModel = m
mSignal = CancellationSignal()
}
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Bitmap>) {
val t = System.currentTimeMillis()
val bmp = mContext?.contentResolver?.loadThumbnail(mModel?.mData?.uri!!, Size(MainActivity.SIZE, MainActivity.SIZE), mSignal)
if (bmp != null) {
Log.d(MainActivity.TAG, "loadData 耗时:${System.currentTimeMillis() - t} MyData=${mModel?.mData.toString()}")
callback.onDataReady(scale(bmp))
} else {
callback.onLoadFailed(Exception("loadThumbnail Error"))
}
}
override fun cleanup() {
}
override fun cancel() {
mSignal?.cancel()
}
override fun getDataClass(): Class<Bitmap> {
return Bitmap::class.java
}
override fun getDataSource(): DataSource {
Log.d(MainActivity.TAG, "getDataSource MyData=${mModel?.mData.toString()}")
return DataSource.LOCAL
}
//Bitmap scale to centerCrop()
private fun scale(srcBmp: Bitmap): Bitmap {
val bmp = Bitmap.createBitmap(MainActivity.SIZE, MainActivity.SIZE, Bitmap.Config.ARGB_8888)
val c = Canvas(bmp)
val width: Int = srcBmp.width
val height: Int = srcBmp.height
val bmpCenterX: Float = width / 2f
val bmpCenterY: Float = height / 2f
val minVal = min(width, height)
val srcRectF = RectF(
bmpCenterX - minVal / 2,
bmpCenterY - minVal / 2,
bmpCenterX + minVal / 2,
bmpCenterY + minVal / 2
)
val dstRectF = RectF(0f, 0f, MainActivity.SIZE.toFloat(), MainActivity.SIZE.toFloat())
val matrix = Matrix()
matrix.setRectToRect(srcRectF, dstRectF, Matrix.ScaleToFit.CENTER)
c.drawBitmap(srcBmp, matrix, null)
return bmp
}
}
import com.bumptech.glide.load.Key
import java.security.MessageDigest
class MyThumbKey : Key {
private var mModel: MyThumb? = null
private var mHashCode = 0
constructor(m: MyThumb) {
mModel = m
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
//messageDigest.update(mModel.toString().toByte())
}
override fun equals(other: Any?): Boolean {
if (other is MyThumbKey) {
return (mModel?.mData == other.mModel?.mData) && (mModel?.mWith == other.mModel?.mWith) && (mModel?.mHeight == other.mModel?.mHeight) && (mModel?.mSignature == other.mModel?.mSignature)
}
return false
}
override fun hashCode(): Int {
if (mHashCode == 0) {
mHashCode = mModel!!.mData.hashCode()
mHashCode = 31 * mHashCode + mModel!!.mWith
mHashCode = 31 * mHashCode + mModel!!.mHeight
mHashCode = 31 * mHashCode + mModel!!.mSignature.hashCode()
}
return mHashCode
}
}
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.ModelLoader
class MyThumbModelLoader : ModelLoader<MyThumb, Bitmap> {
private var mContext: Context? = null
constructor(c: Context) {
mContext = c
}
override fun buildLoadData(model: MyThumb, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap>? {
Log.d(MainActivity.TAG, "buildLoadData w=$width h=$height model=$model")
return ModelLoader.LoadData(MyThumbKey(model), MyThumbDataFetcher(mContext!!, model))
}
override fun handles(model: MyThumb): Boolean {
return true
}
}
import android.content.Context
import android.graphics.Bitmap
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
class MyThumbModelLoaderFactory : ModelLoaderFactory<MyThumb, Bitmap> {
private var mContext: Context? = null
constructor(c: Context) {
mContext = c
}
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MyThumb, Bitmap> {
return MyThumbModelLoader(mContext!!)
}
override fun teardown() {
}
}