mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
drive overscroll from the factory, fix the gradient height, share the bar colour
This commit is contained in:
@@ -4,93 +4,108 @@ import androidx.compose.animation.core.Animatable
|
|||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.foundation.LocalOverscrollFactory
|
import androidx.compose.foundation.LocalOverscrollFactory
|
||||||
|
import androidx.compose.foundation.OverscrollEffect
|
||||||
|
import androidx.compose.foundation.OverscrollFactory
|
||||||
import androidx.compose.foundation.layout.Box
|
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.remember
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
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.TransformOrigin
|
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.drawscope.scale
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
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.input.nestedscroll.nestedScroll
|
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 kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
private const val MAX_STRETCH_FRACTION = 0.30f
|
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 class StretchOverscrollEffect : OverscrollEffect {
|
||||||
|
private val overscroll = mutableFloatStateOf(0f)
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val node: DelegatableNode get() = drawNode
|
||||||
|
|
||||||
|
override val isInProgress: Boolean get() = overscroll.floatValue != 0f
|
||||||
|
|
||||||
|
override fun applyToScroll(
|
||||||
|
delta: Offset,
|
||||||
|
source: NestedScrollSource,
|
||||||
|
performScroll: (Offset) -> Offset,
|
||||||
|
): Offset {
|
||||||
|
if (delta.y == 0f) return performScroll(delta)
|
||||||
|
var remainingY = delta.y
|
||||||
|
if (overscroll.floatValue != 0f) {
|
||||||
|
val current = overscroll.floatValue
|
||||||
|
val next = current + remainingY
|
||||||
|
val clamped = if (current > 0f) next.coerceAtLeast(0f) else next.coerceAtMost(0f)
|
||||||
|
remainingY -= clamped - current
|
||||||
|
overscroll.floatValue = clamped
|
||||||
|
}
|
||||||
|
val consumed = performScroll(Offset(delta.x, remainingY))
|
||||||
|
val leftover = remainingY - consumed.y
|
||||||
|
if (leftover != 0f) {
|
||||||
|
val current = overscroll.floatValue
|
||||||
|
val next = current + leftover
|
||||||
|
overscroll.floatValue =
|
||||||
|
when {
|
||||||
|
current > 0f -> next.coerceAtLeast(0f)
|
||||||
|
current < 0f -> next.coerceAtMost(0f)
|
||||||
|
else -> next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Offset(consumed.x, delta.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun applyToFling(
|
||||||
|
velocity: Velocity,
|
||||||
|
performFling: suspend (Velocity) -> Velocity,
|
||||||
|
) {
|
||||||
|
if (overscroll.floatValue != 0f) {
|
||||||
|
Animatable(overscroll.floatValue).animateTo(0f, spring(stiffness = Spring.StiffnessMedium)) {
|
||||||
|
overscroll.floatValue = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
performFling(velocity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StretchOverscroll(
|
fun StretchOverscroll(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val overscroll = remember { Animatable(0f) }
|
CompositionLocalProvider(LocalOverscrollFactory provides StretchOverscrollFactory) {
|
||||||
val scope = rememberCoroutineScope()
|
Box(modifier) { content() }
|
||||||
|
|
||||||
val connection =
|
|
||||||
remember(overscroll) {
|
|
||||||
object : NestedScrollConnection {
|
|
||||||
override fun onPreScroll(
|
|
||||||
available: Offset,
|
|
||||||
source: NestedScrollSource,
|
|
||||||
): Offset {
|
|
||||||
val current = overscroll.value
|
|
||||||
if (current == 0f || available.y == 0f) return Offset.Zero
|
|
||||||
val next = current + available.y
|
|
||||||
scope.launch {
|
|
||||||
overscroll.snapTo(if (current > 0f) next.coerceAtLeast(0f) else next.coerceAtMost(0f))
|
|
||||||
}
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostScroll(
|
|
||||||
consumed: Offset,
|
|
||||||
available: Offset,
|
|
||||||
source: NestedScrollSource,
|
|
||||||
): Offset {
|
|
||||||
val current = overscroll.value
|
|
||||||
if (available.y == 0f) return Offset.Zero
|
|
||||||
val next = current + available.y
|
|
||||||
scope.launch {
|
|
||||||
overscroll.snapTo(if (current > 0f) next.coerceAtLeast(0f) else next.coerceAtMost(0f))
|
|
||||||
}
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPostFling(
|
|
||||||
consumed: Velocity,
|
|
||||||
available: Velocity,
|
|
||||||
): Velocity {
|
|
||||||
overscroll.animateTo(0f, spring(stiffness = Spring.StiffnessMedium))
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CompositionLocalProvider(LocalOverscrollFactory provides null) {
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
modifier
|
|
||||||
.nestedScroll(connection)
|
|
||||||
.graphicsLayer {
|
|
||||||
val raw = overscroll.value
|
|
||||||
if (raw == 0f) {
|
|
||||||
scaleY = 1f
|
|
||||||
translationY = 0f
|
|
||||||
} else {
|
|
||||||
val limit = size.height * MAX_STRETCH_FRACTION
|
|
||||||
val capped = raw.coerceIn(-limit, limit)
|
|
||||||
val resisted = capped / (1f + abs(capped) / (size.height * 0.5f))
|
|
||||||
scaleY = 1f + abs(resisted) / size.height
|
|
||||||
translationY = resisted
|
|
||||||
transformOrigin = TransformOrigin(0.5f, if (resisted > 0f) 0f else 1f)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredHeight
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -136,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 titleTopPx by remember { mutableFloatStateOf(0f) }
|
var titleBottomPx by remember { mutableFloatStateOf(0f) }
|
||||||
val scrollPx by remember {
|
val scrollPx by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val index = listState.firstVisibleItemIndex
|
val index = listState.firstVisibleItemIndex
|
||||||
@@ -146,10 +147,10 @@ fun AlbumDetailScreen(
|
|||||||
}
|
}
|
||||||
val barFraction by remember {
|
val barFraction by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (titleTopPx <= 0f || fadeDistancePx <= 0f) {
|
if (titleBottomPx <= 0f || fadeDistancePx <= 0f) {
|
||||||
0f
|
0f
|
||||||
} else {
|
} else {
|
||||||
((scrollPx - (titleTopPx - barHeightPx)) / fadeDistancePx).coerceIn(0f, 1f)
|
((scrollPx - (titleBottomPx - barHeightPx)) / fadeDistancePx).coerceIn(0f, 1f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,7 +165,7 @@ fun AlbumDetailScreen(
|
|||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(maxHeight * GRADIENT_SCREENS)
|
.requiredHeight(maxHeight * GRADIENT_SCREENS)
|
||||||
.offset { IntOffset(0, -scrollPx.roundToInt()) }
|
.offset { IntOffset(0, -scrollPx.roundToInt()) }
|
||||||
.alpha(gradientAlpha),
|
.alpha(gradientAlpha),
|
||||||
)
|
)
|
||||||
@@ -188,7 +189,10 @@ fun AlbumDetailScreen(
|
|||||||
onDownload = onDownload,
|
onDownload = onDownload,
|
||||||
onMore = onMore,
|
onMore = onMore,
|
||||||
topInset = statusBarDp,
|
topInset = statusBarDp,
|
||||||
titleModifier = Modifier.onGloballyPositioned { titleTopPx = it.positionInParent().y },
|
titleModifier =
|
||||||
|
Modifier.onGloballyPositioned {
|
||||||
|
titleBottomPx = it.positionInParent().y + it.size.height
|
||||||
|
},
|
||||||
modifier = Modifier.onSizeChanged { headerHeightPx = it.height.toFloat() },
|
modifier = Modifier.onSizeChanged { headerHeightPx = it.height.toFloat() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -217,6 +221,18 @@ fun AlbumDetailScreen(
|
|||||||
).alpha(1f - barFraction),
|
).alpha(1f - barFraction),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(statusBarDp)
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
listOf(Color.Black.copy(alpha = 0.55f), Color.Transparent),
|
||||||
|
),
|
||||||
|
).alpha(1f - barFraction),
|
||||||
|
)
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
@@ -236,7 +252,6 @@ fun AlbumDetailScreen(
|
|||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopStart)
|
||||||
.background(Color.Black.copy(alpha = barFraction))
|
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = null,
|
indication = null,
|
||||||
@@ -264,7 +279,7 @@ private fun AlbumHeader(
|
|||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 24.dp, end = 24.dp, top = topInset + 8.dp, bottom = 16.dp),
|
.padding(start = 24.dp, end = 24.dp, top = topInset + 24.dp, bottom = 16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
CoverArt(
|
CoverArt(
|
||||||
|
|||||||
Reference in New Issue
Block a user