mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
bound and harden the artwork seed cache
This commit is contained in:
@@ -9,12 +9,11 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.lerp
|
import androidx.compose.ui.graphics.lerp
|
||||||
|
|
||||||
const val ARTWORK_THEME_TRANSITION_MS = 600
|
const val ARTWORK_THEME_TRANSITION_MS = 600
|
||||||
|
|
||||||
fun baseArtworkColors(): ArtworkColors =
|
private val baseArtworkColors =
|
||||||
ArtworkColors(
|
ArtworkColors(
|
||||||
scheme = SubtracksColorScheme,
|
scheme = SubtracksColorScheme,
|
||||||
gradientHigh = SubtracksColorScheme.surfaceContainerHigh,
|
gradientHigh = SubtracksColorScheme.surfaceContainerHigh,
|
||||||
@@ -29,15 +28,14 @@ fun rememberAnimatedArtworkColors(
|
|||||||
target: ArtworkColors?,
|
target: ArtworkColors?,
|
||||||
durationMillis: Int = ARTWORK_THEME_TRANSITION_MS,
|
durationMillis: Int = ARTWORK_THEME_TRANSITION_MS,
|
||||||
): ArtworkColors {
|
): ArtworkColors {
|
||||||
val base = remember { baseArtworkColors() }
|
var from by remember { mutableStateOf(target ?: baseArtworkColors) }
|
||||||
var from by remember { mutableStateOf(target ?: base) }
|
var to by remember { mutableStateOf(target ?: baseArtworkColors) }
|
||||||
var to by remember { mutableStateOf(target ?: base) }
|
|
||||||
val progress = remember { Animatable(1f) }
|
val progress = remember { Animatable(1f) }
|
||||||
|
|
||||||
LaunchedEffect(target) {
|
LaunchedEffect(target, durationMillis) {
|
||||||
val next = target ?: base
|
val next = target ?: baseArtworkColors
|
||||||
if (next !== to) {
|
if (next !== to) {
|
||||||
from = to
|
from = lerpArtworkColors(from, to, progress.value)
|
||||||
to = next
|
to = next
|
||||||
progress.snapTo(0f)
|
progress.snapTo(0f)
|
||||||
progress.animateTo(1f, tween(durationMillis = durationMillis))
|
progress.animateTo(1f, tween(durationMillis = durationMillis))
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.subtracks.ui.theme
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.collection.LruCache
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -21,26 +23,42 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
private const val SEED_ART_SIZE_PX = 128
|
private const val SEED_ART_SIZE_PX = 128
|
||||||
private const val PREFETCH_SETTLE_MS = 250L
|
private const val PREFETCH_SETTLE_MS = 250L
|
||||||
|
private const val MAX_CACHED_SEEDS = 256
|
||||||
|
|
||||||
object ArtworkSeedCache {
|
object ArtworkSeedCache {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val jobs = ConcurrentHashMap<String, Deferred<Pair<Int, Int?>?>>()
|
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,
|
context: Context,
|
||||||
ref: CoverArtRef,
|
ref: CoverArtRef,
|
||||||
): Deferred<Pair<Int, Int?>?> =
|
): Pair<Int, Int?>? {
|
||||||
jobs.getOrPut(ref.cacheKey) {
|
cache.get(ref.cacheKey)?.let { return it }
|
||||||
scope.async {
|
val job = jobs.computeIfAbsent(ref.cacheKey) { scope.async { extract(context, ref) } }
|
||||||
val seeds = extract(context, ref)
|
val seeds = job.await()
|
||||||
if (seeds == null) jobs.remove(ref.cacheKey)
|
jobs.remove(ref.cacheKey, job)
|
||||||
seeds
|
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(
|
private suspend fun extract(
|
||||||
@@ -108,6 +126,6 @@ fun PrefetchArtworkSeeds(ref: CoverArtRef?) {
|
|||||||
LaunchedEffect(ref?.cacheKey) {
|
LaunchedEffect(ref?.cacheKey) {
|
||||||
if (ref == null) return@LaunchedEffect
|
if (ref == null) return@LaunchedEffect
|
||||||
delay(PREFETCH_SETTLE_MS)
|
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 {
|
fun ArtworkColors.gradientColorAt(fraction: Float): Color {
|
||||||
if (fraction.isNaN()) return gradientHigh
|
if (!fraction.isFinite()) return gradientHigh
|
||||||
val f = fraction.mod(1f)
|
val f = fraction.mod(1f)
|
||||||
return if (f < 0.5f) {
|
return if (f < 0.5f) {
|
||||||
lerp(gradientHigh, gradientLow, f * 2f)
|
lerp(gradientHigh, gradientLow, f * 2f)
|
||||||
@@ -173,7 +173,7 @@ fun rememberArtworkColors(
|
|||||||
private fun rememberArtworkSeed(ref: CoverArtRef?): State<Pair<Int, Int?>?> {
|
private fun rememberArtworkSeed(ref: CoverArtRef?): State<Pair<Int, Int?>?> {
|
||||||
val context = LocalPlatformContext.current
|
val context = LocalPlatformContext.current
|
||||||
return produceState<Pair<Int, Int?>?>(initialValue = null, ref?.cacheKey) {
|
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
|
@Before
|
||||||
fun installImageLoader() {
|
fun installImageLoader() {
|
||||||
|
ArtworkSeedCache.clear()
|
||||||
SingletonImageLoader.reset()
|
SingletonImageLoader.reset()
|
||||||
SingletonImageLoader.setUnsafe(
|
SingletonImageLoader.setUnsafe(
|
||||||
ImageLoader
|
ImageLoader
|
||||||
@@ -48,6 +49,7 @@ class ArtworkExtractionTest {
|
|||||||
|
|
||||||
@After
|
@After
|
||||||
fun resetImageLoader() {
|
fun resetImageLoader() {
|
||||||
|
ArtworkSeedCache.clear()
|
||||||
SingletonImageLoader.reset()
|
SingletonImageLoader.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user