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 org.dolphinemu.dolphinemu.utils.CoilUtils
import java.util.ArrayList import java.util.ArrayList
class GameAdapter(private val mActivity: FragmentActivity) : RecyclerView.Adapter<GameViewHolder>(), class GameAdapter : RecyclerView.Adapter<GameViewHolder>(),
View.OnClickListener, OnLongClickListener { View.OnClickListener, OnLongClickListener {
private var mGameFiles: List<GameFile> = ArrayList() private var mGameFiles: List<GameFile> = ArrayList()
@ -72,20 +72,7 @@ class GameAdapter(private val mActivity: FragmentActivity) : RecyclerView.Adapte
binding.textGameCaption.visibility = View.GONE binding.textGameCaption.visibility = View.GONE
} }
} }
CoilUtils.loadGameCover(holder, holder.binding.imageGameScreen, gameFile)
mActivity.lifecycleScope.launch {
withContext(Dispatchers.IO) {
val customCoverUri = CoilUtils.findCustomCover(gameFile)
withContext(Dispatchers.Main) {
CoilUtils.loadGameCover(
holder,
holder.binding.imageGameScreen,
gameFile,
customCoverUri
)
}
}
}
val animateIn = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_in) val animateIn = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_in)
animateIn.fillAfter = true 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 * The Leanback library / docs call this a Presenter, but it works very
* similarly to a RecyclerView.Adapter. * similarly to a RecyclerView.Adapter.
*/ */
class GameRowPresenter(private val mActivity: FragmentActivity) : Presenter() { class GameRowPresenter : Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
// Create a new view. // Create a new view.
@ -69,20 +69,7 @@ class GameRowPresenter(private val mActivity: FragmentActivity) : Presenter() {
holder.cardParent.contentText = gameFile.getCompany() holder.cardParent.contentText = gameFile.getCompany()
} }
} }
CoilUtils.loadGameCover(null, holder.imageScreenshot, gameFile)
mActivity.lifecycleScope.launch {
withContext(Dispatchers.IO) {
val customCoverUri = CoilUtils.findCustomCover(gameFile)
withContext(Dispatchers.Main) {
CoilUtils.loadGameCover(
null,
holder.imageScreenshot,
gameFile,
customCoverUri
)
}
}
}
} }
override fun onUnbindViewHolder(viewHolder: ViewHolder) { override fun onUnbindViewHolder(viewHolder: ViewHolder) {

View File

@ -268,7 +268,7 @@ class TvMainActivity : FragmentActivity(), MainView, OnRefreshListener {
} }
// Create an adapter for this row. // Create an adapter for this row.
val row = ArrayObjectAdapter(GameRowPresenter(this)) val row = ArrayObjectAdapter(GameRowPresenter())
row.addAll(0, gameFiles) row.addAll(0, gameFiles)
// Keep a reference to the row in case we need to refresh it. // 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
swipeRefresh = binding.swipeRefresh swipeRefresh = binding.swipeRefresh
val gameAdapter = GameAdapter(requireActivity()) val gameAdapter = GameAdapter()
gameAdapter.stateRestorationPolicy = gameAdapter.stateRestorationPolicy =
RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY

View File

@ -2,11 +2,22 @@
package org.dolphinemu.dolphinemu.utils package org.dolphinemu.dolphinemu.utils
import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import coil.load import coil.ImageLoader
import coil.target.ImageViewTarget 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.R
import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting 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.File
import java.io.FileNotFoundException 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 { 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( fun loadGameCover(
gameViewHolder: GameViewHolder?, gameViewHolder: GameViewHolder?,
imageView: ImageView, imageView: ImageView,
gameFile: GameFile, gameFile: GameFile
customCoverUri: Uri?
) { ) {
imageView.scaleType = ImageView.ScaleType.FIT_CENTER imageView.scaleType = ImageView.ScaleType.FIT_CENTER
val imageTarget = ImageViewTarget(imageView) val imageRequest = ImageRequest.Builder(imageView.context)
if (customCoverUri != null) { .data(gameFile)
imageView.load(customCoverUri) { .error(R.drawable.no_banner)
error(R.drawable.no_banner) .target(imageView)
target( .listener(
onSuccess = { success -> onSuccess = { _, _ -> disableInnerTitle(gameViewHolder) },
disableInnerTitle(gameViewHolder) onError = { _, _ -> enableInnerTitle(gameViewHolder) }
imageTarget.drawable = success )
}, .build()
onError = { error -> imageLoader.enqueue(imageRequest)
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)
}
} }
private fun enableInnerTitle(gameViewHolder: GameViewHolder?) { private fun enableInnerTitle(gameViewHolder: GameViewHolder?) {