bound and harden the artwork seed cache

This commit is contained in:
austinried
2026-10-04 00:29:53 +00:00
parent f2ebac783d
commit ee3bf734da
5 changed files with 38 additions and 20 deletions
@@ -9,12 +9,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
const val ARTWORK_THEME_TRANSITION_MS = 600
fun baseArtworkColors(): ArtworkColors =
private val baseArtworkColors =
ArtworkColors(
scheme = SubtracksColorScheme,
gradientHigh = SubtracksColorScheme.surfaceContainerHigh,
@@ -29,15 +28,14 @@ fun rememberAnimatedArtworkColors(
target: ArtworkColors?,
durationMillis: Int = ARTWORK_THEME_TRANSITION_MS,
): ArtworkColors {
val base = remember { baseArtworkColors() }
var from by remember { mutableStateOf(target ?: base) }
var to by remember { mutableStateOf(target ?: base) }
var from by remember { mutableStateOf(target ?: baseArtworkColors) }
var to by remember { mutableStateOf(target ?: baseArtworkColors) }
val progress = remember { Animatable(1f) }
LaunchedEffect(target) {
val next = target ?: base
LaunchedEffect(target, durationMillis) {
val next = target ?: baseArtworkColors
if (next !== to) {
from = to
from = lerpArtworkColors(from, to, progress.value)
to = next
progress.snapTo(0f)
progress.animateTo(1f, tween(durationMillis = durationMillis))
@@ -2,6 +2,8 @@ package com.subtracks.ui.theme
import android.content.Context
import android.graphics.Bitmap
import androidx.annotation.VisibleForTesting
import androidx.collection.LruCache
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.graphics.Color
@@ -21,27 +23,43 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.abs
private const val SEED_ART_SIZE_PX = 128
private const val PREFETCH_SETTLE_MS = 250L
private const val MAX_CACHED_SEEDS = 256
object ArtworkSeedCache {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val jobs = ConcurrentHashMap<String, Deferred<Pair<Int, Int?>?>>()
private val cache = LruCache<String, Pair<Int, Int?>>(MAX_CACHED_SEEDS)
fun load(
suspend fun seeds(
context: Context,
ref: CoverArtRef,
): Deferred<Pair<Int, Int?>?> =
jobs.getOrPut(ref.cacheKey) {
scope.async {
val seeds = extract(context, ref)
if (seeds == null) jobs.remove(ref.cacheKey)
seeds
}
}
): Pair<Int, Int?>? {
cache.get(ref.cacheKey)?.let { return it }
val job = jobs.computeIfAbsent(ref.cacheKey) { scope.async { extract(context, ref) } }
val seeds = job.await()
jobs.remove(ref.cacheKey, job)
if (seeds != null) cache.put(ref.cacheKey, seeds)
return seeds
}
fun prefetch(
context: Context,
ref: CoverArtRef,
) {
scope.launch { seeds(context, ref) }
}
@VisibleForTesting
fun clear() {
jobs.clear()
cache.evictAll()
}
private suspend fun extract(
context: Context,
@@ -108,6 +126,6 @@ fun PrefetchArtworkSeeds(ref: CoverArtRef?) {
LaunchedEffect(ref?.cacheKey) {
if (ref == null) return@LaunchedEffect
delay(PREFETCH_SETTLE_MS)
ArtworkSeedCache.load(context, ref)
ArtworkSeedCache.prefetch(context, ref)
}
}
@@ -141,7 +141,7 @@ private fun Color.withMinLuminance(min: Float): Color {
}
fun ArtworkColors.gradientColorAt(fraction: Float): Color {
if (fraction.isNaN()) return gradientHigh
if (!fraction.isFinite()) return gradientHigh
val f = fraction.mod(1f)
return if (f < 0.5f) {
lerp(gradientHigh, gradientLow, f * 2f)
@@ -173,7 +173,7 @@ fun rememberArtworkColors(
private fun rememberArtworkSeed(ref: CoverArtRef?): State<Pair<Int, Int?>?> {
val context = LocalPlatformContext.current
return produceState<Pair<Int, Int?>?>(initialValue = null, ref?.cacheKey) {
value = ref?.let { ArtworkSeedCache.load(context, it).await() }
value = ref?.let { ArtworkSeedCache.seeds(context, it) }
}
}
@@ -37,6 +37,7 @@ class ArtworkExtractionTest {
@Before
fun installImageLoader() {
ArtworkSeedCache.clear()
SingletonImageLoader.reset()
SingletonImageLoader.setUnsafe(
ImageLoader
@@ -48,6 +49,7 @@ class ArtworkExtractionTest {
@After
fun resetImageLoader() {
ArtworkSeedCache.clear()
SingletonImageLoader.reset()
}