forked from hardcoresushi/DroidFS
Limit the number of thumbnails loaded concurrently
This commit is contained in:
parent
8776d2ee28
commit
1727170cb6
92
app/src/main/java/sushi/hardcore/droidfs/ThumbnailsLoader.kt
Normal file
92
app/src/main/java/sushi/hardcore/droidfs/ThumbnailsLoader.kt
Normal file
@ -0,0 +1,92 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.DrawableImageViewTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||
|
||||
class ThumbnailsLoader(
|
||||
private val context: Context,
|
||||
private val encryptedVolume: EncryptedVolume,
|
||||
private val maxSize: Long,
|
||||
private val lifecycleScope: LifecycleCoroutineScope
|
||||
) {
|
||||
internal class ThumbnailData(val id: Int, val path: String, val imageView: ImageView, val onLoaded: (Drawable) -> Unit)
|
||||
internal class ThumbnailTask(var senderJob: Job?, var workerJob: Job?, var target: DrawableImageViewTarget?)
|
||||
|
||||
private val concurrentTasks = Runtime.getRuntime().availableProcessors()/4
|
||||
private val channel = Channel<ThumbnailData>(concurrentTasks)
|
||||
private var taskId = 0
|
||||
private val tasks = HashMap<Int, ThumbnailTask>()
|
||||
|
||||
private suspend fun loadThumbnail(data: ThumbnailData) {
|
||||
withContext(Dispatchers.IO) {
|
||||
encryptedVolume.loadWholeFile(data.path, maxSize = maxSize).first?.let {
|
||||
yield()
|
||||
withContext(Dispatchers.Main) {
|
||||
tasks[data.id]?.let { task ->
|
||||
val channel = Channel<Unit>(1)
|
||||
task.target = Glide.with(context).load(it).skipMemoryCache(true).into(object : DrawableImageViewTarget(data.imageView) {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
transition: Transition<in Drawable>?
|
||||
) {
|
||||
super.onResourceReady(resource, transition)
|
||||
data.onLoaded(resource)
|
||||
channel.trySend(Unit)
|
||||
}
|
||||
})
|
||||
channel.receive()
|
||||
tasks.remove(data.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initialize() {
|
||||
for (i in 0 until concurrentTasks) {
|
||||
lifecycleScope.launch {
|
||||
while (true) {
|
||||
val data = channel.receive()
|
||||
val workerJob = launch {
|
||||
loadThumbnail(data)
|
||||
}
|
||||
tasks[data.id]?.workerJob = workerJob
|
||||
workerJob.join()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAsync(path: String, target: ImageView, onLoaded: (Drawable) -> Unit): Int {
|
||||
val id = taskId++
|
||||
tasks[id] = ThumbnailTask(null, null, null)
|
||||
val senderJob = lifecycleScope.launch {
|
||||
channel.send(ThumbnailData(id, path, target, onLoaded))
|
||||
}
|
||||
tasks[id]!!.senderJob = senderJob
|
||||
return id
|
||||
}
|
||||
|
||||
fun cancel(id: Int) {
|
||||
tasks[id]?.let { task ->
|
||||
task.senderJob?.cancel()
|
||||
task.workerJob?.cancel()
|
||||
task.target?.let {
|
||||
Glide.with(context).clear(it)
|
||||
}
|
||||
}
|
||||
tasks.remove(id)
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ package sushi.hardcore.droidfs.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.LruCache
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -11,13 +10,12 @@ import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.DrawableImageViewTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import kotlinx.coroutines.*
|
||||
import sushi.hardcore.droidfs.FileTypes
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.ThumbnailsLoader
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerElement
|
||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||
import sushi.hardcore.droidfs.filesystems.Stat
|
||||
@ -29,7 +27,7 @@ class ExplorerElementAdapter(
|
||||
val activity: AppCompatActivity,
|
||||
val encryptedVolume: EncryptedVolume?,
|
||||
private val listener: Listener,
|
||||
val thumbnailMaxSize: Long,
|
||||
thumbnailMaxSize: Long,
|
||||
) : SelectableAdapter<ExplorerElement>(listener::onSelectionChanged) {
|
||||
val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault())
|
||||
var explorerElements = listOf<ExplorerElement>()
|
||||
@ -40,12 +38,18 @@ class ExplorerElementAdapter(
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
var isUsingListLayout = true
|
||||
private var thumbnailsLoader: ThumbnailsLoader? = null
|
||||
private var thumbnailsCache: LruCache<String, Bitmap>? = null
|
||||
var loadThumbnails = true
|
||||
|
||||
init {
|
||||
if (encryptedVolume != null) {
|
||||
thumbnailsCache = LruCache((Runtime.getRuntime().maxMemory() / 1024 / 8).toInt())
|
||||
thumbnailsLoader = ThumbnailsLoader(activity, encryptedVolume, thumbnailMaxSize, activity.lifecycleScope).apply {
|
||||
initialize()
|
||||
}
|
||||
thumbnailsCache = object : LruCache<String, Bitmap>((Runtime.getRuntime().maxMemory() / 4).toInt()) {
|
||||
override fun sizeOf(key: String, value: Bitmap) = value.byteCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,40 +119,11 @@ class ExplorerElementAdapter(
|
||||
}
|
||||
|
||||
class FileViewHolder(itemView: View) : RegularElementViewHolder(itemView) {
|
||||
private var target: DrawableImageViewTarget? = null
|
||||
private var job: Job? = null
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private fun loadThumbnail(fullPath: String, adapter: ExplorerElementAdapter) {
|
||||
adapter.encryptedVolume?.let { volume ->
|
||||
job = scope.launch {
|
||||
volume.loadWholeFile(fullPath, maxSize = adapter.thumbnailMaxSize).first?.let {
|
||||
if (isActive) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (isActive && !adapter.activity.isFinishing && !adapter.activity.isDestroyed) {
|
||||
target = Glide.with(adapter.activity).load(it).skipMemoryCache(true).into(object : DrawableImageViewTarget(icon) {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
transition: Transition<in Drawable>?
|
||||
) {
|
||||
target = null
|
||||
val bitmap = resource.toBitmap()
|
||||
adapter.thumbnailsCache!!.put(fullPath, bitmap.copy(bitmap.config, true))
|
||||
super.onResourceReady(resource, transition)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private var task = -1
|
||||
|
||||
fun cancelThumbnailLoading(adapter: ExplorerElementAdapter) {
|
||||
job?.cancel()
|
||||
target?.let {
|
||||
Glide.with(adapter.activity).clear(it)
|
||||
if (task != -1) {
|
||||
adapter.thumbnailsLoader?.cancel(task)
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,7 +136,10 @@ class ExplorerElementAdapter(
|
||||
icon.setImageBitmap(thumbnail)
|
||||
setDefaultIcon = false
|
||||
} else if (adapter.loadThumbnails) {
|
||||
loadThumbnail(fullPath, adapter)
|
||||
task = adapter.thumbnailsLoader!!.loadAsync(fullPath, icon) { resource ->
|
||||
val bitmap = resource.toBitmap()
|
||||
adapter.thumbnailsCache!!.put(fullPath, bitmap.copy(bitmap.config, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user