mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +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.foundation.LocalOverscrollFactory
|
||||
import androidx.compose.foundation.OverscrollEffect
|
||||
import androidx.compose.foundation.OverscrollFactory
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
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.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 kotlinx.coroutines.launch
|
||||
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 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
|
||||
fun StretchOverscroll(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val overscroll = remember { Animatable(0f) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
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()
|
||||
}
|
||||
CompositionLocalProvider(LocalOverscrollFactory provides StretchOverscrollFactory) {
|
||||
Box(modifier) { content() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -136,7 +137,7 @@ fun AlbumDetailScreen(
|
||||
|
||||
var headerHeightPx 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 {
|
||||
derivedStateOf {
|
||||
val index = listState.firstVisibleItemIndex
|
||||
@@ -146,10 +147,10 @@ fun AlbumDetailScreen(
|
||||
}
|
||||
val barFraction by remember {
|
||||
derivedStateOf {
|
||||
if (titleTopPx <= 0f || fadeDistancePx <= 0f) {
|
||||
if (titleBottomPx <= 0f || fadeDistancePx <= 0f) {
|
||||
0f
|
||||
} else {
|
||||
((scrollPx - (titleTopPx - barHeightPx)) / fadeDistancePx).coerceIn(0f, 1f)
|
||||
((scrollPx - (titleBottomPx - barHeightPx)) / fadeDistancePx).coerceIn(0f, 1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +165,7 @@ fun AlbumDetailScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(maxHeight * GRADIENT_SCREENS)
|
||||
.requiredHeight(maxHeight * GRADIENT_SCREENS)
|
||||
.offset { IntOffset(0, -scrollPx.roundToInt()) }
|
||||
.alpha(gradientAlpha),
|
||||
)
|
||||
@@ -188,7 +189,10 @@ fun AlbumDetailScreen(
|
||||
onDownload = onDownload,
|
||||
onMore = onMore,
|
||||
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() },
|
||||
)
|
||||
}
|
||||
@@ -217,6 +221,18 @@ fun AlbumDetailScreen(
|
||||
).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(
|
||||
title = {
|
||||
Text(
|
||||
@@ -236,7 +252,6 @@ fun AlbumDetailScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.background(Color.Black.copy(alpha = barFraction))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
@@ -264,7 +279,7 @@ private fun AlbumHeader(
|
||||
modifier =
|
||||
modifier
|
||||
.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,
|
||||
) {
|
||||
CoverArt(
|
||||
|
||||
Reference in New Issue
Block a user