From f32135d8d286fadb852917b00a2d486df945dc5b Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Thu, 24 Sep 2026 03:30:38 +0000 Subject: [PATCH] reduce theme-transition and album scroll recomposition --- .../subtracks/ui/library/AlbumDetailScreen.kt | 38 +++++++++-------- .../ui/theme/AnimatedArtworkColors.kt | 41 ++++++++++++++++++- .../com/subtracks/ui/theme/ArtworkTheme.kt | 2 + 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt index 14fbe61d..cfd898fb 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt @@ -48,11 +48,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.lerp import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged @@ -122,14 +123,15 @@ fun AlbumDetailScreen( playingSongId: String? = null, modifier: Modifier = Modifier, ) { - ArtworkTheme(artwork) { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) { + ArtworkTheme(artwork) { BoxWithConstraints(modifier.fillMaxSize().background(Color.Black)) { val listState = rememberLazyListState() val fill = rememberViewportFill(listState) val density = LocalDensity.current val gradientHeight = maxHeight * GRADIENT_SCREENS val gradientHeightPx = with(density) { gradientHeight.toPx() } + val screenHeightPx = with(density) { maxHeight.toPx() } val statusBarTop = WindowInsets.statusBars.getTop(density) val navBarBottom = WindowInsets.navigationBars.getBottom(density) val statusBarDp = with(density) { statusBarTop.toDp() } @@ -155,16 +157,20 @@ fun AlbumDetailScreen( } } } - val barColor = - if (artwork == null || gradientHeightPx <= 0f) { - Color.Black - } else { - val mid = scrollPx + barHeightPx / 2f - lerp( - artwork.gradientColorAt(mid / gradientHeightPx), - artwork.darkPrimary, - heroDarkenAt(mid, with(density) { maxHeight.toPx() }), - ) + val barColor by + remember(artwork, barHeightPx, gradientHeightPx, screenHeightPx) { + derivedStateOf { + if (artwork == null || gradientHeightPx <= 0f) { + Color.Black + } else { + val mid = scrollPx + barHeightPx / 2f + lerp( + artwork.gradientColorAt(mid / gradientHeightPx), + artwork.darkPrimary, + heroDarkenAt(mid, screenHeightPx), + ) + } + } } HeroGradient( @@ -220,7 +226,7 @@ fun AlbumDetailScreen( Brush.verticalGradient( listOf(Color.Black.copy(alpha = 0.55f), Color.Transparent), ), - ).alpha(1f - barFraction), + ).graphicsLayer { alpha = 1f - barFraction }, ) TopAppBar( @@ -230,7 +236,7 @@ fun AlbumDetailScreen( style = MaterialTheme.typography.headlineMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.alpha(barFraction), + modifier = Modifier.graphicsLayer { alpha = barFraction }, ) }, navigationIcon = { @@ -242,7 +248,7 @@ fun AlbumDetailScreen( modifier = Modifier .align(Alignment.TopStart) - .background(barColor.copy(alpha = barFraction)) + .drawBehind { drawRect(barColor.copy(alpha = barFraction)) } .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, diff --git a/app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt b/app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt index d7bb1a2c..f5ea4012 100644 --- a/app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt +++ b/app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt @@ -10,9 +10,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.lerp +import kotlin.math.roundToInt const val ARTWORK_THEME_TRANSITION_MS = 600 +private const val TRANSITION_STEPS = 24 + private val baseArtworkColors = ArtworkColors( scheme = SubtracksColorScheme, @@ -42,7 +45,8 @@ fun rememberAnimatedArtworkColors( } } - val fraction = progress.value + if (from === to) return to + val fraction = (progress.value * TRANSITION_STEPS).roundToInt() / TRANSITION_STEPS.toFloat() return remember(from, to, fraction) { lerpArtworkColors(from, to, fraction) } } @@ -79,4 +83,39 @@ private fun lerpScheme( onTertiary = lerp(a.onTertiary, b.onTertiary, t), tertiaryContainer = lerp(a.tertiaryContainer, b.tertiaryContainer, t), onTertiaryContainer = lerp(a.onTertiaryContainer, b.onTertiaryContainer, t), + background = lerp(a.background, b.background, t), + onBackground = lerp(a.onBackground, b.onBackground, t), + surface = lerp(a.surface, b.surface, t), + onSurface = lerp(a.onSurface, b.onSurface, t), + surfaceVariant = lerp(a.surfaceVariant, b.surfaceVariant, t), + onSurfaceVariant = lerp(a.onSurfaceVariant, b.onSurfaceVariant, t), + surfaceTint = lerp(a.surfaceTint, b.surfaceTint, t), + inverseSurface = lerp(a.inverseSurface, b.inverseSurface, t), + inverseOnSurface = lerp(a.inverseOnSurface, b.inverseOnSurface, t), + error = lerp(a.error, b.error, t), + onError = lerp(a.onError, b.onError, t), + errorContainer = lerp(a.errorContainer, b.errorContainer, t), + onErrorContainer = lerp(a.onErrorContainer, b.onErrorContainer, t), + outline = lerp(a.outline, b.outline, t), + outlineVariant = lerp(a.outlineVariant, b.outlineVariant, t), + scrim = lerp(a.scrim, b.scrim, t), + surfaceBright = lerp(a.surfaceBright, b.surfaceBright, t), + surfaceDim = lerp(a.surfaceDim, b.surfaceDim, t), + surfaceContainer = lerp(a.surfaceContainer, b.surfaceContainer, t), + surfaceContainerHigh = lerp(a.surfaceContainerHigh, b.surfaceContainerHigh, t), + surfaceContainerHighest = lerp(a.surfaceContainerHighest, b.surfaceContainerHighest, t), + surfaceContainerLow = lerp(a.surfaceContainerLow, b.surfaceContainerLow, t), + surfaceContainerLowest = lerp(a.surfaceContainerLowest, b.surfaceContainerLowest, t), + primaryFixed = lerp(a.primaryFixed, b.primaryFixed, t), + primaryFixedDim = lerp(a.primaryFixedDim, b.primaryFixedDim, t), + onPrimaryFixed = lerp(a.onPrimaryFixed, b.onPrimaryFixed, t), + onPrimaryFixedVariant = lerp(a.onPrimaryFixedVariant, b.onPrimaryFixedVariant, t), + secondaryFixed = lerp(a.secondaryFixed, b.secondaryFixed, t), + secondaryFixedDim = lerp(a.secondaryFixedDim, b.secondaryFixedDim, t), + onSecondaryFixed = lerp(a.onSecondaryFixed, b.onSecondaryFixed, t), + onSecondaryFixedVariant = lerp(a.onSecondaryFixedVariant, b.onSecondaryFixedVariant, t), + tertiaryFixed = lerp(a.tertiaryFixed, b.tertiaryFixed, t), + tertiaryFixedDim = lerp(a.tertiaryFixedDim, b.tertiaryFixedDim, t), + onTertiaryFixed = lerp(a.onTertiaryFixed, b.onTertiaryFixed, t), + onTertiaryFixedVariant = lerp(a.onTertiaryFixedVariant, b.onTertiaryFixedVariant, t), ) diff --git a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt index e2d52ebd..b2160eea 100644 --- a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt +++ b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState @@ -31,6 +32,7 @@ private const val DARKEN_START_SCREENS = 0.5f private const val DARKEN_END_SCREENS = 1.5f private const val DARKEN_KNEE = 0.2f +@Immutable data class ArtworkColors( val scheme: ColorScheme, val gradientHigh: Color,