mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
render the stretch from the wrapper layer, later bar fade, punchier blobs
This commit is contained in:
@@ -10,51 +10,26 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.scale
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.node.DelegatableNode
|
||||
import androidx.compose.ui.node.DrawModifierNode
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import kotlin.math.abs
|
||||
|
||||
private const val MAX_STRETCH_FRACTION = 0.30f
|
||||
|
||||
private object StretchOverscrollFactory : OverscrollFactory {
|
||||
override fun createOverscrollEffect(): OverscrollEffect = StretchOverscrollEffect()
|
||||
|
||||
override fun hashCode(): Int = 0x5EED_5EED
|
||||
|
||||
override fun equals(other: Any?): Boolean = other is StretchOverscrollFactory
|
||||
}
|
||||
private const val MAX_STRETCH_FRACTION = 0.14f
|
||||
|
||||
private class StretchOverscrollEffect : OverscrollEffect {
|
||||
private val overscroll = mutableFloatStateOf(0f)
|
||||
val overscroll = mutableFloatStateOf(0f)
|
||||
var limitPx: Float = 1f
|
||||
|
||||
private val drawNode =
|
||||
object : Modifier.Node(), DrawModifierNode {
|
||||
override fun ContentDrawScope.draw() {
|
||||
val raw = overscroll.floatValue
|
||||
if (raw == 0f) {
|
||||
drawContent()
|
||||
return
|
||||
}
|
||||
val limit = size.height * MAX_STRETCH_FRACTION
|
||||
val capped = raw.coerceIn(-limit, limit)
|
||||
val resisted = capped / (1f + abs(capped) / (size.height * 0.5f))
|
||||
val pivot = Offset(size.width / 2f, if (resisted > 0f) 0f else size.height)
|
||||
scale(scaleX = 1f, scaleY = 1f + abs(resisted) / size.height, pivot = pivot) {
|
||||
translate(left = 0f, top = resisted) {
|
||||
this@draw.drawContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val emptyNode = object : Modifier.Node() {}
|
||||
|
||||
override val node: DelegatableNode get() = drawNode
|
||||
override val node: DelegatableNode get() = emptyNode
|
||||
|
||||
override val isInProgress: Boolean get() = overscroll.floatValue != 0f
|
||||
|
||||
@@ -76,7 +51,7 @@ private class StretchOverscrollEffect : OverscrollEffect {
|
||||
val leftover = remainingY - consumed.y
|
||||
if (leftover != 0f) {
|
||||
val current = overscroll.floatValue
|
||||
val next = current + leftover
|
||||
val next = (current + leftover).coerceIn(-limitPx, limitPx)
|
||||
overscroll.floatValue =
|
||||
when {
|
||||
current > 0f -> next.coerceAtLeast(0f)
|
||||
@@ -92,9 +67,10 @@ private class StretchOverscrollEffect : OverscrollEffect {
|
||||
performFling: suspend (Velocity) -> Velocity,
|
||||
) {
|
||||
if (overscroll.floatValue != 0f) {
|
||||
Animatable(overscroll.floatValue).animateTo(0f, spring(stiffness = Spring.StiffnessMedium)) {
|
||||
overscroll.floatValue = value
|
||||
}
|
||||
Animatable(overscroll.floatValue)
|
||||
.animateTo(0f, spring(stiffness = Spring.StiffnessMedium)) {
|
||||
overscroll.floatValue = value
|
||||
}
|
||||
}
|
||||
performFling(velocity)
|
||||
}
|
||||
@@ -105,7 +81,34 @@ fun StretchOverscroll(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
CompositionLocalProvider(LocalOverscrollFactory provides StretchOverscrollFactory) {
|
||||
Box(modifier) { content() }
|
||||
val effect = remember { StretchOverscrollEffect() }
|
||||
val factory =
|
||||
remember {
|
||||
object : OverscrollFactory {
|
||||
override fun createOverscrollEffect(): OverscrollEffect = effect
|
||||
|
||||
override fun hashCode(): Int = 0x5EED_1234
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === this
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalOverscrollFactory provides factory) {
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.onSizeChanged { effect.limitPx = (it.height * MAX_STRETCH_FRACTION).coerceAtLeast(1f) }
|
||||
.graphicsLayer {
|
||||
val value = effect.overscroll.floatValue
|
||||
if (value == 0f) return@graphicsLayer
|
||||
val height = size.height
|
||||
val resisted = value / (1f + abs(value) / (height * 0.35f))
|
||||
scaleY = 1f + abs(resisted) / height
|
||||
translationY = resisted
|
||||
transformOrigin = TransformOrigin(0.5f, if (resisted > 0f) 0f else 1f)
|
||||
},
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ fun AlbumDetailScreen(
|
||||
|
||||
var headerHeightPx by remember { mutableFloatStateOf(0f) }
|
||||
var rowHeightPx by remember { mutableFloatStateOf(0f) }
|
||||
var titleBottomPx by remember { mutableFloatStateOf(0f) }
|
||||
var controlsTopPx by remember { mutableFloatStateOf(0f) }
|
||||
val scrollPx by remember {
|
||||
derivedStateOf {
|
||||
val index = listState.firstVisibleItemIndex
|
||||
@@ -147,10 +147,10 @@ fun AlbumDetailScreen(
|
||||
}
|
||||
val barFraction by remember {
|
||||
derivedStateOf {
|
||||
if (titleBottomPx <= 0f || fadeDistancePx <= 0f) {
|
||||
if (controlsTopPx <= 0f || fadeDistancePx <= 0f) {
|
||||
0f
|
||||
} else {
|
||||
((scrollPx - (titleBottomPx - barHeightPx)) / fadeDistancePx).coerceIn(0f, 1f)
|
||||
((scrollPx - (controlsTopPx - barHeightPx)) / fadeDistancePx).coerceIn(0f, 1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,9 +189,9 @@ fun AlbumDetailScreen(
|
||||
onDownload = onDownload,
|
||||
onMore = onMore,
|
||||
topInset = statusBarDp,
|
||||
titleModifier =
|
||||
controlsModifier =
|
||||
Modifier.onGloballyPositioned {
|
||||
titleBottomPx = it.positionInParent().y + it.size.height
|
||||
controlsTopPx = it.positionInParent().y
|
||||
},
|
||||
modifier = Modifier.onSizeChanged { headerHeightPx = it.height.toFloat() },
|
||||
)
|
||||
@@ -272,7 +272,7 @@ private fun AlbumHeader(
|
||||
onDownload: () -> Unit,
|
||||
onMore: () -> Unit,
|
||||
topInset: Dp,
|
||||
titleModifier: Modifier = Modifier,
|
||||
controlsModifier: Modifier = Modifier,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
@@ -294,7 +294,6 @@ private fun AlbumHeader(
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = titleModifier,
|
||||
)
|
||||
val subtitle =
|
||||
listOfNotNull(
|
||||
@@ -314,6 +313,7 @@ private fun AlbumHeader(
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Row(
|
||||
modifier = controlsModifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.palette.graphics.Palette
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.compose.LocalPlatformContext
|
||||
@@ -45,8 +46,7 @@ fun artworkColorsFromSeed(seed: Int): ArtworkColors {
|
||||
sat: Float,
|
||||
l: Float,
|
||||
) = Color.hsl(h % 360f, sat.coerceIn(0f, 1f), l.coerceIn(0f, 1f))
|
||||
|
||||
val primary = tone(hue, s, 0.62f)
|
||||
val primary = toneMaxLuminance(hue, s, 0.62f, 0.45f)
|
||||
val background = tone(hue, (s * 0.35f).coerceAtMost(0.20f), 0.06f)
|
||||
val onBackground = tone(hue, (s * 0.10f).coerceAtMost(0.08f), 0.95f)
|
||||
|
||||
@@ -82,12 +82,28 @@ fun artworkColorsFromSeed(seed: Int): ArtworkColors {
|
||||
|
||||
return ArtworkColors(
|
||||
scheme = scheme,
|
||||
gradientHigh = tone(hue, (s * 0.8f).coerceAtMost(0.60f), 0.30f),
|
||||
gradientLow = tone(hue, s * 0.25f, 0.04f),
|
||||
accents = listOf(tone(hue, s, 0.78f), tone(hue, s * 0.9f, 0.20f)),
|
||||
gradientHigh = tone(hue, (s * 0.6f).coerceAtMost(0.50f), 0.22f),
|
||||
gradientLow = tone(hue, s * 0.30f, 0.03f),
|
||||
accents = listOf(tone(hue, s * 0.75f, 0.95f), tone(hue, s, 0.08f)),
|
||||
)
|
||||
}
|
||||
|
||||
private fun toneMaxLuminance(
|
||||
h: Float,
|
||||
s: Float,
|
||||
lightness: Float,
|
||||
maxLuminance: Float,
|
||||
): Color {
|
||||
var l = lightness
|
||||
var color = Color.hsl(h % 360f, s.coerceIn(0f, 1f), l.coerceIn(0f, 1f))
|
||||
var guard = 0
|
||||
while (color.luminance() > maxLuminance && guard++ < 30) {
|
||||
l = (l - 0.02f).coerceAtLeast(0.05f)
|
||||
color = Color.hsl(h % 360f, s.coerceIn(0f, 1f), l)
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberArtworkColors(ref: CoverArtRef?): ArtworkColors? {
|
||||
val seed by rememberArtworkSeed(ref)
|
||||
@@ -169,9 +185,9 @@ fun HeroGradient(
|
||||
drawRect(
|
||||
brush =
|
||||
Brush.radialGradient(
|
||||
colors = listOf(accent.copy(alpha = 0.55f), accent.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.18f, size.height * 0.03f),
|
||||
radius = size.width * 0.95f,
|
||||
colors = listOf(accent.copy(alpha = 0.85f), accent.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.20f, size.height * 0.00f),
|
||||
radius = size.width * 0.60f,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -179,9 +195,9 @@ fun HeroGradient(
|
||||
drawRect(
|
||||
brush =
|
||||
Brush.radialGradient(
|
||||
colors = listOf(accent.copy(alpha = 0.50f), accent.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.92f, size.height * 0.28f),
|
||||
radius = size.width * 0.85f,
|
||||
colors = listOf(accent.copy(alpha = 0.80f), accent.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.90f, size.height * 0.26f),
|
||||
radius = size.width * 0.70f,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -189,18 +205,18 @@ fun HeroGradient(
|
||||
drawRect(
|
||||
brush =
|
||||
Brush.radialGradient(
|
||||
colors = listOf(accent.copy(alpha = 0.30f), accent.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.85f, size.height * 0.02f),
|
||||
radius = size.width * 0.70f,
|
||||
colors = listOf(accent.copy(alpha = 0.55f), accent.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.82f, size.height * 0.00f),
|
||||
radius = size.width * 0.45f,
|
||||
),
|
||||
)
|
||||
}
|
||||
drawRect(
|
||||
brush =
|
||||
Brush.radialGradient(
|
||||
colors = listOf(high.copy(alpha = 0.45f), high.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.35f, size.height * 0.52f),
|
||||
radius = size.width * 0.95f,
|
||||
colors = listOf(high.copy(alpha = 0.55f), high.copy(alpha = 0f)),
|
||||
center = Offset(size.width * 0.40f, size.height * 0.50f),
|
||||
radius = size.width * 0.80f,
|
||||
),
|
||||
)
|
||||
drawRect(
|
||||
|
||||
Reference in New Issue
Block a user