mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
make the library header fade over the scrolling hero gradient
This commit is contained in:
@@ -50,7 +50,6 @@ import androidx.compose.ui.draw.drawBehind
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.lerp
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.layout.positionInParent
|
import androidx.compose.ui.layout.positionInParent
|
||||||
@@ -63,10 +62,8 @@ import com.subtracks.data.model.CoverArtRef
|
|||||||
import com.subtracks.ui.theme.ArtworkColors
|
import com.subtracks.ui.theme.ArtworkColors
|
||||||
import com.subtracks.ui.theme.ArtworkTheme
|
import com.subtracks.ui.theme.ArtworkTheme
|
||||||
import com.subtracks.ui.theme.HeroGradient
|
import com.subtracks.ui.theme.HeroGradient
|
||||||
import com.subtracks.ui.theme.gradientColorAt
|
import com.subtracks.ui.theme.heroBarColor
|
||||||
import com.subtracks.ui.theme.heroDarkenAt
|
|
||||||
|
|
||||||
private const val GRADIENT_SCREENS = 2.0f
|
|
||||||
private const val FADE_DISTANCE_DP = 64
|
private const val FADE_DISTANCE_DP = 64
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +89,6 @@ fun HeroDetailScaffold(
|
|||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val fill = rememberViewportFill(listState)
|
val fill = rememberViewportFill(listState)
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val gradientHeightPx = with(density) { (maxHeight * GRADIENT_SCREENS).toPx() }
|
|
||||||
val screenHeightPx = with(density) { maxHeight.toPx() }
|
val screenHeightPx = with(density) { maxHeight.toPx() }
|
||||||
val statusBarTop = WindowInsets.statusBars.getTop(density)
|
val statusBarTop = WindowInsets.statusBars.getTop(density)
|
||||||
val statusBarDp = with(density) { statusBarTop.toDp() }
|
val statusBarDp = with(density) { statusBarTop.toDp() }
|
||||||
@@ -120,18 +116,9 @@ fun HeroDetailScaffold(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val barColor by
|
val barColor by
|
||||||
remember(artwork, barHeightPx, gradientHeightPx, screenHeightPx) {
|
remember(artwork, barHeightPx, screenHeightPx) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (artwork == null || gradientHeightPx <= 0f) {
|
heroBarColor(artwork, scrollPx, barHeightPx, screenHeightPx)
|
||||||
Color.Black
|
|
||||||
} else {
|
|
||||||
val mid = scrollPx + barHeightPx / 2f
|
|
||||||
lerp(
|
|
||||||
artwork.gradientColorAt(mid / gradientHeightPx),
|
|
||||||
artwork.darkPrimary,
|
|
||||||
heroDarkenAt(mid, screenHeightPx),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -39,7 +40,9 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -50,10 +53,16 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.lerp
|
import androidx.compose.ui.graphics.lerp
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.layout.boundsInParent
|
import androidx.compose.ui.layout.boundsInParent
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
@@ -71,7 +80,7 @@ import com.subtracks.data.model.Playlist
|
|||||||
import com.subtracks.data.model.SongListItem
|
import com.subtracks.data.model.SongListItem
|
||||||
import com.subtracks.ui.theme.HeroGradient
|
import com.subtracks.ui.theme.HeroGradient
|
||||||
import com.subtracks.ui.theme.LocalPlayerArtwork
|
import com.subtracks.ui.theme.LocalPlayerArtwork
|
||||||
import com.subtracks.ui.theme.playerSurfaceColor
|
import com.subtracks.ui.theme.heroBarColor
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -170,22 +179,66 @@ fun LibraryScreen(
|
|||||||
SideEffect { scrollBehavior.state.heightOffsetLimit = -with(density) { titleHeight.toPx() } }
|
SideEffect { scrollBehavior.state.heightOffsetLimit = -with(density) { titleHeight.toPx() } }
|
||||||
val titleFraction = scrollBehavior.state.collapsedFraction
|
val titleFraction = scrollBehavior.state.collapsedFraction
|
||||||
|
|
||||||
val statusBarTop = with(density) { WindowInsets.statusBars.getTop(this).toDp() }
|
var scrollPx by remember { mutableFloatStateOf(0f) }
|
||||||
val artwork = LocalPlayerArtwork.current
|
val scrollTracker =
|
||||||
val headerColor = artwork?.let(::playerSurfaceColor) ?: MaterialTheme.colorScheme.surfaceContainerHigh
|
remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource,
|
||||||
|
): Offset {
|
||||||
|
scrollPx = (scrollPx - consumed.y).coerceAtLeast(0f)
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
val artwork = LocalPlayerArtwork.current
|
||||||
|
val statusBarTopPx = WindowInsets.statusBars.getTop(density)
|
||||||
|
val statusBarTop = with(density) { statusBarTopPx.toDp() }
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.nestedScroll(scrollTracker)
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) {
|
) {
|
||||||
|
val screenHeightPx = with(density) { maxHeight.toPx() }
|
||||||
|
val barHeightPx = statusBarTopPx + with(density) { TopAppBarDefaults.TopAppBarExpandedHeight.toPx() }
|
||||||
|
val selectedColor = heroBarColor(artwork, 0f, barHeightPx, screenHeightPx)
|
||||||
|
val barColor by
|
||||||
|
remember(artwork, barHeightPx, screenHeightPx) {
|
||||||
|
derivedStateOf { heroBarColor(artwork, scrollPx, barHeightPx, screenHeightPx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
HeroGradient(
|
||||||
|
colors = artwork,
|
||||||
|
scrollPx = { scrollPx },
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(statusBarTop + 8.dp)
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
listOf(Color.Black.copy(alpha = 0.55f), Color.Transparent),
|
||||||
|
),
|
||||||
|
).graphicsLayer { alpha = 1f - titleFraction },
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(Modifier.fillMaxSize()) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(headerColor)
|
.drawBehind { drawRect(barColor.copy(alpha = titleFraction)) }
|
||||||
.padding(top = statusBarTop),
|
.padding(top = statusBarTop),
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
@@ -213,21 +266,13 @@ fun LibraryScreen(
|
|||||||
syncing = syncing,
|
syncing = syncing,
|
||||||
onSync = onSync,
|
onSync = onSync,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
selectedColor = headerColor,
|
selectedColor = selectedColor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(Modifier.fillMaxWidth().weight(1f)) {
|
|
||||||
if (artwork != null) {
|
|
||||||
HeroGradient(
|
|
||||||
colors = artwork,
|
|
||||||
scrollPx = { 0f },
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||||
) { page ->
|
) { page ->
|
||||||
when (LibraryTab.entries[page]) {
|
when (LibraryTab.entries[page]) {
|
||||||
LibraryTab.Albums -> AlbumsContent(albums, coverArt, bottomInset, onAlbumClick)
|
LibraryTab.Albums -> AlbumsContent(albums, coverArt, bottomInset, onAlbumClick)
|
||||||
|
|||||||
@@ -210,6 +210,18 @@ val LocalPlayerArtwork = compositionLocalOf<ArtworkColors?> { null }
|
|||||||
|
|
||||||
fun playerSurfaceColor(artwork: ArtworkColors): Color = lerp(artwork.scheme.surfaceContainerHigh, artwork.gradientHigh, 0.5f)
|
fun playerSurfaceColor(artwork: ArtworkColors): Color = lerp(artwork.scheme.surfaceContainerHigh, artwork.gradientHigh, 0.5f)
|
||||||
|
|
||||||
|
fun heroBarColor(
|
||||||
|
artwork: ArtworkColors?,
|
||||||
|
scrollPx: Float,
|
||||||
|
barHeightPx: Float,
|
||||||
|
screenHeightPx: Float,
|
||||||
|
): Color {
|
||||||
|
if (artwork == null || screenHeightPx <= 0f) return Color.Black
|
||||||
|
val mid = scrollPx + barHeightPx / 2f
|
||||||
|
val gradientHeightPx = screenHeightPx * PERIOD_SCREENS
|
||||||
|
return lerp(artwork.gradientColorAt(mid / gradientHeightPx), artwork.darkPrimary, heroDarkenAt(mid, screenHeightPx))
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HeroGradient(
|
fun HeroGradient(
|
||||||
colors: ArtworkColors?,
|
colors: ArtworkColors?,
|
||||||
|
|||||||
Reference in New Issue
Block a user