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