fix(android): hold previous NowPlaying gradient color across track change
rememberDominantColor was keyed on coverUrl, so each track change reset the extracted color to Transparent and the gradient dipped toward the fallback before tweening to the new color. Drop the key so the held color stays on the previous track's dominant until the new palette resolves -- the gradient now tweens directly to the new color. Combined with CoverPrefetcher warming the next cover, this matches Flutter's preload-then-atomic-swap UX (no flash on track change) via native Compose mechanisms rather than a literal preload pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,15 +27,21 @@ private const val GRADIENT_TWEEN_MS = 600
|
|||||||
* androidx.palette on a background thread, and animates the result
|
* androidx.palette on a background thread, and animates the result
|
||||||
* via [animateColorAsState] so track changes tween smoothly.
|
* via [animateColorAsState] so track changes tween smoothly.
|
||||||
*
|
*
|
||||||
* Returns [Color.Transparent] while the bitmap is loading or on
|
* The held color is NOT reset when [coverUrl] changes — it stays on
|
||||||
* any failure — callers should layer a fallback (e.g.
|
* the previous track's dominant until the new palette resolves, so the
|
||||||
* `MaterialTheme.colorScheme.surface`) underneath the gradient
|
* gradient tweens old→new directly instead of dipping toward the
|
||||||
* tint so the screen never sits on a flat black.
|
* fallback mid-swap. Mirrors `now_playing_screen.dart`'s preload-then-
|
||||||
|
* swap ("keep the previous dominant"); the cover image swaps smoothly
|
||||||
|
* via `CoverPrefetcher`, which warms the next track's bytes into Coil.
|
||||||
|
*
|
||||||
|
* Starts at [Color.Transparent] (cold mount) and resets to it only
|
||||||
|
* when a track has no cover — callers layer a base color underneath so
|
||||||
|
* the screen never sits on flat black.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberDominantColor(coverUrl: String?): Color {
|
fun rememberDominantColor(coverUrl: String?): Color {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var extracted by remember(coverUrl) { mutableStateOf(Color.Transparent) }
|
var extracted by remember { mutableStateOf(Color.Transparent) }
|
||||||
|
|
||||||
LaunchedEffect(coverUrl) {
|
LaunchedEffect(coverUrl) {
|
||||||
if (coverUrl.isNullOrEmpty()) {
|
if (coverUrl.isNullOrEmpty()) {
|
||||||
@@ -47,6 +53,7 @@ fun rememberDominantColor(coverUrl: String?): Color {
|
|||||||
.allowHardware(false)
|
.allowHardware(false)
|
||||||
.build()
|
.build()
|
||||||
val result = SingletonImageLoader.get(context).execute(request)
|
val result = SingletonImageLoader.get(context).execute(request)
|
||||||
|
// Keep the previous dominant on load failure rather than dropping.
|
||||||
if (result !is SuccessResult) return@LaunchedEffect
|
if (result !is SuccessResult) return@LaunchedEffect
|
||||||
val bitmap = result.image.toBitmap()
|
val bitmap = result.image.toBitmap()
|
||||||
val palette = withContext(Dispatchers.Default) {
|
val palette = withContext(Dispatchers.Default) {
|
||||||
|
|||||||
Reference in New Issue
Block a user