Skip to content

Commit

Permalink
<Merge> recyclerview-performance -> dev
Browse files Browse the repository at this point in the history
  • Loading branch information
chr56 committed Jun 6, 2024
2 parents d4634ea + f1bd088 commit 369d55b
Show file tree
Hide file tree
Showing 18 changed files with 140 additions and 104 deletions.
2 changes: 2 additions & 0 deletions app/src/main/java/player/phonograph/coil/PreloadImageCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ abstract class AbsPreloadImageCache<K, G : Any>(size: Int, @CacheImplementation
}
}

fun peek(context: Context, key: K): G? = cache[id(key)]

suspend fun preload(context: Context, key: K) {
loadAndStore(context, key)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,10 @@ package player.phonograph.coil.album

import coil.map.Mapper
import coil.request.Options
import player.phonograph.coil.model.SongImage
import player.phonograph.coil.model.AlbumImage
import player.phonograph.model.Album
import player.phonograph.repo.loader.Songs
import kotlinx.coroutines.runBlocking

class AlbumImageMapper : Mapper<Album, AlbumImage> {
override fun map(data: Album, options: Options): AlbumImage? = AlbumImage(
data.id,
data.title,
runBlocking{ Songs.album(options.context, data.id).map { SongImage.from(it) } }
).takeIf { it.files.isNotEmpty() }
override fun map(data: Album, options: Options): AlbumImage =
AlbumImage(data.id, data.title)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,9 @@ package player.phonograph.coil.artist
import coil.map.Mapper
import coil.request.Options
import player.phonograph.coil.model.ArtistImage
import player.phonograph.coil.model.SongImage
import player.phonograph.model.Artist
import player.phonograph.repo.loader.Songs
import kotlinx.coroutines.runBlocking

class ArtistImageMapper : Mapper<Artist, ArtistImage> {
override fun map(data: Artist, options: Options): ArtistImage? =
ArtistImage(
data.id,
data.name,
runBlocking { Songs.artist(options.context, data.id).map { SongImage.from(it) } }
).takeIf { it.files.isNotEmpty() }
override fun map(data: Artist, options: Options): ArtistImage =
ArtistImage(data.id, data.name)
}
9 changes: 6 additions & 3 deletions app/src/main/java/player/phonograph/coil/model/AlbumImage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@

package player.phonograph.coil.model

import player.phonograph.repo.loader.Songs
import android.content.Context

data class AlbumImage(
override val id: Long,
val name: String,
val files: List<SongImage>,
) : CompositeLoaderTarget<SongImage> {

override fun disassemble(): Iterable<SongImage> = files
override suspend fun items(context: Context): Iterable<SongImage> =
Songs.album(context, id).map { SongImage.from(it) }

override fun toString(): String =
"AlbumImage(name=$name, id=$id, files=${files.joinToString(prefix = "[", postfix = "]") { it.path }})"
"AlbumImage(name=$name, id=$id,)"
}
9 changes: 6 additions & 3 deletions app/src/main/java/player/phonograph/coil/model/ArtistImage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@

package player.phonograph.coil.model

import player.phonograph.repo.loader.Songs
import android.content.Context

data class ArtistImage(
override val id: Long,
val name: String,
val files: List<SongImage>,
) : CompositeLoaderTarget<SongImage> {

override fun disassemble(): Iterable<SongImage> = files
override suspend fun items(context: Context): Iterable<SongImage> =
Songs.artist(context, id).map { SongImage.from(it) }

override fun toString(): String =
"ArtistImage(name=$name, id=$id, files=${files.joinToString(prefix = "[", postfix = "]") { it.path }})"
"ArtistImage(name=$name, id=$id,)"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package player.phonograph.coil.model

import android.content.Context

interface CompositeLoaderTarget<I> : LoaderTarget {
fun disassemble(): Iterable<I>
suspend fun items(context: Context): Iterable<I>
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import player.phonograph.coil.model.CompositeLoaderTarget
import player.phonograph.coil.model.LoaderTarget
import player.phonograph.coil.model.SongImage
import player.phonograph.mechanism.setting.CoilImageConfig
import player.phonograph.settings.Keys
import player.phonograph.settings.Setting
import player.phonograph.util.debug
import android.content.Context
import android.util.Log
Expand All @@ -31,7 +29,7 @@ sealed class FetcherDelegate<T : LoaderTarget, R : ImageRetriever> {

abstract val cacheStore: CacheStore.Cache<T>

fun retrieve(
suspend fun retrieve(
target: T,
context: Context,
size: Size,
Expand Down Expand Up @@ -75,7 +73,7 @@ sealed class FetcherDelegate<T : LoaderTarget, R : ImageRetriever> {
}
}

abstract fun retrieveImpl(
abstract suspend fun retrieveImpl(
target: T,
context: Context,
size: Size,
Expand All @@ -95,7 +93,7 @@ class AudioFileImageFetcherDelegate<R : ImageRetriever>(

override val cacheStore: CacheStore.Cache<SongImage> = CacheStore.AudioFiles(context.applicationContext)

override fun retrieveImpl(target: SongImage, context: Context, size: Size, rawImage: Boolean): FetchResult? {
override suspend fun retrieveImpl(target: SongImage, context: Context, size: Size, rawImage: Boolean): FetchResult? {
return retriever.retrieve(target.path, target.albumId, context, size, rawImage)
}
}
Expand All @@ -104,7 +102,7 @@ sealed class CompositeFetcherDelegate<T : CompositeLoaderTarget<SongImage>, R :
override val retriever: R,
) : FetcherDelegate<T, R>() {

override fun retrieveImpl(target: T, context: Context, size: Size, rawImage: Boolean): FetchResult? {
override suspend fun retrieveImpl(target: T, context: Context, size: Size, rawImage: Boolean): FetchResult? {

if (enableCache(context)) {
val noImage = cacheStore.isNoImage(target, retriever.id)
Expand All @@ -118,7 +116,7 @@ sealed class CompositeFetcherDelegate<T : CompositeLoaderTarget<SongImage>, R :

val audioFilesCache = CacheStore.AudioFiles(context.applicationContext)

for (file in target.disassemble()) {
for (file in target.items(context)) {

if (enableCache(context)) {
val noSpecificImage = audioFilesCache.isNoImage(file, retriever.id)
Expand Down
36 changes: 15 additions & 21 deletions app/src/main/java/player/phonograph/model/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,27 @@ import android.content.Context
import java.util.*
import player.phonograph.R

fun songCountString(context: Context, songCount: Int): String {
val songString =
if (songCount == 1) context.resources.getString(R.string.song) else context.resources.getString(R.string.songs)
return "$songCount $songString"
}

fun albumCountString(context: Context, albumCount: Int): String {
val albumString =
if (albumCount == 1) context.resources.getString(R.string.album) else context.resources.getString(R.string.albums)
return "$albumCount $albumString"
}

fun Song.infoString(): String =
buildInfoString(artistName, albumName)

fun songCountString(context: Context, songCount: Int): String =
context.resources.getQuantityString(R.plurals.item_songs, songCount, songCount)

fun Artist.infoString(context: Context): String =
buildInfoString(
albumCountString(context, albumCount),
songCountString(context, songCount)
)

fun isArtistNameUnknown(artistName: String?): Boolean = when {
artistName.isNullOrBlank() -> false // not certain
artistName == Artist.UNKNOWN_ARTIST_DISPLAY_NAME -> true
artistName.trim().lowercase() == "unknown" -> true
artistName.trim().lowercase() == "<unknown>" -> true
else -> false
}

fun albumCountString(context: Context, albumCount: Int): String {
val albumString = if (albumCount == 1) context.resources.getString(R.string.album) else context.resources.getString(
R.string.albums
)
return "$albumCount $albumString"
}

fun Album.infoString(context: Context): String =
buildInfoString(
artistName,
Expand All @@ -53,10 +47,10 @@ fun Genre.infoString(context: Context): String =
*/
fun buildInfoString(string1: String?, string2: String?): String =
when {
string1.isNullOrEmpty() && !string2.isNullOrEmpty() -> string2
!string1.isNullOrEmpty() && string2.isNullOrEmpty() -> string1
string1.isNullOrEmpty() && !string2.isNullOrEmpty() -> string2
!string1.isNullOrEmpty() && string2.isNullOrEmpty() -> string1
!string1.isNullOrEmpty() && !string2.isNullOrEmpty() -> "$string1$string2"
else -> ""
else -> ""
}

fun getReadableDurationString(songDurationMillis: Long): String {
Expand Down
85 changes: 53 additions & 32 deletions app/src/main/java/player/phonograph/ui/adapter/DisplayAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.Q
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
Expand All @@ -44,7 +43,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext

abstract class DisplayAdapter<I : Displayable>(
protected val activity: FragmentActivity,
Expand All @@ -57,7 +55,7 @@ abstract class DisplayAdapter<I : Displayable>(
@SuppressLint("NotifyDataSetChanged")
set(value) {
field = value
if (config.layoutStyle.hasImage) imageCacheDelegate.preloadImages(activity, value)
if (config.layoutStyle.hasImage) imageCacheDelegate.startPreloadImages(activity, value)
notifyDataSetChanged()
}

Expand All @@ -84,24 +82,14 @@ abstract class DisplayAdapter<I : Displayable>(

override fun onBindViewHolder(holder: DisplayViewHolder<I>, position: Int) {
val item: I = dataset[position]
holder.bind(item, position, dataset, controller, config.imageType, config.usePalette)
holder.bind(item, position, dataset, controller, config.imageType, config.usePalette, imageCacheDelegate)
}

override fun getItemCount(): Int = dataset.size

override fun getSectionName(position: Int): String =
if (config.showSectionName) getSectionNameImp(position) else ""

override fun onViewAttachedToWindow(holder: DisplayViewHolder<I>) {
if (holder.bindingAdapterPosition in dataset.indices) {
imageCacheDelegate.updateImage(activity, holder, dataset[holder.bindingAdapterPosition], config.usePalette)
} else{
Log.v("ImageCacheDelegate", "Holder has already detached?")
}
}

// override fun onViewDetachedFromWindow(holder: DisplayViewHolder<I>) {}

// for inheriting
open fun getSectionNameImp(position: Int): String =
dataset[position].defaultSortOrderReference()?.substring(0..1) ?: ""
Expand All @@ -115,6 +103,7 @@ abstract class DisplayAdapter<I : Displayable>(
controller: MultiSelectionController<I>,
@ImageType imageType: Int,
usePalette: Boolean,
imageCacheDelegate: ImageCacheDelegate<I>,
) {
shortSeparator?.visibility = View.VISIBLE
itemView.isActivated = controller.isSelected(item)
Expand All @@ -123,7 +112,7 @@ abstract class DisplayAdapter<I : Displayable>(
textSecondary?.text = item.getSecondaryText(itemView.context)
textTertiary?.text = item.getTertiaryText(itemView.context)

prepareImage(imageType, item)
prepareImage(imageType, item, usePalette, imageCacheDelegate)

controller.registerClicking(itemView, position) {
onClick(position, dataset, image)
Expand Down Expand Up @@ -160,7 +149,12 @@ abstract class DisplayAdapter<I : Displayable>(
protected open fun getDescription(item: I): CharSequence? =
item.getDescription(context = itemView.context)

protected open fun prepareImage(@ImageType imageType: Int, item: I) {
protected open fun prepareImage(
@ImageType imageType: Int,
item: I,
usePalette: Boolean,
imageCacheDelegate: ImageCacheDelegate<I>,
) {
when (imageType) {
IMAGE_TYPE_FIXED_ICON -> {
image?.visibility = View.VISIBLE
Expand All @@ -169,8 +163,7 @@ abstract class DisplayAdapter<I : Displayable>(

IMAGE_TYPE_IMAGE -> {
image?.visibility = View.VISIBLE
image?.setImageDrawable(defaultIcon)
setPaletteColors(themeFooterColor(itemView.context))
image?.let { imageView -> fetchImage(item, imageView, usePalette, imageCacheDelegate) }
}

IMAGE_TYPE_TEXT -> {
Expand All @@ -180,6 +173,41 @@ abstract class DisplayAdapter<I : Displayable>(
}
}

protected open fun fetchImage(
item: I,
imageView: ImageView,
usePalette: Boolean,
imageCacheDelegate: ImageCacheDelegate<I>,
) {
val context = imageView.context
val cached = imageCacheDelegate.peek(context, item)
if (cached != null) {
imageView.setImageBitmap(cached.bitmap)
if (usePalette) setPaletteColors(cached.paletteColor)
} else {
coroutineScope.launch {
loadImage(context)
.from(item)
.into(
PaletteTargetBuilder()
.defaultColor(themeFooterColor(context))
.view(imageView)
.onStart {
imageView.setImageDrawable(defaultIcon)
setPaletteColors(themeFooterColor(itemView.context))
}
.onResourceReady { result, paletteColor ->
imageView.setImageDrawable(result)
if (usePalette) setPaletteColors(paletteColor)
}
.build()
)
.enqueue()
}
}
}


protected open fun getIcon(item: I): Drawable? = defaultIcon

protected open fun setImageText(text: String) {
Expand All @@ -199,6 +227,10 @@ abstract class DisplayAdapter<I : Displayable>(
textTertiary?.setTextColor(context.secondaryTextColor(color))
}
}

companion object {
val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
}

class ImageCacheDelegate<I : Displayable>(val config: DisplayConfig) {
Expand All @@ -215,8 +247,8 @@ abstract class DisplayAdapter<I : Displayable>(
_imageCache = value
}

fun preloadImages(context: Context, items: Collection<I>) {
if (config.imageType != IMAGE_TYPE_IMAGE && enabledPreload) return
fun startPreloadImages(context: Context, items: Collection<I>) {
if (!enabledPreload || config.imageType != IMAGE_TYPE_IMAGE) return

imageCache = DisplayPreloadImageCache(items.size.coerceAtLeast(1))
imageCache.imageLoaderScope.launch {
Expand All @@ -226,18 +258,7 @@ abstract class DisplayAdapter<I : Displayable>(
}
}

fun updateImage(context: Context, viewHolder: DisplayViewHolder<I>, item: I, usePalette: Boolean) {
if (config.imageType != IMAGE_TYPE_IMAGE) return

imageCache.imageLoaderScope.launch {
val loaded =
if (enabledPreload) imageCache.fetch(context, item) else imageCache.read(context, item)
withContext(Dispatchers.Main) {
viewHolder.image?.setImageBitmap(loaded.bitmap)
if (usePalette) viewHolder.setPaletteColors(loaded.paletteColor)
}
}
}
fun peek(context: Context, item: I): PaletteBitmap? = imageCache.peek(context, item)

private val enabledPreload: Boolean = Setting(App.instance)[Keys.preloadImages].data
}
Expand Down
Loading

0 comments on commit 369d55b

Please sign in to comment.