Android: Use custom image loader for game covers

This fixes a bug where custom cover loading was initiated but would finish by the time another image view would be in the place of the previous one.
This commit is contained in:
Charles Lombardo 2023-09-20 14:12:22 -04:00
parent 579ccb0710
commit f13b29196d
5 changed files with 82 additions and 69 deletions

View File

@ -25,7 +25,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.utils.CoilUtils
import java.util.ArrayList
class GameAdapter(private val mActivity: FragmentActivity) : RecyclerView.Adapter<GameViewHolder>(),
class GameAdapter : RecyclerView.Adapter<GameViewHolder>(),
View.OnClickListener, OnLongClickListener {
private var mGameFiles: List<GameFile> = ArrayList()
@ -72,20 +72,7 @@ class GameAdapter(private val mActivity: FragmentActivity) : RecyclerView.Adapte
binding.textGameCaption.visibility = View.GONE
}
}
mActivity.lifecycleScope.launch {
withContext(Dispatchers.IO) {
val customCoverUri = CoilUtils.findCustomCover(gameFile)
withContext(Dispatchers.Main) {
CoilUtils.loadGameCover(
holder,
holder.binding.imageGameScreen,
gameFile,
customCoverUri
)
}
}
}
CoilUtils.loadGameCover(holder, holder.binding.imageGameScreen, gameFile)
val animateIn = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_in)
animateIn.fillAfter = true

View File

@ -24,7 +24,7 @@ import org.dolphinemu.dolphinemu.utils.CoilUtils
* The Leanback library / docs call this a Presenter, but it works very
* similarly to a RecyclerView.Adapter.
*/
class GameRowPresenter(private val mActivity: FragmentActivity) : Presenter() {
class GameRowPresenter : Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
// Create a new view.
@ -69,20 +69,7 @@ class GameRowPresenter(private val mActivity: FragmentActivity) : Presenter() {
holder.cardParent.contentText = gameFile.getCompany()
}
}
mActivity.lifecycleScope.launch {
withContext(Dispatchers.IO) {
val customCoverUri = CoilUtils.findCustomCover(gameFile)
withContext(Dispatchers.Main) {
CoilUtils.loadGameCover(
null,
holder.imageScreenshot,
gameFile,
customCoverUri
)
}
}
}
CoilUtils.loadGameCover(null, holder.imageScreenshot, gameFile)
}
override fun onUnbindViewHolder(viewHolder: ViewHolder) {

View File

@ -268,7 +268,7 @@ class TvMainActivity : FragmentActivity(), MainView, OnRefreshListener {
}
// Create an adapter for this row.
val row = ArrayObjectAdapter(GameRowPresenter(this))
val row = ArrayObjectAdapter(GameRowPresenter())
row.addAll(0, gameFiles)
// Keep a reference to the row in case we need to refresh it.

View File

@ -37,7 +37,7 @@ class PlatformGamesFragment : Fragment(), PlatformGamesView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
swipeRefresh = binding.swipeRefresh
val gameAdapter = GameAdapter(requireActivity())
val gameAdapter = GameAdapter()
gameAdapter.stateRestorationPolicy =
RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY

View File

@ -2,11 +2,22 @@
package org.dolphinemu.dolphinemu.utils
import android.graphics.drawable.Drawable
import android.net.Uri
import android.view.View
import android.widget.ImageView
import coil.load
import coil.target.ImageViewTarget
import coil.ImageLoader
import coil.decode.DataSource
import coil.executeBlocking
import coil.fetch.DrawableResult
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.imageLoader
import coil.key.Keyer
import coil.memory.MemoryCache
import coil.request.ImageRequest
import coil.request.Options
import org.dolphinemu.dolphinemu.DolphinApplication
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
@ -14,47 +25,75 @@ import org.dolphinemu.dolphinemu.model.GameFile
import java.io.File
import java.io.FileNotFoundException
class GameCoverFetcher(
private val game: GameFile,
private val options: Options
) : Fetcher {
override suspend fun fetch(): FetchResult {
val customCoverUri = CoilUtils.findCustomCover(game)
val builder = ImageRequest.Builder(DolphinApplication.getAppContext())
var dataSource = DataSource.DISK
val drawable: Drawable? = if (customCoverUri != null) {
val request = builder.data(customCoverUri).error(R.drawable.no_banner).build()
DolphinApplication.getAppContext().imageLoader.executeBlocking(request).drawable
} else if (BooleanSetting.MAIN_USE_GAME_COVERS.boolean) {
val request = builder.data(
CoverHelper.buildGameTDBUrl(game, CoverHelper.getRegion(game))
).error(R.drawable.no_banner).build()
dataSource = DataSource.NETWORK
DolphinApplication.getAppContext().imageLoader.executeBlocking(request).drawable
} else {
null
}
return DrawableResult(
// In the case where the drawable is null, intentionally throw an NPE. This tells Coil
// to load the error drawable.
drawable = drawable!!,
isSampled = false,
dataSource = dataSource
)
}
class Factory : Fetcher.Factory<GameFile> {
override fun create(data: GameFile, options: Options, imageLoader: ImageLoader): Fetcher =
GameCoverFetcher(data, options)
}
}
class GameCoverKeyer : Keyer<GameFile> {
override fun key(data: GameFile, options: Options): String = data.getGameId() + data.getPath()
}
object CoilUtils {
private val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext())
.components {
add(GameCoverKeyer())
add(GameCoverFetcher.Factory())
}
.memoryCache {
MemoryCache.Builder(DolphinApplication.getAppContext())
.maxSizePercent(0.25)
.build()
}
.build()
fun loadGameCover(
gameViewHolder: GameViewHolder?,
imageView: ImageView,
gameFile: GameFile,
customCoverUri: Uri?
gameFile: GameFile
) {
imageView.scaleType = ImageView.ScaleType.FIT_CENTER
val imageTarget = ImageViewTarget(imageView)
if (customCoverUri != null) {
imageView.load(customCoverUri) {
error(R.drawable.no_banner)
target(
onSuccess = { success ->
disableInnerTitle(gameViewHolder)
imageTarget.drawable = success
},
onError = { error ->
enableInnerTitle(gameViewHolder)
imageTarget.drawable = error
}
)
}
} else if (BooleanSetting.MAIN_USE_GAME_COVERS.boolean) {
imageView.load(CoverHelper.buildGameTDBUrl(gameFile, CoverHelper.getRegion(gameFile))) {
error(R.drawable.no_banner)
target(
onSuccess = { success ->
disableInnerTitle(gameViewHolder)
imageTarget.drawable = success
},
onError = { error ->
enableInnerTitle(gameViewHolder)
imageTarget.drawable = error
}
)
}
} else {
imageView.load(R.drawable.no_banner)
enableInnerTitle(gameViewHolder)
}
val imageRequest = ImageRequest.Builder(imageView.context)
.data(gameFile)
.error(R.drawable.no_banner)
.target(imageView)
.listener(
onSuccess = { _, _ -> disableInnerTitle(gameViewHolder) },
onError = { _, _ -> enableInnerTitle(gameViewHolder) }
)
.build()
imageLoader.enqueue(imageRequest)
}
private fun enableInnerTitle(gameViewHolder: GameViewHolder?) {